• Contact
  • Journal
  • Home
  • Arcade
  • DonkeyKong
  • Code
  • Site
  • Page
  • Amiga
    • Rainbow Islands
      • 1loader-dec
      • 1loader
      • boot
      • orig-boot
      • Journal
  • Arcade
    • Asteroids 80%
      • Hardware
      • RAMUse
      • Code

      • DVG
      • VectorROM

      • Journal
    • Crazy Climber 1%
      • BigSpriteGraphics.pdf
      • Buildings.pdf
      • cclimber.asm
      • CharEnc.inc
      • CrazyClimberMemoryMapInfo.pdf
      • GraphicsAndCharacterAssetInfo.pdf
      • MemoryMap.inc
    • Defender 75%
      • RAMUse
      • Hardware
      • Bank1
      • Bank2
      • Bank3
      • Bank7
      • BankFixed
      • Mapping.txt
      • SoundHardware
      • SoundRAMUse
      • SoundCode
      • Defender-Theory-Early.pdf
      • Defender-Theory-Later.pdf
      • Defender.CPU.jpg
      • Defender.ROM.B&W.jpg
      • Defender.Vid.B&W.jpg
      • SoundROM.txt
    • DonkeyKong
      • Hardware
      • RAMUse
      • Code
    • Frogger (Sound)
      • SoundHardware
      • SoundRAMUse
      • SoundCode

      • Hardware
      • RAMUse
      • Code

      • GFX

      • Journal
    • Galaga 5%
      • CPU1 (Main)
      • CPU2 (Secondary)
      • CPU3 (Sound)
      • GFX1 (Characters)
      • GFX2 (Sprites)
      • PROMcolors
      • PROMpaletteChar
      • PROMpaletteSprite

      • CPU1Fix

      • Journal
    • Moon Patrol 75%
      • Hardware
      • RAMUse
      • Code

      • GFX1 (Text)
      • GFX2 (Sprites)
      • GFX3 (Mountains)
      • GFX4 (Hills)
      • GFX5 (City)
      • ImageBackgroundColors
      • SpriteColors
      • SpriteColorSets
      • TextColors

      • MoonPatrolSound
      • SoundHardware
      • SoundRAMUse
      • SoundCode

      • Journal
    • Omega Race 10%
      • Hardware
      • RAMUse
      • MainBoard

      • SoundHardware
      • SoundRAMUse
      • SoundBoard

      • DVGPROM
      • VectorROM

      • Journal
    • Phoenix 90%
      • Hardware
      • RAMUse
      • Code

      • Background
      • Foreground

      • Journal

      • Scramble HHi
        • phoenixj-func-main2.pdf
        • phoenixj-func-main2.scap
        • phoenixj.asm-may2025.txt
        • phoenixj.asm-may2025.txt.pdf
    • Space Invaders
      • invaders.asm
      • Hardware
      • RAMUse
      • Code

      • Journal
    • The Pit
      • Hardware
      • RAMUse
      • Code
    • Time Pilot (Sound)
      • SoundHardware
      • SoundRAMUse
      • SoundCode

      • Hardware
      • RAMUse
      • Code

      • Journal
    • Sea Wolf 1%
      • Hardware
      • RAMUse
      • Code

      • Journal
    • Scramble 1%
      • SoundHardware
      • SoundRAMUse
      • SoundCode

      • Journal
  • Atari2600
    • Stella (Hardware Info)
    • Asteroids 5%
      • RAMUse
      • Code

      • Journal
    • Battle Zone 1%
      • RAMUse
      • Code

      • Journal
    • Chess 1%
      • RAMUse
      • Code

      • Journal
    • Combat 10%
      • RAMUse
      • Code
      • CodePAL

      • Journal
    • Double Gap
      • Code
      • DoubleGap.asm

      • Journal
    • Entombed 1%
      • RAMUse
      • Code

      • Journal
    • ET 1%
      • RAMUse
      • Code

      • Journal
    • Burger Time 1%
      • RAMUse
      • CodeBank0
      • CodeBank1
      • CodeBank2
      • CodeBank3
      • CodeBank4
      • CodeBank5
      • CodeBank6
      • CodeBank7

      • Journal
    • Missile Command 1%
      • RAMUse
      • Code

      • Journal
    • Space Invaders 1%
      • RAMUse
      • Code

      • Journal
  • ColossalCave
    • CodeOrg
    • DataOrg

    • Code350
    • Data350
  • CoCo
    • Hardware
    • Early Work
    • Pyramid
      • RAMUse
      • Code

      • Journal
    • Raaka Tu
      • RAMUse
      • Code

      • Journal
    • Bedlam
      • RAMUse
      • Code

      • Journal
    • Madness & Minotaur
      • Walk Through
        • after_start.txt
        • after_start.cas
        • after_1.txt
        • after_1.cas
        • after_2.txt
        • after_2.cas
        • after_3.txt
        • after_3.cas
        • after_4.txt
        • after_4.cas
        • after_5.txt
        • after_5.cas
        • after_6.txt
        • after_6.cas
        • after_7.txt
        • after_7.cas
        • after_8.txt
        • after_8.cas
        • after_9.txt
        • after_9.cas
        • after_10.txt
        • after_10.cas
        • after_11.txt
        • after_11.cas
        • after_12.txt
        • after_12.cas
        • after_13.txt
        • after_13.cas
        • after_14.txt
        • after_14.cas
        • after_15.txt
        • after_15.cas
        • after_16.txt
        • after_16.cas
        • after_17.txt
        • after_17.cas
        • after_18.txt
        • after_18.cas
        • after_19.txt
        • after_19.cas
        • after_20.txt
        • after_20.cas
        • after_21.txt
        • after_21.cas
        • after_22.txt
        • after_22.cas
        • after_23.txt
        • after_23.cas
        • after_24.txt
        • after_24.cas
        • after_25.txt
        • after_25.cas
      • RAMUse
      • Code

      • SaveGameViewer

      • Journal
    • Mega-Bug
      • RAMUse
      • Code

      • Journal
    • Popcorn
      • RAMUse
      • Code

      • Journal
    • Daggorath
      • RAMUse
      • Code

      • Level Maps

      • Journal
    • Downland 5%
      • RAMUse
      • Code

      • Journal
    • Audio Analyzer 5%
      • RAMUse
      • Code

      • Journal
    • Doubleback
      • RAMUse
      • Code

      • Journal
  • NES
    • Zelda 5%
      • Hardware
      • RAMUse
      • Bank0
      • Bank1
      • Bank2
      • Bank3
      • Bank4
      • Bank5
      • Bank6
      • Bank7

      • Journal
    • Kid Icarus 1%
      • Hardware
      • RAMUse
      • Bank0
      • Bank1
      • Bank2
      • Bank3
      • Bank4
      • Bank5
      • Bank6
      • Bank7

      • Journal
  • Gameboy
    • Hardware
    • Link's Awakening 1%
      • RAMUse
      • Bank00
      • Bank01
      • Bank02
      • Bank03
      • Bank04
      • Bank05
      • Bank06
      • Bank07
      • Bank08
      • Bank09
      • Bank0A
      • Bank0B
      • Bank0C
      • Bank0D
      • Bank0E
      • Bank0F
      • Bank10
      • Bank11
      • Bank12
      • Bank13
      • Bank14
      • Bank15
      • Bank16
      • Bank17
      • Bank18
      • Bank19
      • Bank1A
      • Bank1B
      • Bank1C
      • Bank1D
      • Bank1E
      • Bank1F

      • Journal
    • Tetris 1%
      • RAMUse
      • Code

      • Journal
  • TRS80
    • HardwareLevel2
    • HardwareLevel1
    • HardwareDisk
    • HauntedHouse
      • RAMUse1
      • Code1
      • RAMUse2
      • Code2

      • Journal
    • Pyramid
      • RAMUse
      • Code

      • RAMUse1
      • Code1

      • Journal
    • RaakaTu
      • RAMUse
      • Code

      • Journal
    • Bedlam
      • RAMUse
      • Code

      • Journal
    • Xenos
      • RAMUse
      • Code
      • Section1
      • Section2
      • Section3
      • Section4
      • Section5
      • Section6
      • Section7
      • Section8
      • Section9
      • SSVDOBJS
      • USVDOBJS

      • Journal
  • Virus
    • Morris Worm 1%
      • Journal
    • Stoned
      • Journal

  • Tools
    • Blend
      • blend.zip

      • Journal
  • Donkey Kong Main CPU (Z80)

Donkey Kong Main CPU (Z80)

Hardware Info

RAM Usage

; Reset entry: run the power-on initialisation, then fall into the forever main loop.
boot:
0000: 3E 00           LD      A,$00               
0002: 32 84 7D        LD      (nmiEnable),A       ; mask the vblank NMI before initialising
0005: C3 66 02        JP      clearRamAndInitHardware; power-on setup, then the main loop

; Caller-skip guard: proceed only while a credited game is in play. It reads bit 0 of
; attract -- non-zero while no credited game is running, zero once a coin has been taken
; -- and tells its caller to run its body or skip the rest of it. Mind the polarity: this
; one proceeds while the bit is CLEAR, the opposite of the guard keyed on Mario being
; alive. A leaf: reads one byte, writes none.
gameActiveGuard:
0008: 3A 07 60        LD      A,(workRam+7)       ; read the attract flag
000B: 0F              RRCA                        ; shift the attract bit into carry
000C: D0              RET     NC                  ; clear: a game is credited, let the caller run
000D: 33              INC     SP                  ; no game: step over the caller's return address
000E: 33              INC     SP                  
000F: C9              RET                         ; so the caller's remaining work is abandoned

; Answer "is Mario alive?" for the caller. Reads marioActive, where bit 0 set means alive
; and being processed. If it is clear the routine steps over the caller's return address
; so the caller's remaining work is abandoned. The player-motion, hammer and bonus
; updates all open with this test.
marioActiveGuard:
0010: 3A 00 62        LD      A,(workRam+200)     ; read the player-alive flag
0013: 0F              RRCA                        
0014: D8              RET     C                   ; Mario is alive, let the caller run
0015: 33              INC     SP                  ; dead: discard the caller's return address
0016: 33              INC     SP                  
0017: C9              RET                         ; abandon the rest of the caller's frame

; The shared "wait N frames, then act" gate. Counts substateTimer down by one in place
; and reports whether that tick is the one that brought it to zero. On expiry the caller
; goes on and does its work; while the counter is still above zero the caller abandons
; the rest of its frame. A leaf: it reads and writes substateTimer and nothing else.
tickSubstateTimer:
0018: 21 09 60        LD      HL,$6009            ; point at the sub-state frame countdown
001B: 35              DEC     (HL)                ; tick one frame off the wait
001C: C8              RET     Z                   ; the wait just expired, let the caller act
001D: 33              INC     SP                  ; still waiting: skip the caller's remainder
001E: 33              INC     SP                  
001F: C9              RET                         

; Fast half of the two-level sub-state countdown. Every call takes one off
; substateTimerLo; while that is still counting the caller is skipped. Only on its
; underflow does control chain into the slow half, which steps substateTimer and reports
; ITS expiry -- so the caller's remainder runs only on the frame both counters expire
; together.
tickSubstatePrescaler:
0020: 21 08 60        LD      HL,$6008            ; the fast half of the sub-state timer
0023: 35              DEC     (HL)                ; tick the prescaler
0024: 28 F2           JR      Z,tickSubstateTimer ; on underflow, chain into the slow half

loc_0026:
0026: E1              POP     HL                  ; still counting: discard the caller's return
0027: C9              RET                         

; The inline-jump-table trampoline. A caller arrives through a one-byte restart with the
; target index in A, having laid a list of 2-byte little-endian addresses immediately
; after that opcode -- so the pushed return address points AT the table. This recovers
; that base off the stack, reads the word at table[index], and jumps there. The index
; doubling is an 8-bit result, so the address is base + (2*index & 0xff) and an index of
; 0x80 wraps back to entry 0. Every computed dispatch in the game comes through here: the
; per-frame game-state table, the in-game sub-state table, the opening cutscene, the
; difficulty guards.
dispatchInlineJumpTable:
0028: 87              ADD     A,A                 ; two bytes per table entry
0029: E1              POP     HL                  ; the return address IS the table base
002A: 5F              LD      E,A                 
002B: 16 00           LD      D,$00               
002D: C3 32 00        JP      loc_0032            ; 

; The "does THIS board want it?" gate. The caller passes a per-board mask -- bit 0 for
; 25m through bit 3 for 100m -- and this rotates the mask right by the current board
; number so the board's own bit falls into carry. Set, and the caller carries on; clear,
; and the caller's return is stepped over so its next action never runs. It rotates
; rather than shifts, so a board number of 0 would come back holding bit 7.
boardBitGate:
0030: 18 12           JR      loc_0044            ; restart vector for the per-board gate

loc_0032:
0032: 19              ADD     HL,DE               ; index the entry for this dispatch value
0033: 5E              LD      E,(HL)              ; fetch the target address, low byte
0034: 23              INC     HL                  
0035: 56              LD      D,(HL)              ; then its high byte
0036: EB              EX      DE,HL               
0037: E9              JP      (HL)                ; enter the selected handler

; Add one signed delta into the SAME field of all ten sprite-object records at once,
; shifting a whole column of them together. It fixes the two numbers that make the
; general strided add specific to this block -- a stride of 4, one record, and a count
; of 10 -- and runs the shared loop. Aimed at a record's first byte it slides the
; group's X; three bytes in, its Y. Board and cutscene setup use it to reposition a row
; of scenery or a staged figure by one number.
addToSpriteObjectColumn:
0038: 11 04 00        LD      DE,$0004            ; four bytes per sprite-object record
003B: 06 0A           LD      B,$0A               ; ten records in the block

; Add a signed byte into each of a run of bytes spaced at a fixed stride. Its usual job
; is nudging one column of the sprite shadow buffer -- the X or the Y field of ten
; records, four bytes apart -- by a single delta during board and cutscene setup; the
; heaviest use passes -4, so it decrements. Each add is 8-bit and wraps. The count is
; tested after a pass rather than before the first, so a count of 0 on entry means 256
; passes, not none.
addStrided:
003D: 79              LD      A,C                 ; the caller's signed delta
003E: 86              ADD     A,(HL)              ; add it into this record's field
003F: 77              LD      (HL),A              
0040: 19              ADD     HL,DE               ; step to the same field of the next record
0041: 10 FA           DJNZ    addStrided          ; 
0043: C9              RET                         

loc_0044:
0044: 21 27 62        LD      HL,$6227            ; point at the current board number
0047: 46              LD      B,(HL)              ; rotate the mask by the board number

loc_0048:
0048: 0F              RRCA                        
0049: 10 FD           DJNZ    loc_0048            ; so this board's bit lands in carry
004B: D8              RET     C                   ; this board wants it: let the caller run
004C: E1              POP     HL                  ; not this board: discard the caller's return
004D: C9              RET                         

; Copy 40 bytes -- ten 4-byte sprite records -- from the caller's source pointer into
; spriteObjBlock. Destination and length are hard-wired; the source is whatever scene
; template the caller points at, board decor or cutscene props. The copy runs forward
; byte by byte, so it stays correct when source and destination overlap.
loadSpriteObjectBlock:
004E: 11 08 69        LD      DE,$6908            ; destination: the ten sprite-object records
0051: 01 28 00        LD      BC,$0028            ; forty bytes, ten 4-byte records
0054: ED B0           LDIR                        ; copy the scene template into the block
0056: C9              RET                         

; Stir the pseudo-random seed, once per vblank. The machine has no random source, so
; entropy is manufactured by summing two counters that advance at unrelated rates: random
; takes random + frame + spinCount, wrapping at 8 bits. frame is decremented once per
; vblank and spinCount incremented once per main-loop pass, so spinCount's rate depends
; on how much work the frame actually did -- that jitter is what makes the sum
; unpredictable enough to drive spawns and difficulty coin-flips.
stirRandomSeed:
0057: 3A 18 60        LD      A,(workRam+18)      ; read the pseudo-random seed
005A: 21 1A 60        LD      HL,$601A            ; point at the frame counter
005D: 86              ADD     A,(HL)              ; mix in the frame counter
005E: 21 19 60        LD      HL,$6019            ; point at the main-loop spin counter
0061: 86              ADD     A,(HL)              ; mix in the spin counter, whose rate jitters
0062: 32 18 60        LD      (workRam+18),A      ; store the stirred seed
0065: C9              RET                         

; The vblank NMI -- the per-frame service, and where everything time-critical happens.
; In order: clear nmiEnable, which IS the acknowledge and also blocks re-entry until the
; tail turns it back on; read in2, which is what feeds the watchdog once a frame, and
; test the SERVICE switch; blit the sprite shadow buffer into sprite RAM through the
; i8257 from a 9-byte setup block; read and edge-debounce the joystick, but only while a
; credited game is in play, since attract skips input entirely; then the frame tail --
; decrement frame, stir random, drain the sound and task schedulers, dispatch on
; gameState -- and finally restore the six saved register pairs and re-enable the NMI.
serviceVblankNmi:
0066: F5              PUSH    AF                  ; save the interrupted code's registers
0067: C5              PUSH    BC                  
0068: D5              PUSH    DE                  
0069: E5              PUSH    HL                  
006A: DD E5           PUSH    IX                  
006C: FD E5           PUSH    IY                  
006E: AF              XOR     A                   
006F: 32 84 7D        LD      (nmiEnable),A       ; acknowledge the NMI and block re-entry
0072: 3A 00 7D        LD      A,(in2)             ; read IN2; the read is the watchdog kick
0075: E6 01           AND     $01                 ; isolate the SERVICE switch
0077: C2 00 40        JP      NZ,$4000            ; SERVICE switch held: leave the game ROM
007A: 21 38 01        LD      HL,$0138            ; point at the DMA setup block
007D: CD 41 01        CALL    blitSpritesViaDma   ; blit the sprite buffer to sprite RAM
0080: 3A 07 60        LD      A,(workRam+7)       ; attract skips the control read entirely
0083: A7              AND     A                   
0084: C2 B5 00        JP      NZ,perFrame         ; no credited game: on to the frame tail

; Read the joystick and cook it into p1Input. An upright cabinet always reads IN0; a
; cocktail reads IN1 while player 2 is up. The direction nibble is taken live every
; frame, so a held direction registers continuously, but JUMP is EDGE-detected against
; last frame's p1InputRaw and lifted into bit 7 -- set for exactly one frame per press,
; so a held jump does not repeat. The raw port goes to p1InputRaw as next frame's
; comparison. Raw bit 6 is a service / soft-reset line. Skipped entirely while attract
; is set.
readControls:
0087: 3A 26 60        LD      A,(workRam+26)      ; an upright cabinet always reads IN0
008A: A7              AND     A                   
008B: C2 98 00        JP      NZ,loc_0098         ; upright: read IN0
008E: 3A 0E 60        LD      A,(workRam+E)       ; cocktail: is player 2 the one up?
0091: A7              AND     A                   
0092: 3A 80 7C        LD      A,(in1)             ; read player 2's joystick port
0095: C2 9B 00        JP      NZ,loc_009b         ; player 2 up on a cocktail: keep IN1

loc_0098:
0098: 3A 00 7C        LD      A,(in0)             ; read player 1's joystick port

loc_009b:
009B: 47              LD      B,A                 ; keep the raw port byte
009C: E6 0F           AND     $0F                 ; the direction nibble: right, left, up, down
009E: 4F              LD      C,A                 
009F: 3A 11 60        LD      A,(workRam+11)      ; last frame's raw port, for the edge test
00A2: 2F              CPL                         
00A3: A0              AND     B                   ; keep only bits that went from 0 to 1
00A4: E6 10           AND     $10                 ; isolate the JUMP button's press edge
00A6: 17              RLA                         
00A7: 17              RLA                         
00A8: 17              RLA                         ; lift the jump edge into bit 7
00A9: B1              OR      C                   ; merge it with the direction nibble
00AA: 60              LD      H,B                 
00AB: 6F              LD      L,A                 
00AC: 22 10 60        LD      (workRam+10),HL     ; store the cooked word and the raw port
00AF: 78              LD      A,B                 
00B0: CB 77           BIT     6,A                 ; test the service / soft-reset line
00B2: C2 00 00        JP      NZ,boot             ; soft-reset the machine

; The tail of the vblank NMI: the once-a-frame service, then the game-state dispatch.
; First it decrements frame. That is the beat the whole game keeps time to -- the main
; loop spins comparing frame against its saved copy, so this decrement is what releases
; the loop for the new frame, and every periodic event keys off it. Then the three
; service routines: stir the random seed, debounce the coin line and award credits, drain
; the sound-trigger countdowns. Then dispatch gameState to its handler -- 0 power-on, 1
; attract, 2 credited, 3 in-game. Finally re-enable the NMI mask and return to the
; interrupted main loop.
perFrame:
00B5: 21 1A 60        LD      HL,$601A            ; point at the frame counter
00B8: 35              DEC     (HL)                ; advance the frame clock; this frees the main loop
00B9: CD 57 00        CALL    stirRandomSeed      ; stir the pseudo-random seed
00BC: CD 7B 01        CALL    serviceCoinInput    ; debounce the coin line, award credits
00BF: CD E0 00        CALL    soundDriverTick     ; push the sound shadows to the hardware
00C2: 21 D2 00        LD      HL,$00D2            ; push the epilogue as the dispatch's return
00C5: E5              PUSH    HL                  
00C6: 3A 05 60        LD      A,(workRam+5)       ; 0 power-on, 1 attract, 2 credited, 3 in-game
00C9: EF              RST     $28                 ; dispatch through the table that follows

; ---- $00CA-$00D1: jump table ----
00CA: C3 01 3C 07 B2 08 FE 06

loc_00d2:
00D2: FD E1           POP     IY                  ; restore the interrupted code's registers
00D4: DD E1           POP     IX                  
00D6: E1              POP     HL                  
00D7: D1              POP     DE                  
00D8: C1              POP     BC                  
00D9: 3E 01           LD      A,$01               
00DB: 32 84 7D        LD      (nmiEnable),A       ; re-enable the vblank NMI for the next frame
00DE: F1              POP     AF                  
00DF: C9              RET                         ; return to the interrupted main loop

; Re-drive the audio hardware from its work-RAM shadows, once per vblank. The sound
; latches are write-only, so game code stores into shadow bytes and this driver pushes
; them out every frame. It returns at once when attract is non-zero -- the driver is
; silent with no credited game. Otherwise it walks the eight sndTrigger bytes in lockstep
; with the eight bits of the addressable sound latch: a non-zero shadow is a frame
; countdown, decremented with its bit driven high; a zero shadow drives its bit low. So
; storing N into a trigger holds that sound asserted for N frames. Then the tune latch
; takes sndPriority while sndPriorityFrames is still running and the looping sndBgm
; otherwise, and sndIrqTrigger counts down the same way onto the sound-CPU interrupt
; line.
soundDriverTick:
00E0: 21 80 60        LD      HL,$6080            ; point at the eight sound-trigger shadows
00E3: 11 00 7D        LD      DE,$7D00            ; and at the eight sound-latch bits
00E6: 3A 07 60        LD      A,(workRam+7)       ; the driver is silent during attract
00E9: A7              AND     A                   
00EA: C0              RET     NZ                  ; no credited game: leave the latches alone
00EB: 06 08           LD      B,$08               ; eight triggers, one per latch bit

loc_00ed:
00ED: 7E              LD      A,(HL)              ; read this trigger's frame countdown
00EE: A7              AND     A                   
00EF: CA F5 00        JP      Z,loc_00f5          ; zero: release this sound's bit
00F2: 35              DEC     (HL)                ; count the assert down one frame
00F3: 3E 01           LD      A,$01               ; and hold the latch bit asserted

loc_00f5:
00F5: 12              LD      (DE),A              ; drive the addressable latch bit
00F6: 1C              INC     E                   ; next latch bit
00F7: 2C              INC     L                   ; next trigger shadow
00F8: 10 F3           DJNZ    loc_00ed            ; 
00FA: 21 8B 60        LD      HL,$608B            ; point at the priority-tune countdown
00FD: 7E              LD      A,(HL)              
00FE: A7              AND     A                   
00FF: C2 08 01        JP      NZ,loc_0108         ; a priority tune is still running
0102: 2D              DEC     L                   
0103: 2D              DEC     L                   ; step back to the background tune index
0104: 7E              LD      A,(HL)              ; no priority tune: play the looping theme
0105: C3 0B 01        JP      loc_010b            ; 

loc_0108:
0108: 35              DEC     (HL)                ; tick the priority tune's frame count
0109: 2D              DEC     L                   ; step back to the priority tune index
010A: 7E              LD      A,(HL)              ; it overrides the background tune

loc_010b:
010B: 32 00 7C        LD      (in0),A             ; hand the tune index to the sound CPU
010E: 21 88 60        LD      HL,$6088            ; the sound-CPU interrupt countdown
0111: AF              XOR     A                   
0112: BE              CP      (HL)                
0113: CA 18 01        JP      Z,loc_0118          ; nothing queued: drop the interrupt line
0116: 35              DEC     (HL)                ; count the queued interrupt down
0117: 3C              INC     A                   ; and raise the line this frame

loc_0118:
0118: 32 80 7D        LD      (dsw1),A            ; drive the sound-CPU interrupt line
011B: C9              RET                         

; Silence everything at once. Writes 0 to the eight soundLatch bits at 0x7D00-0x7D07 and
; to their readable work-RAM shadow sndTrigger, cleared as a pair per index; to the
; four-byte control block sndIrqTrigger / sndBgm / sndPriority / sndPriorityFrames; and
; to the sound-CPU interrupt line and the tune latch. The hardware latches are
; write-only, which is why the shadow exists at all. It reads nothing -- every store is
; a constant it produces itself. Reached at boot with the NMI still masked, and on
; reset, game-over and new-life.
silenceSound:
011C: 06 08           LD      B,$08               ; eight latch bits and their shadows
011E: AF              XOR     A                   
011F: 21 00 7D        LD      HL,$7D00            ; the addressable sound latch
0122: 11 80 60        LD      DE,$6080            ; and its readable work-RAM shadow

loc_0125:
0125: 77              LD      (HL),A              ; release the latch bit
0126: 12              LD      (DE),A              ; and clear its shadow to match
0127: 2C              INC     L                   
0128: 1C              INC     E                   
0129: 10 FA           DJNZ    loc_0125            ; 
012B: 06 04           LD      B,$04               ; then the four-byte tune control block

loc_012d:
012D: 12              LD      (DE),A              ; clear the tune and priority controls
012E: 1C              INC     E                   
012F: 10 FC           DJNZ    loc_012d            ; 
0131: 32 80 7D        LD      (dsw1),A            ; drop the sound-CPU interrupt line
0134: 32 00 7C        LD      (in0),A             ; and silence the tune latch
0137: C9              RET                         

; ---- $0138-$0140: data ----
0138: 53 00 69 80 41 00 70 80 81

; Program the i8257 and blit the sprite shadow buffer into sprite RAM. Sprites never
; reach the screen by CPU writes: the CPU fills spriteBuffer and this runs once a
; vblank, copying nine setup bytes into the controller in the order it expects -- mode
; register first, which also resets its internal byte flip-flop, then source,
; destination and counts, each 16-bit register being two stores to the SAME address --
; and then pulsing DRQ. The rising edge IS the blit: 385 transfers, 96 records of four
; bytes. Program the controller without the pulse and sprite RAM is left stale.
blitSpritesViaDma:
0141: AF              XOR     A                   
0142: 32 85 7D        LD      (dmaRequest),A      ; DRQ low before touching the controller
0145: 7E              LD      A,(HL)              
0146: 32 08 78        LD      (dma8257+8),A       ; mode register; this resets the byte flip-flop
0149: 23              INC     HL                  
014A: 7E              LD      A,(HL)              
014B: 32 00 78        LD      (dma8257),A         ; source address low: the sprite buffer
014E: 23              INC     HL                  
014F: 7E              LD      A,(HL)              
0150: 32 00 78        LD      (dma8257),A         ; and its high byte, same register
0153: 23              INC     HL                  
0154: 7E              LD      A,(HL)              
0155: 32 01 78        LD      (dma8257+1),A       ; the source channel's transfer count
0158: 23              INC     HL                  
0159: 7E              LD      A,(HL)              
015A: 32 01 78        LD      (dma8257+1),A       ; and its high byte
015D: 23              INC     HL                  
015E: 7E              LD      A,(HL)              
015F: 32 02 78        LD      (dma8257+2),A       ; destination address low: sprite RAM
0162: 23              INC     HL                  
0163: 7E              LD      A,(HL)              
0164: 32 02 78        LD      (dma8257+2),A       ; and its high byte
0167: 23              INC     HL                  
0168: 7E              LD      A,(HL)              
0169: 32 03 78        LD      (dma8257+3),A       ; the destination channel's count
016C: 23              INC     HL                  
016D: 7E              LD      A,(HL)              
016E: 32 03 78        LD      (dma8257+3),A       ; and its high byte
0171: 3E 01           LD      A,$01               
0173: 32 85 7D        LD      (dmaRequest),A      ; the DRQ rising edge IS the blit
0176: AF              XOR     A                   
0177: 32 85 7D        LD      (dmaRequest),A      ; drop DRQ again
017A: C9              RET                         

; Debounce the coin line, tally pulses, and award credits. coinEdge is a one-bit latch
; held at 1 while no coin is present; a coin counts only when it finds the latch armed,
; and counting clears it, so holding the coin line cannot repeat-credit. A fresh
; insertion also silences whatever is playing and fires the coin chime, unless a game is
; already running, then clears the latch and bumps coinsPartial. Once the tally reaches
; dipCoinsPerCredit it is a full group: reset the tally, add dipCreditsPerCoin to credits
; as a BCD sum unless credits is already at its 0x90 cap, and post a deferred credit
; task.
serviceCoinInput:
017B: 3A 00 7D        LD      A,(in2)             ; read the coin/start input port
017E: CB 7F           BIT     7,A                 ; COIN1 is bit 7
0180: 21 03 60        LD      HL,$6003            ; point at the coin edge latch
0183: C2 89 01        JP      NZ,loc_0189         ; a coin is on the line
0186: 36 01           LD      (HL),$01            ; line idle: re-arm the latch
0188: C9              RET                         

loc_0189:
0189: 7E              LD      A,(HL)              ; was the latch still armed?
018A: A7              AND     A                   
018B: C8              RET     Z                   ; already counted; a held line cannot repeat
018C: E5              PUSH    HL                  
018D: 3A 05 60        LD      A,(workRam+5)       ; a game in play keeps its own audio
0190: FE 03           CP      $03                 
0192: CA 9D 01        JP      Z,loc_019d          ; in-game: no chime
0195: CD 1C 01        CALL    silenceSound        ; cut whatever is playing
0198: 3E 03           LD      A,$03               ; assert the chime for three frames
019A: 32 83 60        LD      (workRam+83),A      ; fire the coin-insert chime

loc_019d:
019D: E1              POP     HL                  
019E: 36 00           LD      (HL),$00            ; consume the edge latch
01A0: 2B              DEC     HL                  ; step to the partial-coin tally
01A1: 34              INC     (HL)                ; count this coin pulse
01A2: 11 24 60        LD      DE,$6024            ; point at the coins-per-credit setting
01A5: 1A              LD      A,(DE)              
01A6: 96              SUB     (HL)                ; is the tally a full coin group yet?
01A7: C0              RET     NZ                  ; not yet; leave the tally standing
01A8: 77              LD      (HL),A              ; full group: reset the tally to zero
01A9: 13              INC     DE                  ; point at the credits-per-coin setting
01AA: 2B              DEC     HL                  ; and at the credit count
01AB: EB              EX      DE,HL               
01AC: 1A              LD      A,(DE)              ; read the current credit count
01AD: FE 90           CP      $90                 ; credits are capped at ninety
01AF: D0              RET     NC                  ; at the cap: award nothing further
01B0: 86              ADD     A,(HL)              ; add the credits this group buys
01B1: 27              DAA                         ; keep the count in packed decimal
01B2: 12              LD      (DE),A              ; store the new credit count
01B3: 11 00 04        LD      DE,$0400            ; the deferred credit task, opcode 4
01B6: CD 9F 30        CALL    enqueueTask         ; onto the task ring
01B9: C9              RET                         

; ---- $01BA-$01C2: data ----
01BA: 00 37 00 AA AA AA 50 76 00

; Game state 0: the one-time power-on setup. gameState is 0 only at reset, and this
; handler's next-to-last act steps it to 1, which is what keeps it from ever running
; again. In order: blank the playfield and the sprite shadow buffer; seed p1Score,
; p2Score and highScore from a nine-byte template in program data (player 1 zero, player
; 2 the attract placeholder, highScore the factory default); set attract = level = lives
; = 1 and repaint the lives and level indicator; decode the DIP bank into the settings
; block; raise the flip-screen latch, advance gameState to 1 (attract), select the 25m
; board and clear gameSubstate; stamp player 1's static "1UP" marker; and post three
; opening tasks onto the task ring.
powerOnInit:
01C3: CD 74 08        CALL    clearPlayfieldAndSprites; blank the screen
01C6: 21 BA 01        LD      HL,$01BA            ; the nine-byte score template in program data
01C9: 11 B2 60        LD      DE,$60B2            ; the three score slots: P1, P2 and high
01CC: 01 09 00        LD      BC,$0009            
01CF: ED B0           LDIR                        ; P1 zero, P2 placeholder, factory high score
01D1: 3E 01           LD      A,$01               
01D3: 32 07 60        LD      (workRam+7),A       ; start in attract
01D6: 32 29 62        LD      (workRam+229),A     ; level 1
01D9: 32 28 62        LD      (workRam+228),A     ; one life, for the indicator
01DC: CD B8 06        CALL    drawLivesAndLevel   ; repaint the lives and level panel

loc_01df:
01DF: CD 07 02        CALL    decodeDipSwitches   ; unpack the operator settings
01E2: 3E 01           LD      A,$01               
01E4: 32 82 7D        LD      (flipScreen),A      ; raise the flip-screen latch
01E7: 32 05 60        LD      (workRam+5),A       ; advance to state 1, attract
01EA: 32 27 62        LD      (workRam+227),A     ; select the 25m board
01ED: AF              XOR     A                   
01EE: 32 0A 60        LD      (workRam+A),A       ; clear the in-game sub-state selector
01F1: CD 53 0A        CALL    draw1UpLabel        ; stamp player 1's static 1UP marker
01F4: 11 04 03        LD      DE,$0304            ; the first of three opening task messages
01F7: CD 9F 30        CALL    enqueueTask         ; post it onto the task ring
01FA: 11 02 02        LD      DE,$0202            ; draw-score task for the high score
01FD: CD 9F 30        CALL    enqueueTask         ; 
0200: 11 00 02        LD      DE,$0200            ; draw-score task for player 1's score
0203: CD 9F 30        CALL    enqueueTask         ; 
0206: C9              RET                         

; Read the cabinet's dsw1 bank once at power-on and fan the one byte out into every
; operator-selectable setting:
;   bits 0-1  lives      -> dipLives, the bits plus 3, so 3 to 6
;   bits 2-3  bonus life -> dipBonusLife, packed decimal 07/10/15/20 = 7000..20000 points
;   bits 4-6  coinage    -> dipCoinsFor1P, dipCoinsFor2P, dipCoinsPerCredit and
;                           dipCreditsPerCoin. With no coinage bit set all four take
;                           their defaults; otherwise bits 5-6 form a 0..3 selector and
;                           bit 4 chooses how it is spread across the four counters
;   bit 7     cabinet    -> dipUpright, 1 upright / 0 cocktail
; Then it copies a fixed 170-byte option table into work RAM. That copy is constant and
; does not depend on the switches at all.
decodeDipSwitches:
0207: 3A 80 7D        LD      A,(dsw1)            ; read the operator dip-switch bank
020A: 4F              LD      C,A                 ; keep the switch byte
020B: 21 20 60        LD      HL,$6020            ; point at the settings block
020E: E6 03           AND     $03                 ; bits 0-1 select the lives count
0210: C6 03           ADD     A,$03               ; three to six lives
0212: 77              LD      (HL),A              
0213: 23              INC     HL                  ; on to the bonus-life threshold
0214: 79              LD      A,C                 
0215: 0F              RRCA                        
0216: 0F              RRCA                        ; bits 2-3 select the bonus-life threshold
0217: E6 03           AND     $03                 
0219: 47              LD      B,A                 ; use it as a repeat count
021A: 3E 07           LD      A,$07               ; setting 0 is 7000 points
021C: CA 26 02        JP      Z,loc_0226          ; take that default
021F: 3E 05           LD      A,$05               ; the others start at 5 and step by 5000

loc_0221:
0221: C6 05           ADD     A,$05               ; add five thousand per step
0223: 27              DAA                         ; the threshold is packed decimal
0224: 10 FB           DJNZ    loc_0221            ; 

loc_0226:
0226: 77              LD      (HL),A              ; store 7/10/15/20 thousand
0227: 23              INC     HL                  ; on to the four coinage counters
0228: 79              LD      A,C                 
0229: 01 01 01        LD      BC,$0101            ; defaults: one coin per credit, one credit
022C: 11 02 01        LD      DE,$0102            ; and one coin for 1P, two for 2P
022F: E6 70           AND     $70                 ; bits 4-6 hold the coinage setting
0231: 17              RLA                         
0232: 17              RLA                         
0233: 17              RLA                         
0234: 17              RLA                         ; leaves the 0..3 selector in A, bit 4 in carry
0235: CA 47 02        JP      Z,loc_0247          ; no coinage bits set: keep the defaults
0238: DA 41 02        JP      C,loc_0241          ; bit 4 picks how the selector is spread
023B: 3C              INC     A                   ; one coin buys selector+1 credits
023C: 4F              LD      C,A                 
023D: 5A              LD      E,D                 ; a 2-player game costs the same as 1-player
023E: C3 47 02        JP      loc_0247            ; 

loc_0241:
0241: C6 02           ADD     A,$02               ; a credit costs selector+2 coins
0243: 47              LD      B,A                 
0244: 57              LD      D,A                 
0245: 87              ADD     A,A                 ; and a 2-player game costs twice that
0246: 5F              LD      E,A                 

loc_0247:
0247: 72              LD      (HL),D              ; coins for a 1-player game
0248: 23              INC     HL                  
0249: 73              LD      (HL),E              ; coins for a 2-player game
024A: 23              INC     HL                  
024B: 70              LD      (HL),B              ; coins swallowed per credit group
024C: 23              INC     HL                  
024D: 71              LD      (HL),C              ; credits awarded per completed group
024E: 23              INC     HL                  ; on to the cabinet setting
024F: 3A 80 7D        LD      A,(dsw1)            ; read the switch bank again
0252: 07              RLCA                        ; bit 7 is the cabinet type
0253: 3E 01           LD      A,$01               
0255: DA 59 02        JP      C,loc_0259          ; set means upright
0258: 3D              DEC     A                   ; clear means cocktail

loc_0259:
0259: 77              LD      (HL),A              
025A: 21 65 35        LD      HL,$3565            ; a fixed option table in program data
025D: 11 00 61        LD      DE,$6100            
0260: 01 AA 00        LD      BC,$00AA            ; 170 bytes, the same whatever the switches say
0263: ED B0           LDIR                        
0265: C9              RET                         

; Power-on setup. Zeroes the whole 4K work-RAM page (real RAM stops short of the end, so
; the tail of the wipe lands on nothing), clears sprite RAM, fills video RAM with the
; BLANK tile rather than zero, which is a real glyph, marks every task-ring slot free
; and parks taskHead and taskTail at the ring base, clears the sprite and both palette
; banks, turns flip-screen on, points the stack just past the top of work RAM, silences
; the sound, and unmasks the vblank interrupt. It reads no RAM and stores only
; constants, so what it leaves behind cannot depend on what was there before.
clearRamAndInitHardware:
0266: 06 10           LD      B,$10               ; sixteen passes of 256 bytes: the whole page
0268: 21 00 60        LD      HL,$6000            ; the base of work RAM
026B: AF              XOR     A                   

loc_026c:
026C: 4F              LD      C,A                 ; 256 bytes per pass

loc_026d:
026D: 77              LD      (HL),A              
026E: 23              INC     HL                  
026F: 0D              DEC     C                   
0270: 20 FB           JR      NZ,loc_026d         ; 
0272: 10 F8           DJNZ    loc_026c            ; real RAM ends early; the tail lands on nothing
0274: 06 04           LD      B,$04               ; four passes: one kilobyte of sprite RAM
0276: 21 00 70        LD      HL,$7000            ; the base of sprite RAM

loc_0279:
0279: 4F              LD      C,A                 

loc_027a:
027A: 77              LD      (HL),A              
027B: 23              INC     HL                  
027C: 0D              DEC     C                   
027D: 20 FB           JR      NZ,loc_027a         ; 
027F: 10 F8           DJNZ    loc_0279            ; 
0281: 06 04           LD      B,$04               ; four passes: one kilobyte of video RAM
0283: 3E 10           LD      A,$10               ; fill with the BLANK tile; zero is a real glyph
0285: 21 00 74        LD      HL,$7400            ; the base of the tilemap

loc_0288:
0288: 0E 00           LD      C,$00               

loc_028a:
028A: 77              LD      (HL),A              
028B: 23              INC     HL                  
028C: 0D              DEC     C                   
028D: 20 FB           JR      NZ,loc_028a         ; 
028F: 10 F7           DJNZ    loc_0288            ; 
0291: 21 C0 60        LD      HL,$60C0            ; the base of the 32-slot task ring
0294: 06 40           LD      B,$40               ; sixty-four bytes, two per slot
0296: 3E FF           LD      A,$FF               ; 0xFF marks a slot free

loc_0298:
0298: 77              LD      (HL),A              
0299: 23              INC     HL                  
029A: 10 FC           DJNZ    loc_0298            ; 
029C: 3E C0           LD      A,$C0               
029E: 32 B0 60        LD      (workRam+B0),A      ; park the enqueue pointer at the ring base
02A1: 32 B1 60        LD      (workRam+B1),A      ; and the dequeue pointer: an empty queue
02A4: AF              XOR     A                   
02A5: 32 83 7D        LD      (spriteBank),A      ; sprite bank off
02A8: 32 86 7D        LD      (paletteBank0),A    ; both palette banks off
02AB: 32 87 7D        LD      (paletteBank1),A    ; 
02AE: 3C              INC     A                   
02AF: 32 82 7D        LD      (flipScreen),A      ; flip-screen on
02B2: 31 00 6C        LD      SP,$6C00            ; the first push lands on the last byte of RAM
02B5: CD 1C 01        CALL    silenceSound        ; silence the audio hardware
02B8: 3E 01           LD      A,$01               
02BA: 32 84 7D        LD      (nmiEnable),A       ; unmask the vblank NMI; from here it can fire

; The task-scheduler main loop, and it runs forever. Each pass reads the current task
; byte out of the scheduler's small table in page 0x60, indexed by a pointer byte the
; table itself supplies, and tests bit 7. Clear: dispatch that task, then wait for
; vblank. Set: run the per-frame work -- repaint the player-up marker, award a bonus life
; if the score has crossed the threshold, bump a frame-work counter -- then compare frame
; against the last frame this loop handled. Unchanged means the frame has not turned over
; yet, so wait. Changed means a new frame arrived: record it, ramp the difficulty, step
; the fixed-hazard animation and its fire release, and wait.
mainLoop:
02BD: 26 60           LD      H,$60               ; the scheduler's table lives in page 0x60
02BF: 3A B1 60        LD      A,(workRam+B1)      ; the dequeue pointer
02C2: 6F              LD      L,A                 
02C3: 7E              LD      A,(HL)              ; read the task opcode at the queue head
02C4: 87              ADD     A,A                 ; double for the table; bit 7 marks it free
02C5: 30 1C           JR      NC,loc_02e3         ; a real task: dispatch it
02C7: CD 15 03        CALL    redrawPlayerUpIndicator; blink the player-up marker
02CA: CD 50 03        CALL    awardBonusLifeAtThreshold; grant the score bonus life
02CD: 21 19 60        LD      HL,$6019            ; the spin counter that jitters the seed
02D0: 34              INC     (HL)                ; one per pass, however long the frame took
02D1: 21 83 63        LD      HL,$6383            ; the last frame this loop serviced
02D4: 3A 1A 60        LD      A,(workRam+1A)      ; has the vblank moved the frame counter on?
02D7: BE              CP      (HL)                
02D8: 28 E3           JR      Z,mainLoop          ; unchanged: this spin IS the vblank wait
02DA: 77              LD      (HL),A              ; a new frame arrived; remember it
02DB: CD 7F 03        CALL    rampDifficulty      ; ramp the difficulty
02DE: CD A2 03        CALL    animateFixedHazardAndReleaseFire; step the fixed hazard
02E1: 18 DA           JR      mainLoop            ; 

loc_02e3:
02E3: E6 1F           AND     $1F                 ; the doubled opcode as a table offset
02E5: 5F              LD      E,A                 
02E6: 16 00           LD      D,$00               
02E8: 36 FF           LD      (HL),$FF            ; mark the opcode slot free again
02EA: 2C              INC     L                   
02EB: 4E              LD      C,(HL)              ; take the task's argument byte
02EC: 36 FF           LD      (HL),$FF            ; and free that slot too
02EE: 2C              INC     L                   
02EF: 7D              LD      A,L                 
02F0: FE C0           CP      $C0                 ; wrap the dequeue pointer to the ring base
02F2: 30 02           JR      NC,loc_02f6         ; 
02F4: 3E C0           LD      A,$C0               

loc_02f6:
02F6: 32 B1 60        LD      (workRam+B1),A      ; store the advanced dequeue pointer
02F9: 79              LD      A,C                 ; the handler takes its argument here
02FA: 21 BD 02        LD      HL,$02BD            ; the main loop is the handler's return address
02FD: E5              PUSH    HL                  
02FE: 21 07 03        LD      HL,$0307            ; the table of task handlers
0301: 19              ADD     HL,DE               
0302: 5E              LD      E,(HL)              
0303: 23              INC     HL                  
0304: 56              LD      D,(HL)              
0305: EB              EX      DE,HL               
0306: E9              JP      (HL)                ; enter the task handler

; ---- $0307-$0314: data ----
0307: 1C 05 9B 05 C6 05 E9 05 11 06 2A 06 B8 06

; Blink the "player up" marker column, on every sixteenth frame only -- any other frame
; returns at once, and so does any frame with attract set, so no indicator shows during
; attract. Bit 4 of frame picks the phase. Clear paints the current player's three-cell
; column: the player-number tile (currentPlayer + 1) at the base cell, then two fixed
; tiles, one screen row back each. Set blanks that column and then, in a two-player game
; only, paints the OTHER player's column instead -- so two players alternate markers
; while a lone player's single marker simply blinks.
redrawPlayerUpIndicator:
0315: 3A 1A 60        LD      A,(workRam+1A)      ; read the frame counter
0318: 47              LD      B,A                 ; keep it; bit 4 is the blink phase
0319: E6 0F           AND     $0F                 ; repaint only on every sixteenth frame
031B: C0              RET     NZ                  
031C: CF              RST     $08                 ; no indicator during attract
031D: 3A 0D 60        LD      A,(workRam+D)       ; which player is up
0320: CD 47 03        CALL    selectPlayerIndicatorColumnBase; that player's column
0323: 11 E0 FF        LD      DE,$FFE0            ; one tilemap row back between cells
0326: CB 60           BIT     4,B                 ; the blink phase
0328: 28 14           JR      Z,loc_033e          ; clear: paint the marker
032A: 3E 10           LD      A,$10               ; the blank tile
032C: 77              LD      (HL),A              ; blank the three-cell column
032D: 19              ADD     HL,DE               
032E: 77              LD      (HL),A              
032F: 19              ADD     HL,DE               
0330: 77              LD      (HL),A              
0331: 3A 0F 60        LD      A,(workRam+F)       ; a lone player's marker just blinks
0334: A7              AND     A                   
0335: C8              RET     Z                   
0336: 3A 0D 60        LD      A,(workRam+D)       ; two players: show the other one instead
0339: EE 01           XOR     $01                 ; the other player's index
033B: CD 47 03        CALL    selectPlayerIndicatorColumnBase; its column

loc_033e:
033E: 3C              INC     A                   ; the number tile: 1 for P1, 2 for P2
033F: 77              LD      (HL),A              
0340: 19              ADD     HL,DE               
0341: 36 25           LD      (HL),$25            ; then two fixed tiles up the column
0343: 19              ADD     HL,DE               
0344: 36 20           LD      (HL),$20            
0346: C9              RET                         

; A two-way selector: given a player index (0 = player 1, anything else = player 2),
; return the video-RAM base of that player's on-screen indicator column. The caller
; stamps the indicator tiles through it, stepping one tilemap row back per cell, and
; calls again with the other index in a two-player game. Pure: it reads only its input
; and writes nothing.
selectPlayerIndicatorColumnBase:
0347: 21 40 77        LD      HL,$7740            ; player 1's indicator column
034A: A7              AND     A                   
034B: C8              RET     Z                   
034C: 21 E0 74        LD      HL,$74E0            ; player 2's indicator column
034F: C9              RET                         

; Grant the once-per-player bonus life the first time the running score reaches the
; operator-set threshold, then repaint the lives and level panel. bonusLifeAwarded
; latches it, so the award happens at most once per player per game. dipBonusLife holds
; the threshold as a single BCD byte in thousands (0x15 = 15000), and the score's
; thousands and ten-thousands digits are packed into a matching byte to compare against
; it. On a match the latch is set and lives is bumped. The score slot is the one for the
; player up.
awardBonusLifeAtThreshold:
0350: 3A 2D 62        LD      A,(workRam+22D)     ; already granted this player?
0353: A7              AND     A                   
0354: C0              RET     NZ                  ; at most one bonus life per player per game
0355: 21 B3 60        LD      HL,$60B3            ; player 1's hundreds/thousands digit pair
0358: 3A 0D 60        LD      A,(workRam+D)       ; the player up owns the score
035B: A7              AND     A                   
035C: 28 03           JR      Z,loc_0361          ; 
035E: 21 B6 60        LD      HL,$60B6            ; player 2's instead

loc_0361:
0361: 7E              LD      A,(HL)              
0362: E6 F0           AND     $F0                 ; the thousands digit
0364: 47              LD      B,A                 
0365: 23              INC     HL                  
0366: 7E              LD      A,(HL)              
0367: E6 0F           AND     $0F                 ; the ten-thousands digit
0369: B0              OR      B                   ; pack the score's thousands into one byte
036A: 0F              RRCA                        
036B: 0F              RRCA                        
036C: 0F              RRCA                        
036D: 0F              RRCA                        ; line it up with the threshold's format
036E: 21 21 60        LD      HL,$6021            ; the operator-set bonus-life threshold
0371: BE              CP      (HL)                
0372: D8              RET     C                   ; not there yet
0373: 3E 01           LD      A,$01               
0375: 32 2D 62        LD      (workRam+22D),A     ; latch it so it happens only once
0378: 21 28 62        LD      HL,$6228            ; the player's lives count
037B: 34              INC     (HL)                ; award the extra life
037C: C3 B8 06        JP      drawLivesAndLevel   ; repaint the lives panel

; Raise difficulty with the level and with time spent on the board. Two nested dividers
; gate the work: difficultyPrescaler counts every call and the body proceeds only once in
; 256; difficultyClock then advances one step on each of those ticks, and the recompute
; fires on every 8th of those -- once per 2048 calls. On that beat, difficulty =
; min(level + (difficultyClock >> 3), 5). Board setup clears the clock, which is why
; difficulty drops back at the start of every board and then ramps up again.
rampDifficulty:
037F: 21 84 63        LD      HL,$6384            ; the outer rate divider
0382: 7E              LD      A,(HL)              
0383: 34              INC     (HL)                ; counts every serviced frame
0384: A7              AND     A                   
0385: C0              RET     NZ                  ; the body runs once in 256 frames
0386: 21 81 63        LD      HL,$6381            ; the board's difficulty clock
0389: 7E              LD      A,(HL)              
038A: 47              LD      B,A                 
038B: 34              INC     (HL)                ; one step per outer tick
038C: E6 07           AND     $07                 
038E: C0              RET     NZ                  ; recompute only on every eighth step
038F: 78              LD      A,B                 
0390: 0F              RRCA                        
0391: 0F              RRCA                        
0392: 0F              RRCA                        ; how far the clock has run this board
0393: 47              LD      B,A                 
0394: 3A 29 62        LD      A,(workRam+229)     ; difficulty rises with the level too
0397: 80              ADD     A,B                 
0398: FE 05           CP      $05                 
039A: 38 02           JR      C,loc_039e          ; 
039C: 3E 05           LD      A,$05               ; clamped at five

loc_039e:
039E: 32 80 63        LD      (workRam+380),A     ; the barrel and fire behaviour read this
03A1: C9              RET                         

; Animate the board's fixed hazard and, when its counter runs out, ask for a new fire.
; Three gates first: the board mask 0x03, so only 25m and 50m run it at all; Mario must
; be alive; and an event-gate byte's bit 0 must be clear. Then a 4-frame prescaler, so
; the body runs one pass in four. The body needs bit 0 of the hazard's phase byte set --
; the same bit that keeps the drum alight on screen -- and bit 1 then picks one of two
; near-identical arms. Both set the hazard record's X hit extent to 2 and store a sprite
; byte jittered by one on the low bit of spinCount; they differ in the Y extent (0 or 2)
; and the sprite byte (0x40 or 0x42). The bit-1 arm also counts a second timer down and,
; on its underflow, re-sets the phase bit and raises eventReq313c -- which is what makes
; a fire come out a step later.
animateFixedHazardAndReleaseFire:
03A2: 3E 03           LD      A,$03               ; the mask: 25m and 50m only
03A4: F7              RST     $30                 ; the board gate; 75m and 100m skip it all
03A5: D7              RST     $10                 ; and Mario must be alive
03A6: 3A 50 63        LD      A,(workRam+350)     ; an event gate; bit 0 set suppresses this pass
03A9: 0F              RRCA                        
03AA: D8              RET     C                   
03AB: 21 B8 62        LD      HL,$62B8            ; a 4-frame prescaler
03AE: 35              DEC     (HL)                
03AF: C0              RET     NZ                  ; the body runs one pass in four
03B0: 36 04           LD      (HL),$04            ; reload it
03B2: 3A B9 62        LD      A,(workRam+2B9)     ; the hazard's phase byte
03B5: 0F              RRCA                        
03B6: D0              RET     NC                  ; bit 0 clear: nothing to animate this pass
03B7: 21 29 6A        LD      HL,$6A29            ; where the jittered sprite byte lands
03BA: 06 40           LD      B,$40               ; the sprite byte for this arm
03BC: DD 21 A0 66     LD      IX,$66A0            ; the fixed hazard's object record
03C0: 0F              RRCA                        ; bit 1 picks one of two arms
03C1: D2 E4 03        JP      NC,loc_03e4         ; 
03C4: DD 36 09 02     LD      (IX+$09),$02        ; the record's X hit extent
03C8: DD 36 0A 02     LD      (IX+$0A),$02        ; and its Y hit extent
03CC: 04              INC     B                   
03CD: 04              INC     B                   ; sprite byte 0x42 on this arm
03CE: CD F2 03        CALL    loc_03f2            ; 
03D1: 21 BA 62        LD      HL,$62BA            ; the release counter
03D4: 35              DEC     (HL)                
03D5: C0              RET     NZ                  
03D6: 3E 01           LD      A,$01               
03D8: 32 B9 62        LD      (workRam+2B9),A     ; re-arm the hazard's phase bit
03DB: 32 A0 63        LD      (workRam+3A0),A     ; ask for a new fire to be released

loc_03de:
03DE: 3E 10           LD      A,$10               
03E0: 32 BA 62        LD      (workRam+2BA),A     ; reload the release counter to sixteen
03E3: C9              RET                         

loc_03e4:
03E4: DD 36 09 02     LD      (IX+$09),$02        ; the record's X hit extent
03E8: DD 36 0A 00     LD      (IX+$0A),$00        ; no Y extent on this arm
03EC: CD F2 03        CALL    loc_03f2            ; 
03EF: C3 DE 03        JP      loc_03de            ; 

loc_03f2:
03F2: 70              LD      (HL),B              ; store the sprite byte
03F3: 3A 19 60        LD      A,(workRam+19)      ; jitter it on the spin counter's low bit
03F6: 0F              RRCA                        
03F7: D8              RET     C                   
03F8: 04              INC     B                   ; nudge the sprite one frame on
03F9: 70              LD      (HL),B              
03FA: C9              RET                         

; Once a frame, service the colour cycle -- with a 50m-only preamble in front of it. On
; any board but the conveyors it simply drives the cycle. On 50m it first shifts the X
; of all ten spriteObjBlock records by m50Obj1Step, sliding the whole row of props by
; that signed delta, then takes the now-shifted third record's X less a fixed anchor and
; stores it as m50ObjRowShift, the delta the 50m column painter later adds back into
; that same column. What those ten sprite-objects depict is not established: the
; template stamped into the block is the same on every board, so it carries no per-board
; identity.
slide50mSpriteRowAndServiceColorCycle:
03FB: 3A 27 62        LD      A,(workRam+227)     ; the 50m conveyor board has a preamble
03FE: FE 02           CP      $02                 
0400: C2 13 04        JP      NZ,serviceColorCycle; any other board: straight to the cycle
0403: 21 08 69        LD      HL,$6908            ; the X field of the ten sprite-objects
0406: 3A A3 63        LD      A,(workRam+3A3)     ; the 50m object's signed step this frame
0409: 4F              LD      C,A                 
040A: FF              RST     $38                 ; slide the whole row of props by it
040B: 3A 10 69        LD      A,(workRam+910)     ; the third record's shifted X
040E: D6 3B           SUB     $3B                 ; measure it against a fixed anchor
0410: 32 B7 63        LD      (workRam+3B7),A     ; the 50m column painter adds this back

; The once-a-frame entry to the attract colour cycle, picking one of three things to do.
; A sweep already running (colourCycleActive set) is stepped on and this frame's colour
; work painted; with no sweep and frame not at its wrap, only the colour column is
; repainted; with no sweep and frame just wrapped to zero, colourCycleActive is set and
; a fresh sweep advanced at once. So a sweep starts once every 256 frames and then runs
; for its own lifetime. Setting that flag is the only write made here.
serviceColorCycle:
0413: 3A 91 63        LD      A,(workRam+391)     ; is a sweep already running?
0416: A7              AND     A                   
0417: C2 26 04        JP      NZ,advanceColorCycleSweep; step it on
041A: 3A 1A 60        LD      A,(workRam+1A)      ; a sweep starts at the frame-counter wrap
041D: A7              AND     A                   
041E: C2 86 04        JP      NZ,dispatchColorCyclePaint; not yet: repaint only
0421: 3E 01           LD      A,$01               
0423: 32 91 63        LD      (workRam+391),A     ; arm a fresh sweep

; The per-frame driver of the attract-screen colour cycle. It bumps the sweep counter by
; one and routes the frame's colour work four ways on the new value: the counter reaching
; its top ends the sweep (clearing the counter and the active flag) and paints; a
; non-zero reload gate, or a frame that is not a 32-frame boundary, gives the repaint
; alone; a 32-frame boundary with the gate open reloads the 40-byte sprite-object block
; from a fixed template, asserts a 3-frame sound beat, and runs the full colour cascade.
; Bit 5 of the counter alternates the template, so the two swap every 32 frames.
advanceColorCycleSweep:
0426: 21 90 63        LD      HL,$6390            ; the sweep counter
0429: 34              INC     (HL)                ; one step per frame
042A: 7E              LD      A,(HL)              
042B: FE 80           CP      $80                 
042D: CA 64 04        JP      Z,resetColorCycleSweep; the sweep has topped out
0430: 3A 93 63        LD      A,(workRam+393)     ; a gate that suppresses the reload
0433: A7              AND     A                   
0434: C2 86 04        JP      NZ,dispatchColorCyclePaint; gate shut: repaint only
0437: 7E              LD      A,(HL)              
0438: 47              LD      B,A                 
0439: E6 1F           AND     $1F                 ; act only on a 32-frame boundary
043B: C2 86 04        JP      NZ,dispatchColorCyclePaint; in between: repaint only
043E: 21 CF 39        LD      HL,$39CF            ; one of two sprite-object templates
0441: CB 68           BIT     5,B                 ; bit 5 alternates them every 32 frames
0443: 20 03           JR      NZ,loc_0448         ; 
0445: 21 F7 39        LD      HL,$39F7            ; the other template

loc_0448:
0448: CD 4E 00        CALL    loadSpriteObjectBlock; reload the ten records
044B: 3E 03           LD      A,$03               ; a three-frame sound beat
044D: 32 82 60        LD      (workRam+82),A      ; assert it on the cascade's trigger

; The per-frame colour-cascade dispatcher: route by the current board into one of the
; colour-cycle arms. board bit 0 clear (the even boards, 50m and 100m) takes the
; even-board arm, which shifts the sprite-object block's X column by a board-specific
; delta before the colour repaint. 75m goes straight into the repaint with no sprite
; shift. 25m first nudges the whole ten-record block's Y column up 4 pixels, then
; repaints. This routine writes no memory of its own -- the sprite and colour writes all
; belong to the arms.
dispatchColorCascadeByBoard:
0450: 3A 27 62        LD      A,(workRam+227)     ; route the cascade by board
0453: 0F              RRCA                        
0454: D2 78 04        JP      NC,shiftEvenBoardSpriteColumn; the even boards shift X
0457: 0F              RRCA                        
0458: DA 86 04        JP      C,dispatchColorCyclePaint; 75m repaints with no sprite shift
045B: 21 0B 69        LD      HL,$690B            ; 25m: the Y field of the ten records
045E: 0E FC           LD      C,$FC               ; nudge the whole row up four pixels
0460: FF              RST     $38                 
0461: C3 86 04        JP      dispatchColorCyclePaint; 

; Ends the colour-cycle sweep on the frame its counter tops out at 0x80: clear the
; counter back to 0 and lower colourCycleActive, then continue into the frame's colour
; work. With the sprite-object reload gate set it goes straight to the colour repaint;
; with the gate clear it first reloads the 40-byte sprite-object block from its template
; and then runs the full per-board colour cascade. It is the end half only -- what
; STARTS the next sweep is a re-arm at the frame-counter wrap, elsewhere.
resetColorCycleSweep:
0464: AF              XOR     A                   
0465: 77              LD      (HL),A              ; restart the sweep counter
0466: 23              INC     HL                  
0467: 77              LD      (HL),A              ; and lower the active flag
0468: 3A 93 63        LD      A,(workRam+393)     ; the reload gate again
046B: A7              AND     A                   
046C: C2 86 04        JP      NZ,dispatchColorCyclePaint; shut: repaint only
046F: 21 5C 38        LD      HL,$385C            ; re-seed the block from its template
0472: CD 4E 00        CALL    loadSpriteObjectBlock; 
0475: C3 50 04        JP      dispatchColorCascadeByBoard; 

; The even-board arm of the per-frame colour cascade, reached when board's low bit is
; clear -- 50m and 100m. It shifts the X field of all ten spriteObjBlock records by one
; delta, repositioning the whole staged row with a single number: on 50m the delta comes
; from m50ObjRowShift, the byte staged for that board; on the other even board it is a
; fixed +0x44. Control then continues into the per-frame colour-cycle repaint.
shiftEvenBoardSpriteColumn:
0478: 21 08 69        LD      HL,$6908            ; the X field of the ten sprite-objects
047B: 0E 44           LD      C,$44               ; 100m's fixed shift
047D: 0F              RRCA                        ; which even board?
047E: D2 85 04        JP      NC,loc_0485         ; 100m keeps the fixed shift
0481: 3A B7 63        LD      A,(workRam+3B7)     ; 50m uses its own staged delta
0484: 4F              LD      C,A                 

loc_0485:
0485: FF              RST     $38                 ; slide the whole row by one delta

; Decide each frame how the flashing colour column is repainted, and let the right
; painter do it. It takes the sweep counter as this frame's phase, sets up the one-row
; descending stride the painters walk, and hands off to exactly one of three: on the 100m
; rivet board, that board's own two-column blink block; elsewhere, the painter that
; forces the HIGH colour code while bit 6 of the counter is set, and the LOW-code painter
; while it is clear. So bit 6 is the colour toggle and the column flashes as the counter
; advances. It writes nothing itself.
dispatchColorCyclePaint:
0486: 3A 90 63        LD      A,(workRam+390)     ; the sweep counter is this frame's phase
0489: 4F              LD      C,A                 
048A: 11 20 00        LD      DE,$0020            ; the painters step one tilemap row
048D: 3A 27 62        LD      A,(workRam+227)     ; the rivet board has its own painter
0490: FE 04           CP      $04                 
0492: CA BE 04        JP      Z,runRivetColorCycleBlink; 100m
0495: 79              LD      A,C                 
0496: A7              AND     A                   
0497: CA A1 04        JP      Z,paintColorColumnWithLowCode; phase 0: the low code
049A: 3E EF           LD      A,$EF               ; the high colour code
049C: CB 71           BIT     6,C                 ; bit 6 of the counter is the colour toggle
049E: C2 A3 04        JP      NZ,paintColorColumnAndHoldBlink; set: paint high

; The colour-cycle blink driver's LOW-CODE arm. Its only act of its own is to preset the
; fill code to 0x10; it then falls into the shared 3-cell colour-column paint, which lays
; 0x10 / 0x0F / 0x0E down a column at a stride of 0x20 and holds the sprite blink. The
; sibling arm supplies 0xEF instead (0xEF / 0xEE / 0xED), and the driver picks between
; them on its sweep counter, so the element flashes between the two colour sets as it
; advances.
paintColorColumnWithLowCode:
04A1: 3E 10           LD      A,$10               ; the low colour code

; The plain, non-rivet arm of the colour-cycle blink driver. It repaints a 3-cell
; descending colour-memory column, laying A, A-1 and A-2 one stride apart, with both the
; fill value and the stride handed in live by the driver -- so the cell cycles between
; two attribute bytes as the sweep counter advances. It then reloads sprite record #1's
; CURRENT code byte and passes it to the shared blink store UNCHANGED: its sibling arms
; force the blink bit on or off, this one never sets or clears it. Four cells written,
; then return.
paintColorColumnAndHoldBlink:
04A3: 21 C4 75        LD      HL,$75C4            ; the top of the flashing colour column
04A6: CD 14 05        CALL    fillDescendingColumn; lay three descending codes down it
04A9: 3A 05 69        LD      A,(workRam+905)     ; sprite record 1's code byte, unchanged

; The shared store tail of the blink driver behind the attract and how-high screens,
; which blinks two decorative sprites by toggling bit 7 of their code bytes. All three
; of its arms work out sprite record 1's finished code byte and fall in here, the one
; place that stores it. Once per colour-cycle sweep -- when the sweep counter has bit 6
; set and its low three bits clear -- it also flips that code's two low bits, advancing
; the sprite to its alternate animation cell. At any other counter value the byte is
; stored unchanged.
storeBlinkSpriteCode:
04AC: 32 05 69        LD      (workRam+905),A     ; commit the blink sprite's code byte
04AF: CB 71           BIT     6,C                 
04B1: C8              RET     Z                   
04B2: 47              LD      B,A                 
04B3: 79              LD      A,C                 
04B4: E6 07           AND     $07                 ; only every eighth counter value
04B6: C0              RET     NZ                  
04B7: 78              LD      A,B                 
04B8: EE 03           XOR     $03                 ; step the sprite to its alternate cell
04BA: 32 05 69        LD      (workRam+905),A     ; store the advanced code
04BD: C9              RET                         

; The 100m branch of the colour cycle: repaint two decorative colour columns, then blink
; a sprite pair. Column A takes colour codes 16/15/14 and column B continues the SAME
; descending run with 13/12/11, both stepping one tilemap row apart, so the six cells
; read as one continuous gradient that cycles with the sweep counter. Then, on the
; counter's phase bit: clear, blink the first two sprite records purely by which half of
; the screen Mario is in; set, repaint column B (Mario right) or column A in the brighter
; band starting at 223 (Mario left), and force the pair's blink off or on to match.
runRivetColorCycleBlink:
04BE: 3E 10           LD      A,$10               ; the run starts at colour code 16
04C0: 21 23 76        LD      HL,$7623            ; the top of the first colour column
04C3: CD 14 05        CALL    fillDescendingColumn; 16, 15, 14 down the column
04C6: 21 83 75        LD      HL,$7583            ; the second column continues the run
04C9: CD 14 05        CALL    fillDescendingColumn; 13, 12, 11: one continuous gradient
04CC: CB 71           BIT     6,C                 ; the slow blink phase
04CE: CA 09 05        JP      Z,blinkSpritePairByX; blink purely on Mario's screen half
04D1: 3A 03 62        LD      A,(workRam+203)     ; which half of the screen is Mario in?
04D4: FE 80           CP      $80                 
04D6: D2 F1 04        JP      NC,paintColorColumnAndBlinkOff; right half
04D9: 3E DF           LD      A,$DF               ; the brighter band
04DB: 21 23 76        LD      HL,$7623            ; repaint the first column in it
04DE: CD 14 05        CALL    fillDescendingColumn; 

; The colour-cycle driver's "blink ON" arm: raise bit 7 -- the flip/visibility bit -- in
; the code bytes of both decorative blink sprites, records 0 and 1 of the sprite shadow
; buffer. Record 0 is set in place; record 1's result goes to the shared blink-store
; tail, which commits it and may apply its once-per-sweep low-two-bit tile toggle.
; Reached on the rivet board only, selected by Mario's X and the sweep counter's bit 6.
; Its exact mirror is the "blink OFF" arm.
blinkSpritePairOn:
04E1: 3A 01 69        LD      A,(workRam+901)     ; the first decorative sprite's code byte
04E4: F6 80           OR      $80                 ; raise the flip/visibility bit
04E6: 32 01 69        LD      (workRam+901),A     ; 
04E9: 3A 05 69        LD      A,(workRam+905)     ; the second sprite's code byte
04EC: F6 80           OR      $80                 
04EE: C3 AC 04        JP      storeBlinkSpriteCode; the shared store tail commits it

; The rivet-board colour-cycle arm, taken when Mario is in the right half of the screen.
; It presets the fill value to 0xEF and lays a 3-cell descending colour column -- 0xEF,
; 0xEE, 0xED into the top cell and the two cells below it, the stride handed in by the
; driver -- then blinks the decorative sprite pair OFF, forcing the flip/visibility bit
; clear on both records' code bytes. Five cells written.
paintColorColumnAndBlinkOff:
04F1: 3E EF           LD      A,$EF               ; the high colour code
04F3: 21 83 75        LD      HL,$7583            ; repaint the second column
04F6: CD 14 05        CALL    fillDescendingColumn; 0xEF, 0xEE, 0xED down it

; The "blink off" arm of the colour-cycle blink driver: force bit 7, the flip/visibility
; bit, clear on both decorative sprites -- records 0 and 1 of the sprite shadow buffer.
; Record 0 is masked and committed here; record 1 is masked and handed to the shared
; store tail, which commits it and applies the once-per-sweep tile toggle. The off phase
; is taken on 100m and on specific colour paths, so it is not a per-frame arm.
blinkSpritePairOff:
04F9: 3A 01 69        LD      A,(workRam+901)     ; the first decorative sprite's code byte
04FC: E6 7F           AND     $7F                 ; force the flip/visibility bit clear
04FE: 32 01 69        LD      (workRam+901),A     ; 
0501: 3A 05 69        LD      A,(workRam+905)     ; the second sprite's code byte
0504: E6 7F           AND     $7F                 
0506: C3 AC 04        JP      storeBlinkSpriteCode; the shared store tail commits it

; Blink the rivet board's two decorative sprites by which half of the screen Mario is on.
; It drives the top bit -- the flip/visibility bit -- of both sprite code bytes: marioX
; in the right half forces it clear (blink off), the left half forces it set (blink on).
; The split is the screen midpoint and the test is inclusive on the right, so exactly
; midway counts as the right half. Both arms write both bytes.
blinkSpritePairByX:
0509: 3A 03 62        LD      A,(workRam+203)     ; Mario's position picks the blink phase
050C: FE 80           CP      $80                 ; the screen midpoint
050E: D2 F9 04        JP      NC,blinkSpritePairOff; right half: blink off
0511: C3 E1 04        JP      blinkSpritePairOn   ; left half: blink on

; Write three descending bytes at a caller-supplied stride. From the caller's pointer it
; stores the caller's value, steps the pointer by the stride and drops the value by one,
; exactly three times -- v, v-1, v-2 at p, p+stride, p+2*stride. The trip count is fixed,
; so there is no data-dependent branch. Every caller hands it a tilemap column with a
; one-row stride, so in practice it lays a run of three descending TILE CODES down a
; column, during the attract cycle and during board init. This hardware has no writable
; colour RAM -- the whole 7400:77ff window is the tilemap -- so nothing here is coloured.
fillDescendingColumn:
0514: 06 03           LD      B,$03               ; always three cells

loc_0516:
0516: 77              LD      (HL),A              ; lay the colour code
0517: 19              ADD     HL,DE               ; step by the caller's stride
0518: 3D              DEC     A                   ; the next cell is one code lower
0519: 10 FB           DJNZ    loc_0516            ; 
051B: C9              RET                         

; Task opcode 0: add to the score of the player up, redraw it, and promote it to the
; high score if it now leads. The task payload indexes a table of 3-byte packed-BCD
; addends (payload 1 -> +100, 5 -> +500, 11 -> +1000; 0 and 10 add nothing). The whole
; task is skipped while no credited game is in progress. The add runs least-significant
; byte first, rippling the carry upward and BCD-correcting each byte, and the readout is
; repainted from the result. The high-score compare then walks the other way, from the
; top byte DOWN, and stops as soon as a byte is lower or all three prove equal. On a
; byte that is greater the score leads: only the bytes not yet proven equal are copied
; over highScore -- reloading a width of three would clobber bytes the compare already
; matched -- and the on-screen high score is repainted.
addToScoreTask:
051C: 4F              LD      C,A                 ; the task payload picks the amount
051D: CF              RST     $08                 ; no score awarded during attract
051E: CD 5F 05        CALL    selectCurrentPlayerScoreCounter; the player-up's counter
0521: 79              LD      A,C                 
0522: 81              ADD     A,C                 
0523: 81              ADD     A,C                 ; three bytes per table entry
0524: 4F              LD      C,A                 
0525: 21 29 35        LD      HL,$3529            ; the table of packed-BCD addends
0528: 06 00           LD      B,$00               
052A: 09              ADD     HL,BC               ; index the amount this payload awards
052B: A7              AND     A                   ; start the add with a clear carry
052C: 06 03           LD      B,$03               ; three bytes of packed decimal

loc_052e:
052E: 1A              LD      A,(DE)              
052F: 8E              ADC     A,(HL)              ; add, rippling the carry upward
0530: 27              DAA                         ; and keep the result decimal
0531: 12              LD      (DE),A              
0532: 13              INC     DE                  
0533: 23              INC     HL                  
0534: 10 F8           DJNZ    loc_052e            ; 
0536: D5              PUSH    DE                  ; save the end of the counter
0537: 1B              DEC     DE                  ; point at its most significant pair
0538: 3A 0D 60        LD      A,(workRam+D)       ; the player's own score column
053B: CD 6B 05        CALL    loc_056b            ; repaint the six digits
053E: D1              POP     DE                  
053F: 1B              DEC     DE                  ; back to the top pair for the compare
0540: 21 BA 60        LD      HL,$60BA            ; the high score's top byte
0543: 06 03           LD      B,$03               

loc_0545:
0545: 1A              LD      A,(DE)              
0546: BE              CP      (HL)                
0547: D8              RET     C                   ; a lower byte: the record stands
0548: C2 50 05        JP      NZ,loc_0550         ; a greater byte: the score leads
054B: 1B              DEC     DE                  ; equal so far; walk both down a byte
054C: 2B              DEC     HL                  
054D: 10 F6           DJNZ    loc_0545            ; 
054F: C9              RET                         ; all three equal: nothing to promote

loc_0550:
0550: CD 5F 05        CALL    selectCurrentPlayerScoreCounter; the winning counter
0553: 21 B8 60        LD      HL,$60B8            ; the high-score record's base

loc_0556:
0556: 1A              LD      A,(DE)              
0557: 77              LD      (HL),A              ; copy only the bytes not proven equal
0558: 13              INC     DE                  
0559: 23              INC     HL                  
055A: 10 FA           DJNZ    loc_0556            ; 
055C: C3 DA 05        JP      drawHighScore       ; repaint the high score on screen

; Hand back the base address of the score counter for the player up -- p1Score when
; currentPlayer is zero, p2Score otherwise. The score-award and high-score compare paths
; call this to learn which counter to touch, then work from the address returned.
selectCurrentPlayerScoreCounter:
055F: 11 B2 60        LD      DE,$60B2            ; player 1's score counter
0562: 3A 0D 60        LD      A,(workRam+D)       ; which player is up
0565: A7              AND     A                   
0566: C8              RET     Z                   
0567: 11 B5 60        LD      DE,$60B5            ; player 2's score counter
056A: C9              RET                         

loc_056b:
056B: DD 21 81 77     LD      IX,$7781            ; one of the two score columns
056F: A7              AND     A                   
0570: 28 0A           JR      Z,renderBcdColumn   ; selector zero takes this one
0572: DD 21 21 75     LD      IX,$7521            ; the other column
0576: 18 04           JR      renderBcdColumn     ; 

; Draw a packed 3-byte BCD counter as six digits climbing a fixed video column. The
; caller hands over the source pointer -- three bytes, two digits each -- and this entry
; hard-wires the destination cell, sets the per-digit stride to minus one tilemap row,
; sets the count to 3 bytes, and falls into the shared expansion loop, which emits the
; HIGH then the LOW nibble of each source byte while walking the source backwards. A
; second caller enters one instruction later with a column of its own, skipping the
; fixed-cell store. Which counter the fixed cell displays is not established here.
renderBcdColumnFixedCell:
0578: DD 21 41 76     LD      IX,$7641            ; the fixed destination cell

; Draw a packed 3-byte BCD value as six digits climbing a video column. The caller
; supplies the source pointer (three bytes, two digits each) and the destination cell.
; This is the fixed-cell score renderer entered one instruction later, so the caller's
; destination survives where that entry's is hard-wired -- which is how the score column
; and the on-board bonus-item value share one renderer. It sets the per-digit stride to
; -0x20 (back one tilemap row, so digits climb) and the source count to 3, then falls
; into the shared expansion loop, which emits the high nibble then the low of each source
; byte while walking the source backwards.
renderBcdColumn:
057C: EB              EX      DE,HL               ; the caller's source counter
057D: 11 E0 FF        LD      DE,$FFE0            ; back one tilemap row, so digits climb
0580: 01 04 03        LD      BC,$0304            ; three source bytes, six digits

; Turn a packed counter into the individual digit cells that draw it on screen. Per
; source byte it emits the HIGH nibble first and then the LOW one, pushing both through
; a shared store-and-advance step that masks a nibble, writes it at the destination
; cursor and moves the cursor on by the caller's stride. The source pointer walks
; BACKWARDS while the cursor walks forwards: that reversal is what turns a counter held
; least-significant-byte-first into digits laid out in reading order. Getting the high
; digit out is a nibble SWAP, not a shift, so one store serves both digits and there is
; no separate high-digit variant.
expandBcdDigits:
0583: 7E              LD      A,(HL)              
0584: 0F              RRCA                        
0585: 0F              RRCA                        
0586: 0F              RRCA                        
0587: 0F              RRCA                        ; bring the high digit down into place
0588: CD 93 05        CALL    storeDigitAndAdvance; emit the high digit
058B: 7E              LD      A,(HL)              ; the low digit is already in place
058C: CD 93 05        CALL    storeDigitAndAdvance; emit it too
058F: 2B              DEC     HL                  ; walk the source back, the cells forward
0590: 10 F1           DJNZ    expandBcdDigits     ; 
0592: C9              RET                         

; The innermost leaf of the BCD counter renderer: mask the value to its low nibble --
; one digit, the caller loop having already rotated the wanted nibble down -- store it
; into the tilemap cell the cursor points at, and step the cursor by the caller's
; stride. The caller owns the direction and the spacing, so the same leaf draws a column
; of digits either way.
storeDigitAndAdvance:
0593: E6 0F           AND     $0F                 ; one digit
0595: DD 77 00        LD      (IX+$00),A          ; write it into the tilemap cell
0598: DD 19           ADD     IX,DE               ; step the cursor by the caller's stride
059A: C9              RET                         

; Zero one of the three score counters, then repaint it. The task ring dispatches this
; with a payload naming the counter: 0 = p1Score, 2 = highScore, anything else below 3 =
; p2Score. Each is a 3-byte little-endian packed-BCD counter; all three bytes are zeroed
; and the score-draw task then repaints that column, now reading 000000. This is the
; clear-first twin of that draw task, and it is the reset that happens when a game starts
; -- nothing resets a score during attract, which only redraws.
resetScoreCounter:
059B: FE 03           CP      $03                 
059D: D2 BD 05        JP      NC,loc_05bd         ; payload 3 and up clears the lower ones too
05A0: F5              PUSH    AF                  ; the draw task needs the payload back
05A1: 21 B2 60        LD      HL,$60B2            ; payload 0: player 1's score
05A4: A7              AND     A                   
05A5: CA AB 05        JP      Z,loc_05ab          ; 
05A8: 21 B5 60        LD      HL,$60B5            ; otherwise player 2's

loc_05ab:
05AB: FE 02           CP      $02                 
05AD: C2 B3 05        JP      NZ,loc_05b3         ; 
05B0: 21 B8 60        LD      HL,$60B8            ; payload 2: the high score

loc_05b3:
05B3: AF              XOR     A                   
05B4: 77              LD      (HL),A              ; zero all three packed-BCD bytes
05B5: 23              INC     HL                  
05B6: 77              LD      (HL),A              
05B7: 23              INC     HL                  
05B8: 77              LD      (HL),A              
05B9: F1              POP     AF                  
05BA: C3 C6 05        JP      drawScoreTask       ; repaint it, now reading 000000

loc_05bd:
05BD: 3D              DEC     A                   ; step down to the next counter
05BE: F5              PUSH    AF                  
05BF: CD 9B 05        CALL    resetScoreCounter   ; clear that one
05C2: F1              POP     AF                  
05C3: C8              RET     Z                   
05C4: 18 F7           JR      loc_05bd            ; and keep going down to zero

; Repaint one of the three on-screen score readouts, chosen by the task's payload: 0 is
; p1Score, 1 is p2Score, 2 is highScore. Any other non-zero value falls in with p2Score,
; because the arms are single equality tests. Each score is three packed digit-pairs
; stored least-significant first and the renderer walks it from the TOP, so the source
; pointer is set to the counter's base plus two before the hand-off; the high-score arm's
; own tail knows where its record is. Payload 3 selects a clear-and-redraw arm.
drawScoreTask:
05C6: FE 03           CP      $03                 
05C8: CA E0 05        JP      Z,loc_05e0          ; payload 3 redraws every counter
05CB: 11 B4 60        LD      DE,$60B4            ; player 1's most significant digit pair
05CE: A7              AND     A                   
05CF: CA D5 05        JP      Z,loc_05d5          ; 
05D2: 11 B7 60        LD      DE,$60B7            ; player 2's, for any other payload

loc_05d5:
05D5: FE 02           CP      $02                 
05D7: C2 6B 05        JP      NZ,loc_056b         ; paint the six digits up that column

; Repaint the on-screen high-score readout from highScore. It points the render source
; at the counter's most-significant pair, so the fixed-column BCD renderer walks the
; three packed bytes top-down and the six digits paint in reading order up the column.
; It is the shared tail of the two score tasks that need the record redrawn, and it
; overrides whatever source the caller left -- that unconditional override is why it
; exists as a shared tail.
drawHighScore:
05DA: 11 BA 60        LD      DE,$60BA            ; the high score's most significant pair
05DD: C3 78 05        JP      renderBcdColumnFixedCell; paint it up its fixed column

loc_05e0:
05E0: 3D              DEC     A                   ; step down to the next readout
05E1: F5              PUSH    AF                  
05E2: CD C6 05        CALL    drawScoreTask       ; repaint that one
05E5: F1              POP     AF                  
05E6: C8              RET     Z                   
05E7: 18 F7           JR      loc_05e0            ; and keep going down to zero

; Draw one of the game's canned strings, or wipe it off again. A payload byte selects
; the string through two indirections: doubled into a pointer table, whose entry is a
; string DESCRIPTOR -- first word the screen cell to start at, then the characters, then
; a sentinel. Characters go one per cell, stepping the destination back a whole tilemap
; row each time, so the run is vertical in tilemap terms, which is what reads as
; horizontal text on a monitor rotated a quarter turn. Bit 7 of the payload is a
; per-call ERASE flag: every cell gets its character and is then immediately overwritten
; with the blank tile, clearing the string's footprint. It does not change which string
; is selected.
drawStringVertical:
05E9: 21 4B 36        LD      HL,$364B            ; base of the canned-string pointer table
05EC: 87              ADD     A,A                 ; double the payload into a word index; bit 7 falls into carry
05ED: F5              PUSH    AF                  ; hold the erase flag across the whole run
05EE: E6 7F           AND     $7F                 ; drop what the erase flag left in the index
05F0: 5F              LD      E,A                 
05F1: 16 00           LD      D,$00               
05F3: 19              ADD     HL,DE               ; index the pointer table
05F4: 5E              LD      E,(HL)              
05F5: 23              INC     HL                  
05F6: 56              LD      D,(HL)              ; the entry is the string's descriptor address
05F7: EB              EX      DE,HL               
05F8: 5E              LD      E,(HL)              
05F9: 23              INC     HL                  
05FA: 56              LD      D,(HL)              ; descriptor word 0 is the cell to start at
05FB: 23              INC     HL                  ; the characters follow the descriptor word
05FC: 01 E0 FF        LD      BC,$FFE0            ; step back one tilemap row per character
05FF: EB              EX      DE,HL               

loc_0600:
0600: 1A              LD      A,(DE)              ; the next character
0601: FE 3F           CP      $3F                 ; the sentinel that ends the string
0603: CA 26 00        JP      Z,loc_0026          ; done: drop the saved flag word and return
0606: 77              LD      (HL),A              ; put the character in its cell
0607: F1              POP     AF                  ; recover the erase flag
0608: 30 02           JR      NC,loc_060c         ; not erasing, so the character stands
060A: 36 10           LD      (HL),$10            ; erase mode: overwrite it with the blank tile

loc_060c:
060C: F5              PUSH    AF                  
060D: 13              INC     DE                  ; next character
060E: 09              ADD     HL,BC               ; and one tilemap row back
060F: 18 EF           JR      loc_0600            ; 

; Repaint the "CREDIT nn" line, but only while no credited game is in progress. It tests
; bit 0 of attract: set (attract) repaints the whole line, the label plus the current
; credit count; clear (a game in play) leaves the line alone. So the credit line is
; refreshed only on the attract and idle screens, which is where it is shown.
drawCreditLineInAttract:
0611: 3A 07 60        LD      A,(workRam+7)       ; non-zero while no credited game is running
0614: 0F              RRCA                        
0615: D0              RET     NC                  ; bit clear means a game is in play, so leave the line alone

; Paint the "CREDIT nn" line. First draw canned string 5 -- the letters CREDIT -- down
; its tilemap column through the shared vertical string renderer. Then render credits,
; one packed-BCD byte, as its two digits into the display column, stepping one tilemap
; row UP between the high and low digit. The digit expander is entered in tail position,
; so its return goes to this routine's caller.
drawCreditDisplay:
0616: 3E 05           LD      A,$05               ; canned string 5 is the word CREDIT
0618: CD E9 05        CALL    drawStringVertical  ; draw the CREDIT label down its column
061B: 21 01 60        LD      HL,$6001            ; the credit count is the source byte
061E: 11 E0 FF        LD      DE,$FFE0            ; step one tilemap row up between the two digits
0621: DD 21 BF 74     LD      IX,$74BF            ; the credit readout's digit cells
0625: 06 01           LD      B,$01               ; one packed byte, so two digits
0627: C3 83 05        JP      expandBcdDigits     ; returns to this routine's caller

loc_062a:
062A: A7              AND     A                   ; a task payload of 0 means cash the readout in
062B: CA 91 06        JP      Z,awardRemainingBonusToScore; 
062E: 3A 8C 63        LD      A,(workRam+38C)     ; how much is showing on the readout
0631: A7              AND     A                   
0632: C2 A8 06        JP      NZ,stepBonusDisplayDown; still counting: take one notch off it
0635: 3A B8 63        LD      A,(workRam+3B8)     ; already bottomed out, so nothing to do
0638: A7              AND     A                   
0639: C0              RET     NZ                  
063A: 3A B0 62        LD      A,(workRam+2B0)     ; the board's starting bonus
063D: 01 0A 00        LD      BC,$000A            ; divide it by ten by repeated subtraction

loc_0640:
0640: 04              INC     B                   
0641: 91              SUB     C                   
0642: C2 40 06        JP      NZ,loc_0640         ; 
0645: 78              LD      A,B                 ; the quotient is the tens digit
0646: 07              RLCA                        ; swap it into the high nibble of the packed byte
0647: 07              RLCA                        
0648: 07              RLCA                        
0649: 07              RLCA                        
064A: 32 8C 63        LD      (workRam+38C),A     ; seed the readout with it
064D: 21 4A 38        LD      HL,$384A            ; the tile block that frames the readout
0650: 11 65 74        LD      DE,$7465            ; the first of six columns
0653: 3E 06           LD      A,$06               ; six columns of three cells each

loc_0655:
0655: DD 21 1D 00     LD      IX,$001D            ; three written plus 0x1D reaches the next column
0659: 01 03 00        LD      BC,$0003            
065C: ED B0           LDIR                        
065E: DD 19           ADD     IX,DE               
0660: DD E5           PUSH    IX                  
0662: D1              POP     DE                  
0663: 3D              DEC     A                   
0664: C2 55 06        JP      NZ,loc_0655         ; 
0667: 3A 8C 63        LD      A,(workRam+38C)     ; render the freshly seeded value

; Draw a packed two-digit BCD byte into its on-screen field, suppressing a leading zero.
; The low nibble is the units digit and the high nibble the tens; both become tiles and
; go to a shared two-cell stamp that places tens in the high field cell and units one
; column earlier. With the tens digit ZERO the leading digit is suppressed: the high
; cell gets a blank tile instead of a "0", the units digit is shifted into a second tile
; row, a background-music command is latched into sndBgm and a fixed tile is painted
; into two more field cells. It only SHOWS the value -- it does not change the bonus.
renderBonusDisplay:
066A: 4F              LD      C,A                 
066B: E6 0F           AND     $0F                 ; the low nibble is the units digit
066D: 47              LD      B,A                 
066E: 79              LD      A,C                 
066F: 0F              RRCA                        
0670: 0F              RRCA                        
0671: 0F              RRCA                        
0672: 0F              RRCA                        
0673: E6 0F           AND     $0F                 ; the high nibble is the tens digit
0675: C2 89 06        JP      NZ,stampTwoDigitField; tens digit present: stamp both tiles
0678: 3E 03           LD      A,$03               
067A: 32 89 60        LD      (workRam+89),A      ; latch the background-music command
067D: 3E 70           LD      A,$70               ; a fixed tile into two more field cells
067F: 32 86 74        LD      (videoRam+86),A     ; 
0682: 32 A6 74        LD      (videoRam+A6),A     ; 
0685: 80              ADD     A,B                 ; shift the units digit into the second tile row
0686: 47              LD      B,A                 
0687: 3E 10           LD      A,$10               ; a blank tile in place of the suppressed leading zero

; Stamp a two-digit number's tile pair into the bonus readout: the high-digit tile into
; one cell and the low-digit tile into the cell one screen column over, the two being 32
; bytes apart on the rotated tilemap. Both arms of the writer funnel through here -- the
; ordinary one and the leading-zero-suppress one, which enters with the high tile forced
; to a blank -- so only the tile values differ; the two writes are the same.
stampTwoDigitField:
0689: 32 E6 74        LD      (videoRam+E6),A     ; the high digit's field cell
068C: 78              LD      A,B                 
068D: 32 C6 74        LD      (videoRam+C6),A     ; the low digit, one screen column earlier
0690: C9              RET                         

; Cash the bonus readout in: pay one score award per digit. bonusDisplay holds two
; digits, one per nibble, and each buys an award off a table of fixed amounts. The
; add-to-score task runs twice -- the LOW digit indexes the table directly, the HIGH
; digit at an offset of ten -- so the tens digit is worth an order of magnitude more than
; the units digit, making this a payout of the readout's face value. Index 0 and index 10
; are the two do-nothing entries, so a digit of 0 costs nothing and pays nothing.
awardRemainingBonusToScore:
0691: 3A 8C 63        LD      A,(workRam+38C)     ; two digits, one per nibble
0694: 47              LD      B,A                 
0695: E6 0F           AND     $0F                 ; the low digit indexes the award table directly
0697: C5              PUSH    BC                  
0698: CD 1C 05        CALL    addToScoreTask      ; 
069B: C1              POP     BC                  
069C: 78              LD      A,B                 
069D: 0F              RRCA                        ; now the high digit
069E: 0F              RRCA                        
069F: 0F              RRCA                        
06A0: 0F              RRCA                        
06A1: E6 0F           AND     $0F                 
06A3: C6 0A           ADD     A,$0A               ; offset by ten, so it is worth ten times as much
06A5: C3 1C 05        JP      addToScoreTask      ; 

; Take one notch off the bonus readout the player watches count down. bonusDisplay is a
; single byte holding two decimal digits. The current value arrives in the accumulator,
; already known non-zero, and this subtracts one and decimal-adjusts it back into valid
; packed decimal (00 wraps to 99) before storing it and handing the new value to the
; shared two-digit field renderer. When the readout has just reached zero -- it held 01
; -- bonusDisplayZeroed is latched BEFORE the adjust, so a reader can tell "bottomed out"
; from "merely wrapped". It steps the READOUT byte only; the bonus quantity itself is a
; separate cell with its own writers.
stepBonusDisplayDown:
06A8: D6 01           SUB     $01                 ; take one notch off the packed counter
06AA: 20 05           JR      NZ,loc_06b1         ; 
06AC: 21 B8 63        LD      HL,$63B8            ; it held 01, so the readout has bottomed out
06AF: 36 01           LD      (HL),$01            

loc_06b1:
06B1: 27              DAA                         ; correct the decrement back into packed decimal
06B2: 32 8C 63        LD      (workRam+38C),A     ; 
06B5: C3 6A 06        JP      renderBonusDisplay  ; show the new value

; Repaint the top-of-screen HUD: the spare-Mario markers and the level number. A guard
; skips the whole body during attract. It blanks all six marker slots stepping one
; tilemap row UP from the bottom slot, then fills one marker per RESERVE life -- lives
; minus the caller's count of lives in play, which is 1 in practice -- from the bottom
; up, stamps two fixed furniture tiles beside the indicator, and clamps level to 99
; (writing the clamp back only when it exceeds) before splitting it into two decimal
; digits by repeated subtraction of ten. The marker count is 8-bit, so a caller passing
; a count above lives would wrap and paint far past the six slots.
drawLivesAndLevel:
06B8: 4F              LD      C,A                 ; keep the caller's count of lives in play
06B9: CF              RST     $08                 ; skip the whole HUD redraw during attract
06BA: 06 06           LD      B,$06               ; six marker slots
06BC: 11 E0 FF        LD      DE,$FFE0            ; step one tilemap row up per slot
06BF: 21 83 77        LD      HL,$7783            ; the bottom marker slot

loc_06c2:
06C2: 36 10           LD      (HL),$10            ; blank tile
06C4: 19              ADD     HL,DE               
06C5: 10 FB           DJNZ    loc_06c2            ; 
06C7: 3A 28 62        LD      A,(workRam+228)     ; 
06CA: 91              SUB     C                   ; reserve lives are lives minus those in play
06CB: CA D7 06        JP      Z,loc_06d7          ; none in reserve, so draw no markers
06CE: 47              LD      B,A                 
06CF: 21 83 77        LD      HL,$7783            ; back to the bottom slot to fill markers

loc_06d2:
06D2: 36 FF           LD      (HL),$FF            ; one marker tile per reserve life
06D4: 19              ADD     HL,DE               
06D5: 10 FB           DJNZ    loc_06d2            ; 

loc_06d7:
06D7: 21 03 75        LD      HL,$7503            
06DA: 36 1C           LD      (HL),$1C            ; fixed furniture tile beside the indicator
06DC: 21 E3 74        LD      HL,$74E3            
06DF: 36 34           LD      (HL),$34            ; and a second one
06E1: 3A 29 62        LD      A,(workRam+229)     ; 
06E4: FE 64           CP      $64                 ; the level number is clamped to 99
06E6: 38 05           JR      C,loc_06ed          ; 
06E8: 3E 63           LD      A,$63               
06EA: 32 29 62        LD      (workRam+229),A     ; write the clamp back

loc_06ed:
06ED: 01 0A FF        LD      BC,$FF0A            ; split the level into digits by subtracting ten

loc_06f0:
06F0: 04              INC     B                   
06F1: 91              SUB     C                   
06F2: D2 F0 06        JP      NC,loc_06f0         ; 
06F5: 81              ADD     A,C                 ; undo the last subtraction to leave the units
06F6: 32 A3 74        LD      (videoRam+A3),A     ; the level's units digit
06F9: 78              LD      A,B                 
06FA: 32 C3 74        LD      (videoRam+C3),A     ; and its tens digit
06FD: C9              RET                         

; The per-frame dispatcher for a credited game: read gameSubstate and vector through a
; 29-entry jump table. The occupied slots run, in table order, the opening Kong-climb
; cutscene, the how-high interlude, board setup, gameplay, the death sequence and the
; board-cleared advance. The death sequence takes the three slots straight after
; gameplay: Mario's death ANIMATION, then the player-1 life-loss handler, then player
; 2's. The life is decremented in those last two, not in the animation, and the
; animation picks between them by advancing gameSubstate one for player 1 and two for
; player 2. Six slots are null and the selector is NOT range-checked, so a null or
; out-of-range value vectors to address zero or off the end. The index is an 8-bit
; double, so 128 or more wraps back to the start of the table rather than reading past
; it.
dispatchInGameSubstate:
06FE: 3A 0A 60        LD      A,(workRam+A)       ; which step of the credited game this frame runs
0701: EF              RST     $28                 ; vector through the 29-entry table that follows

; ---- $0702-$073B: jump table ----
0702: 86 09 AB 09 D6 09 FE 09 1B 0A 37 0A 63 0A 76 0A
0712: DA 0B 00 00 91 0C 3C 12 7A 19 7C 12 F2 12 44 13
0722: 8F 13 A1 13 AA 13 BB 13 1E 14 86 14 15 16 6B 19
0732: 00 00 00 00 00 00 00 00 00 00

; Service the attract state once per vblank. Two jobs, chosen on the credit count. If a
; credit is present a coin has been accepted: reset gameSubstate and step gameState on
; from attract to credited, which is the move that walks the machine out of attract.
; Otherwise run the current attract sub-state, dispatched from a ten-entry table -- the
; attract-screen draw, the timed-advance gates and, through slot 3, the entire
; demo-gameplay cascade. Over a full attract loop the demo plays a whole game, so all
; eight used slots are reached; the two empty slots are unused indices and are never
; dispatched.
runAttractState:
073C: 21 0A 60        LD      HL,$600A            ; the attract sub-state byte, used by both arms
073F: 3A 01 60        LD      A,(workRam+1)       ; an accepted coin shows up as a credit
0742: A7              AND     A                   
0743: C2 5C 07        JP      NZ,loc_075c         ; a credit is waiting: leave attract
0746: 7E              LD      A,(HL)              ; the current attract sub-state
0747: EF              RST     $28                 ; vector through the ten-entry table that follows

; ---- $0748-$075B: jump table ----
0748: 79 07 63 07 3C 12 77 19 7C 12 C3 07 CB 07 4B 08
0758: 00 00 00 00

loc_075c:
075C: 36 00           LD      (HL),$00            ; restart the sub-state sequence for the credit screen
075E: 21 05 60        LD      HL,$6005            
0761: 34              INC     (HL)                ; step the game state on, attract to credited
0762: C9              RET                         

; Reseed the live context for a fresh attract round and rebuild the board. It is gated by
; the two-level sub-state timer: every pass ticks the prescaler and the body runs only on
; the pass where both halves expire together. Then it clears the object-insert request
; and a paired scratch byte, reseeds board 1 (25m girders), level 1 and one life, and
; hands off to the board builder. ONE life is the detail that names it: no DIP setting
; produces a starting life count of one -- they decode to three, four, five and six -- so
; a credited game can never enter a board through here.
restartAttractDemoAt25m:
0763: E7              RST     $20                 ; the body runs only when both halves of the timer expire
0764: AF              XOR     A                   
0765: 32 92 63        LD      (workRam+392),A     ; clear the paired scratch byte
0768: 32 A0 63        LD      (workRam+3A0),A     ; clear the object-insert request
076B: 3E 01           LD      A,$01               
076D: 32 27 62        LD      (workRam+227),A     ; board 1, the 25m girders
0770: 32 29 62        LD      (workRam+229),A     ; level 1
0773: 32 28 62        LD      (workRam+228),A     ; one life, which no dip setting can produce
0776: C3 92 0C        JP      buildBoard          ; lay out the fresh board

; The first attract sub-state: compose the title screen -- the 1UP / HIGH SCORE / 2UP
; score row and the coins-per-credit readout -- then arm a short wait and step attract
; on. It clears paletteBank0 and paletteBank1 (selecting bank 0), posts the two title
; strings and then the fixed title-screen task batch onto the ring, sets substateTimer
; to 2 and advances gameSubstate by one, blanks the playfield and sprite buffer, draws
; the 1UP label and (only in a two-player game) the 2UP label, and finally stamps the
; coins-per-credit readout from dipCoinsFor1P and dipCoinsFor2P. THE DIGIT WRITER RUNS
; TWICE and the second run is not a copy-paste: the first pass's own tail leaves the
; inputs the second consumes, which stamp the literal "1 2" further down the screen.
; There is no counter -- it is a two-iteration loop written as a call that falls through
; into itself.
composeAttractTitleScreen:
0779: 21 86 7D        LD      HL,$7D86            ; the two-bit palette-bank latch
077C: 36 00           LD      (HL),$00            
077E: 23              INC     HL                  
077F: 36 00           LD      (HL),$00            ; both bits clear selects bank 0
0781: 11 1B 03        LD      DE,$031B            ; draw-string task for the first title string
0784: CD 9F 30        CALL    enqueueTask         ; 
0787: 1C              INC     E                   ; and the second title string
0788: CD 9F 30        CALL    enqueueTask         ; 
078B: CD 65 09        CALL    enqueueTaskBatch    ; 
078E: 21 09 60        LD      HL,$6009            
0791: 36 02           LD      (HL),$02            ; wait two frames
0793: 23              INC     HL                  
0794: 34              INC     (HL)                ; then step attract to its next sub-state
0795: CD 74 08        CALL    clearPlayfieldAndSprites; 
0798: CD 53 0A        CALL    draw1UpLabel        ; 
079B: 3A 0F 60        LD      A,(workRam+F)       ; 2UP is drawn only in a two-player game
079E: FE 01           CP      $01                 
07A0: CC EE 09        CALL    Z,draw2UpLabel      ; 
07A3: ED 5B 22 60     LD      DE,(workRam+22)     ; both coinage settings, read as one word
07A7: 21 6C 75        LD      HL,$756C            ; the coins-per-credit readout cell
07AA: CD AD 07        CALL    writeDigitPairWithCarry; its tail sets up the second pass

; Stamp two digit tiles side by side, two cells apart -- a column is 2 bytes in this
; layout. The left value goes straight in; the right one too, EXCEPT at exactly 10,
; which no single tile can show, so it is split: a 0 into the cell two along and a 1
; into a fixed tens cell elsewhere on screen. Tile codes and digit values coincide here.
; In play this draws the coins-per-credit readout, the left digit the one-player price
; and the right the two-player one. The tail deliberately OVERWRITES the caller's target
; cell and digit pair with a second fixed set, because the caller falls straight back
; into the same code with no loop counter and the second pass runs on exactly the values
; this tail leaves behind.
writeDigitPairWithCarry:
07AD: 73              LD      (HL),E              ; the one-player price digit
07AE: 23              INC     HL                  
07AF: 23              INC     HL                  ; two cells along is one screen column
07B0: 72              LD      (HL),D              ; the two-player price digit
07B1: 7A              LD      A,D                 
07B2: D6 0A           SUB     $0A                 ; a price of exactly ten needs two tiles
07B4: C2 BC 07        JP      NZ,loc_07bc         ; 
07B7: 77              LD      (HL),A              ; the ones digit of ten
07B8: 3C              INC     A                   
07B9: 32 8E 75        LD      (videoRam+18E),A    ; its tens digit, in a fixed cell elsewhere

loc_07bc:
07BC: 11 01 02        LD      DE,$0201            ; hand the second pass its digits, 1 and 2
07BF: 21 8C 76        LD      HL,$768C            ; and its target cell further down the screen
07C2: C9              RET                         

; Blank the screen and move on. It clears the tilemap playfield, the two side columns and
; the sprite shadow buffer, readying the display for whatever the next sub-state draws,
; then adds one to gameSubstate so the next dispatch selects the following step. No
; inputs; both effects land on fixed memory.
clearScreenAndAdvanceSubstate:
07C3: CD 74 08        CALL    clearPlayfieldAndSprites; 
07C6: 21 0A 60        LD      HL,$600A            
07C9: 34              INC     (HL)                ; step to the next sub-state of this state
07CA: C9              RET                         

loc_07cb:
07CB: 3A 8A 63        LD      A,(workRam+38A)     ; frames left in the current animation run
07CE: FE 00           CP      $00                 
07D0: C2 2D 08        JP      NZ,loc_082d         ; a run is already going, so tick it instead
07D3: 3E 60           LD      A,$60               
07D5: 32 8A 63        LD      (workRam+38A),A     ; arm a fresh 96-frame run
07D8: 0E 5F           LD      C,$5F               ; seed the bit pattern this run streams out

loc_07da:
07DA: FE 00           CP      $00                 
07DC: CA 3B 08        JP      Z,loc_083b          ; the tick that reached zero ends the run
07DF: 21 86 7D        LD      HL,$7D86            ; the first of the two pattern latches
07E2: 36 00           LD      (HL),$00            
07E4: 79              LD      A,C                 
07E5: CB 07           RLC     A                   ; shift the pattern's top bit into carry
07E7: 30 02           JR      NC,loc_07eb         ; 
07E9: 36 01           LD      (HL),$01            ; the bit was set, so raise the latch

loc_07eb:
07EB: 23              INC     HL                  
07EC: 36 00           LD      (HL),$00            
07EE: CB 07           RLC     A                   ; the next pattern bit drives the second latch
07F0: 30 02           JR      NC,loc_07f4         ; 
07F2: 36 01           LD      (HL),$01            

loc_07f4:
07F4: 32 8B 63        LD      (workRam+38B),A     ; store the pattern rotated on by two bits
07F7: 21 08 3D        LD      HL,$3D08            ; the table of fill spans, count then destination

loc_07fa:
07FA: 3E B0           LD      A,$B0               ; the tile stamped across every span
07FC: 46              LD      B,(HL)              ; this span's cell count
07FD: 23              INC     HL                  
07FE: 5E              LD      E,(HL)              
07FF: 23              INC     HL                  
0800: 56              LD      D,(HL)              ; and where it starts

loc_0801:
0801: 12              LD      (DE),A              
0802: 13              INC     DE                  
0803: 10 FC           DJNZ    loc_0801            ; 
0805: 23              INC     HL                  
0806: 7E              LD      A,(HL)              
0807: FE 00           CP      $00                 
0809: C2 FA 07        JP      NZ,loc_07fa         ; a zero count terminates the table
080C: 11 1E 03        LD      DE,$031E            ; queue two follow-up draw tasks
080F: CD 9F 30        CALL    enqueueTask         ; 
0812: 13              INC     DE                  
0813: CD 9F 30        CALL    enqueueTask         ; 
0816: 21 CF 39        LD      HL,$39CF            ; the sprite-object template for this screen
0819: CD 4E 00        CALL    loadSpriteObjectBlock; 
081C: CD 24 3F        CALL    stampFixedTilePair  ; 
081F: 00              NOP                         
0820: 21 08 69        LD      HL,$6908            ; the sprite-object block's X column
0823: 0E 44           LD      C,$44               
0825: FF              RST     $38                 
0826: 21 0B 69        LD      HL,$690B            ; and its Y column
0829: 0E 78           LD      C,$78               
082B: FF              RST     $38                 
082C: C9              RET                         

loc_082d:
082D: 3A 8B 63        LD      A,(workRam+38B)     ; carry on with the pattern this run is streaming
0830: 4F              LD      C,A                 
0831: 3A 8A 63        LD      A,(workRam+38A)     ; 
0834: 3D              DEC     A                   ; take one frame off the countdown
0835: 32 8A 63        LD      (workRam+38A),A     ; 
0838: C3 DA 07        JP      loc_07da            ; 

loc_083b:
083B: 21 09 60        LD      HL,$6009            
083E: 36 02           LD      (HL),$02            ; wait two frames
0840: 23              INC     HL                  
0841: 34              INC     (HL)                ; then move to the next sub-state
0842: 21 8A 63        LD      HL,$638A            
0845: 36 00           LD      (HL),$00            ; the run is over: clear its timer
0847: 23              INC     HL                  
0848: 36 00           LD      (HL),$00            ; and its pattern byte
084A: C9              RET                         

; Park on a timed attract sub-state, then release it. Each frame it runs the two-level
; sub-state countdown -- substateTimerLo ticks, and on that byte's underflow
; substateTimer above it. While either half is still counting the routine does nothing
; more, so the sub-state stays parked. Only when both expire on the same frame does it
; clear gameSubstate to 0, so the state's sub-sequence restarts from index 0 on the next
; dispatch.
clearSubstateWhenTimerExpires:
084B: E7              RST     $20                 ; nothing more happens until both halves expire
084C: 21 0A 60        LD      HL,$600A            
084F: 36 00           LD      (HL),$00            ; both expired: restart the sub-sequence at 0
0851: C9              RET                         

; A blunt full-screen wipe for a mode or phase transition. Writes the blank tile 0x10
; across ALL 1024 cells of the tilemap as one uninterrupted sweep -- unlike the narrower
; wipe used elsewhere, which fills only the playfield and side columns -- then zeroes
; the 384-byte spriteBuffer, the DMA source blitted to sprite RAM each vblank.
; Straight-line and constant: every operand is an immediate, so it reads no register and
; no RAM.
clearTilemapAndSprites:
0852: 21 00 74        LD      HL,$7400            ; the first tilemap cell
0855: 0E 04           LD      C,$04               ; four passes of 256 cover all 1024 cells

loc_0857:
0857: 06 00           LD      B,$00               
0859: 3E 10           LD      A,$10               ; the blank tile

loc_085b:
085B: 77              LD      (HL),A              
085C: 23              INC     HL                  
085D: 10 FC           DJNZ    loc_085b            ; 
085F: 0D              DEC     C                   
0860: C2 57 08        JP      NZ,loc_0857         ; 
0863: 21 00 69        LD      HL,$6900            ; the sprite shadow buffer
0866: 0E 02           LD      C,$02               ; two passes of 192 clear its 384 bytes

loc_0868:
0868: 06 C0           LD      B,$C0               
086A: AF              XOR     A                   ; zeroing it takes every sprite off the screen

loc_086b:
086B: 77              LD      (HL),A              
086C: 23              INC     HL                  
086D: 10 FC           DJNZ    loc_086b            ; 
086F: 0D              DEC     C                   
0870: C2 68 08        JP      NZ,loc_0868         ; 
0873: C9              RET                         

; Blank the screen for a board build or for power-on. Writes the blank tile across the
; central 28 columns of all 32 tilemap rows (896 cells, each row advancing 28 written
; and 4 skipped), then down two 14-cell vertical runs beside the playfield, then zeroes
; all 384 bytes of spriteBuffer -- which takes every sprite off the screen, that block
; being what the DMA feeds the sprite hardware each vblank. Every value written is a
; constant.
clearPlayfieldAndSprites:
0874: 21 04 74        LD      HL,$7404            ; the first playfield cell
0877: 0E 20           LD      C,$20               ; all 32 tilemap rows

loc_0879:
0879: 06 1C           LD      B,$1C               ; the playfield is the central 28 columns
087B: 3E 10           LD      A,$10               ; the blank tile
087D: 11 04 00        LD      DE,$0004            ; skip the four off-playfield cells at the row's end

loc_0880:
0880: 77              LD      (HL),A              
0881: 23              INC     HL                  
0882: 10 FC           DJNZ    loc_0880            ; 
0884: 19              ADD     HL,DE               
0885: 0D              DEC     C                   
0886: C2 79 08        JP      NZ,loc_0879         ; 
0889: 21 22 75        LD      HL,$7522            ; the first side column
088C: 11 20 00        LD      DE,$0020            ; one whole tilemap row per cell
088F: 0E 02           LD      C,$02               ; two side columns
0891: 3E 10           LD      A,$10               

loc_0893:
0893: 06 0E           LD      B,$0E               ; fourteen cells each

loc_0895:
0895: 77              LD      (HL),A              
0896: 19              ADD     HL,DE               
0897: 10 FC           DJNZ    loc_0895            ; 
0899: 21 23 75        LD      HL,$7523            ; the second side column
089C: 0D              DEC     C                   
089D: C2 93 08        JP      NZ,loc_0893         ; 
08A0: 21 00 69        LD      HL,$6900            ; the sprite shadow buffer
08A3: 06 00           LD      B,$00               
08A5: 3E 00           LD      A,$00               

loc_08a7:
08A7: 77              LD      (HL),A              
08A8: 23              INC     HL                  
08A9: 10 FC           DJNZ    loc_08a7            ; 
08AB: 06 80           LD      B,$80               ; 128 more bytes makes the full 384

loc_08ad:
08AD: 77              LD      (HL),A              
08AE: 23              INC     HL                  
08AF: 10 FC           DJNZ    loc_08ad            ; 
08B1: C9              RET                         

; Run the right step of the credited game, once a frame. "Credited" is the short stretch
; after a coin has been accepted and before play begins, and it is a two-step machine.
; Step 0 sets the credited game up: clear the playfield, mark that the credit has been
; taken, queue the intro, move on to step 1. Step 1 waits for the start button; when it
; comes it records whether the game is one player or two and hands over to play. The
; selector is not range-checked and the table has only those two slots, so a byte outside
; 0..1 would vector into whatever data follows -- but nothing ever writes anything else
; there.
dispatchCreditedSubstate:
08B2: 3A 0A 60        LD      A,(workRam+A)       ; credited step 0 sets up, step 1 waits for start
08B5: EF              RST     $28                 ; vector through the two-entry table that follows

; ---- $08B6-$08B9: jump table ----
08B6: BA 08 F8 08

; The first credited frame: accept the coin and compose the credit / "PUSH START" screen,
; once. It blanks the playfield tilemap and zeroes the sprite shadow buffer; clears
; attract, the byte that marks a credit accepted, so this is the frame the machine leaves
; attract mode; posts the credit-screen text task onto the task ring; steps gameSubstate
; 0 to 1, so from the next frame the wait-for-start handler runs instead of this setup;
; posts the rest of the screen's text tasks; and selects palette bank 0 by zeroing both
; bits of the two-bit palette-bank latch. It then falls through into the start-button
; read.
enterCreditScreen:
08BA: CD 74 08        CALL    clearPlayfieldAndSprites; 
08BD: AF              XOR     A                   
08BE: 32 07 60        LD      (workRam+7),A       ; clearing this is what leaves attract mode
08C1: 11 0C 03        LD      DE,$030C            ; the credit-screen text task
08C4: CD 9F 30        CALL    enqueueTask         ; 
08C7: 21 0A 60        LD      HL,$600A            
08CA: 34              INC     (HL)                ; step to the wait-for-start sub-state
08CB: CD 65 09        CALL    enqueueTaskBatch    ; the rest of the screen's text
08CE: AF              XOR     A                   
08CF: 21 86 7D        LD      HL,$7D86            
08D2: 77              LD      (HL),A              
08D3: 2C              INC     L                   
08D4: 77              LD      (HL),A              ; both palette-bank bits clear: bank 0

; Runs every frame while a game is CREDITED -- coins in, nobody started yet. It builds a
; start-button mask and a prompt-string index from credits: exactly one credit gives
; mask 0x04 (START1 only) and prompt string 9, any other count gives 0x0C
; (START1|START2) and string 10, so a lone credit only honours the 1-player start. Once
; every 8 frames ((frame & 7) == 0) it redraws the prompt string and then the CREDIT
; line. It returns in2's start bits masked to the allowed set; the credit-screen state
; machine starts a 1-player game on 0x04 and a 2-player game on 0x08 and otherwise keeps
; waiting. A quirk: on the draw frames the two draws clobber the mask register before it
; is used, so the byte returned then is the port masked with whatever the draws left.
; Harmless, because only a clean 0x04 / 0x08 is acted on and those land on the seven
; skip frames in eight.
readStartButtonSelector:
08D5: 06 04           LD      B,$04               ; one credit honours the 1-player start only
08D7: 1E 09           LD      E,$09               ; and shows the 1-player prompt
08D9: 3A 01 60        LD      A,(workRam+1)       ; 
08DC: FE 01           CP      $01                 ; exactly one credit?
08DE: CA E4 08        JP      Z,loc_08e4          ; 
08E1: 06 0C           LD      B,$0C               ; otherwise both start buttons are live
08E3: 1C              INC     E                   ; and the two-player prompt

loc_08e4:
08E4: 3A 1A 60        LD      A,(workRam+1A)      ; 
08E7: E6 07           AND     $07                 ; redraw only one frame in eight
08E9: C2 F3 08        JP      NZ,loc_08f3         ; 
08EC: 7B              LD      A,E                 
08ED: CD E9 05        CALL    drawStringVertical  ; put up the start prompt
08F0: CD 16 06        CALL    drawCreditDisplay   ; 

loc_08f3:
08F3: 3A 00 7D        LD      A,(in2)             ; read the start buttons
08F6: A0              AND     B                   ; keep only the buttons this credit count allows
08F7: C9              RET                         

; Commit a credited game start. Dispatched every frame while gameState is 2 and
; gameSubstate is 1, it reads which start button is down. 0x04, one player: spend one
; credit and CLEAR p2Context, since there is no player 2 this game. 0x08, two players:
; spend two credits, seed p2Context (byte 0 the starting lives from dipLives, bytes 1-7
; a fixed template) and post the "bring player 2 in" task. Neither button, or both held
; at once, does nothing at all -- which is why this re-runs every frame from the credit
; until exactly one is seen. The shared tail then records the 1P/2P flag, wipes the
; playfield and sprites, seeds p1Context the same way, posts player 1's task, and ends
; the sub-state machine with gameSubstate = 0 and gameState = 3, the advance into
; gameplay. activePlayerIndex and twoPlayerGame are adjacent bytes written as one 16-bit
; store.
commitGameStart:
08F8: CD D5 08        CALL    readStartButtonSelector; 
08FB: FE 04           CP      $04                 ; the 1-player button
08FD: CA 06 09        JP      Z,loc_0906          ; 
0900: FE 08           CP      $08                 ; the 2-player button
0902: CA 19 09        JP      Z,loc_0919          ; 
0905: C9              RET                         ; neither, or both at once: keep waiting

loc_0906:
0906: CD 77 09        CALL    spendCredit         ; one credit for the one player
0909: 21 48 60        LD      HL,$6048            ; no player 2, so clear its saved context
090C: 06 08           LD      B,$08               
090E: AF              XOR     A                   

loc_090f:
090F: 77              LD      (HL),A              
0910: 2C              INC     L                   
0911: 10 FC           DJNZ    loc_090f            ; 
0913: 21 00 00        LD      HL,$0000            ; player 1 up, one-player game
0916: C3 38 09        JP      loc_0938            ; 

loc_0919:
0919: CD 77 09        CALL    spendCredit         ; two players cost two credits
091C: CD 77 09        CALL    spendCredit         ; 
091F: 11 48 60        LD      DE,$6048            ; seed player 2's saved context
0922: 3A 20 60        LD      A,(workRam+20)      ; byte 0 is the starting life count
0925: 12              LD      (DE),A              
0926: 1C              INC     E                   
0927: 21 5E 09        LD      HL,$095E            ; bytes 1-7 are a fixed template
092A: 01 07 00        LD      BC,$0007            
092D: ED B0           LDIR                        
092F: 11 01 01        LD      DE,$0101            ; post the bring-player-2-in task
0932: CD 9F 30        CALL    enqueueTask         ; 
0935: 21 00 01        LD      HL,$0100            ; player 1 up, two-player game

loc_0938:
0938: 22 0E 60        LD      (workRam+E),HL      ; active player and 1P/2P flag, one store
093B: CD 74 08        CALL    clearPlayfieldAndSprites; 
093E: 11 40 60        LD      DE,$6040            ; seed player 1's context the same way
0941: 3A 20 60        LD      A,(workRam+20)      ; 
0944: 12              LD      (DE),A              
0945: 1C              INC     E                   
0946: 21 5E 09        LD      HL,$095E            
0949: 01 07 00        LD      BC,$0007            
094C: ED B0           LDIR                        
094E: 11 00 01        LD      DE,$0100            ; post the bring-player-1-in task
0951: CD 9F 30        CALL    enqueueTask         ; 
0954: AF              XOR     A                   
0955: 32 0A 60        LD      (workRam+A),A       ; the credited sub-state machine is done
0958: 3E 03           LD      A,$03               
095A: 32 05 60        LD      (workRam+5),A       ; state 3 is gameplay
095D: C9              RET                         

; ---- $095E-$0964: data ----
095E: 01 65 3A 01 00 00 00

; Post a fixed batch of seven messages onto the task ring: one [0x04, 0x00], then six
; [0x03, 0x14] through [0x03, 0x19]. The dispatcher masks the opcode to its low five bits
; to index its handler table, so this posts handler 4 once and handler 3 -- the
; screen-text draw -- six times with successive string ids. What handler 4 does, and
; which strings those ids name, is not established here. A one-shot screen-composition
; helper; it does nothing else.
enqueueTaskBatch:
0965: 11 00 04        LD      DE,$0400            ; one message for handler 4
0968: CD 9F 30        CALL    enqueueTask         ; 
096B: 11 14 03        LD      DE,$0314            ; then handler 3 with the first string id
096E: 06 06           LD      B,$06               ; six string ids in a row

loc_0970:
0970: CD 9F 30        CALL    enqueueTask         ; 
0973: 1C              INC     E                   ; step to the next string id
0974: 10 FA           DJNZ    loc_0970            ; 
0976: C9              RET                         

; Spend one credit as a game starts -- called once per player being brought in, so a
; two-player start spends two. credits is BCD-decremented by the ten's-complement idiom
; (add the BCD of minus one, then apply the decimal-adjust correction), wrapping 0x00
; back to 0x99, and the deferred "credit changed" task is enqueued -- the same task the
; coin handler posts on an award -- for the main loop to drain and redraw the count.
spendCredit:
0977: 21 01 60        LD      HL,$6001            ; the credit count
097A: 3E 99           LD      A,$99               ; adding this is adding BCD minus one
097C: 86              ADD     A,(HL)              
097D: 27              DAA                         ; decimal-adjust it, so 00 wraps to 99
097E: 77              LD      (HL),A              
097F: 11 00 04        LD      DE,$0400            ; post the credit-changed redraw task
0982: CD 9F 30        CALL    enqueueTask         ; 
0985: C9              RET                         

; The first in-game frame's start-up step, run right after a coin and start commit hand
; control to the in-game state. It blanks every tilemap cell and the sprite shadow
; buffer, zeroes every sound output and its shadow, then turns flipScreen ON
; unconditionally and picks what runs next from activePlayerIndex: zero, a 1-player
; start, selects gameSubstate 1 with flip left on; non-zero, a 2-player start, selects
; gameSubstate 3, and there the cabinet decides -- an upright keeps flip on, a cocktail
; clears it so player 2 sees the mirrored screen.
configureFlipScreenAndSelectSubstate:
0986: CD 52 08        CALL    clearTilemapAndSprites; 
0989: CD 1C 01        CALL    silenceSound        ; 
098C: 11 82 7D        LD      DE,$7D82            ; the flip-screen latch
098F: 3E 01           LD      A,$01               
0991: 12              LD      (DE),A              ; flip is on by default
0992: 21 0A 60        LD      HL,$600A            
0995: 3A 0E 60        LD      A,(workRam+E)       ; zero for a one-player start
0998: A7              AND     A                   
0999: C2 9F 09        JP      NZ,loc_099f         ; 
099C: 36 01           LD      (HL),$01            ; one player: sub-state 1, flip left on
099E: C9              RET                         

loc_099f:
099F: 3A 26 60        LD      A,(workRam+26)      ; an upright cabinet keeps the flip
09A2: 3D              DEC     A                   
09A3: CA A8 09        JP      Z,loc_09a8          ; 
09A6: AF              XOR     A                   
09A7: 12              LD      (DE),A              ; cocktail: clear it so player 2 sees it mirrored

loc_09a8:
09A8: 36 03           LD      (HL),$03            ; two players: sub-state 3
09AA: C9              RET                         

; Run at the start of player 1's turn. Copy p1Context over the live 8-byte context whose
; first cell is lives, then re-derive board by following the just-restored boardSeqPtr
; and copying the byte it points at (1 = 25m, 2 = 50m, 3 = 75m, 4 = 100m). The order is
; load-bearing: the copy spans the pointer itself, so the deref must read the fresh one.
; Then arm the next sub-state from twoPlayerGame -- a two-player game gets the
; player-alternation screen on a 120-frame hold, a one-player game a 1-frame hold.
restorePlayer1Context:
09AB: 21 40 60        LD      HL,$6040            ; player 1's saved context
09AE: 11 28 62        LD      DE,$6228            ; the live context block, lives first
09B1: 01 08 00        LD      BC,$0008            ; eight bytes: lives, level, sequence pointer, progress
09B4: ED B0           LDIR                        
09B6: 2A 2A 62        LD      HL,(workRam+22A)    ; the pointer just restored, not the old one
09B9: 7E              LD      A,(HL)              ; the board it names: 1 = 25m through 4 = 100m
09BA: 32 27 62        LD      (workRam+227),A     ; 
09BD: 3A 0F 60        LD      A,(workRam+F)       ; 
09C0: A7              AND     A                   
09C1: 21 09 60        LD      HL,$6009            
09C4: 11 0A 60        LD      DE,$600A            
09C7: CA D0 09        JP      Z,loc_09d0          ; 
09CA: 36 78           LD      (HL),$78            ; two players: hold 120 frames
09CC: EB              EX      DE,HL               
09CD: 36 02           LD      (HL),$02            ; on the player-alternation screen
09CF: C9              RET                         

loc_09d0:
09D0: 36 01           LD      (HL),$01            ; one player: proceed on the next frame
09D2: EB              EX      DE,HL               
09D3: 36 05           LD      (HL),$05            ; straight to sub-state 5
09D5: C9              RET                         

; The two-player arm of the board-setup step, and it dispatches exactly once, at the
; start of a two-player game -- the one-player path advances straight past it.
; Straight-line, with no branch of its own: clear the two board control latches, post
; draw tasks [3, 2] and [2, 1] onto the task ring, advance gameSubstate from 2 to 5, then
; fall through into the shared 3-cell column painter, which writes three tilemap cells
; one row apart.
armTwoPlayerBoardSetup:
09D6: AF              XOR     A                   
09D7: 32 86 7D        LD      (paletteBank0),A    ; 
09DA: 32 87 7D        LD      (paletteBank1),A    ; clearing both bits selects palette bank 0
09DD: 11 02 03        LD      DE,$0302            ; post this step's two draw tasks
09E0: CD 9F 30        CALL    enqueueTask         ; 
09E3: 11 01 02        LD      DE,$0201            
09E6: CD 9F 30        CALL    enqueueTask         ; 
09E9: 3E 05           LD      A,$05               
09EB: 32 0A 60        LD      (workRam+A),A       ; advance the board setup, 2 to 5

; Stamp the three fixed video-RAM cells of player 2's "2UP" score marker: the player
; digit, then two fixed glyph tiles the font decodes as U and P, one tilemap row apart up
; the screen. Straight-line, input-independent, force-written unconditionally. It is
; drawn only when a second player exists -- attract setup calls it behind a two-player
; guard, and it is the unconditional tail of the player-alternation setup. The same three
; cells are afterwards maintained and blinked every sixteenth frame by the marker
; refresh.
draw2UpLabel:
09EE: 3E 02           LD      A,$02               
09F0: 32 E0 74        LD      (videoRam+E0),A     ; the player digit of the 2UP marker
09F3: 3E 25           LD      A,$25               
09F5: 32 C0 74        LD      (videoRam+C0),A     ; 'U', one tilemap row up
09F8: 3E 20           LD      A,$20               
09FA: 32 A0 74        LD      (videoRam+A0),A     ; 'P', one row further up
09FD: C9              RET                         

; Hand the turn to player 2. Copies p2Context over the live 8-byte context block based
; at lives, then re-derives board from the byte the just-restored boardSeqPtr points at
; -- which is what makes the board follow player 2's own progress rather than whatever
; the other player left behind. Finally it arms the turn with the "wait N frames, then
; run sub-state M" idiom: substateTimer = 120 frames, gameSubstate = 4. Branchless, with
; every write unconditional, so no saved context can steer it.
restorePlayer2Context:
09FE: 21 48 60        LD      HL,$6048            ; player 2's saved context
0A01: 11 28 62        LD      DE,$6228            ; the live context block, lives first
0A04: 01 08 00        LD      BC,$0008            
0A07: ED B0           LDIR                        
0A09: 2A 2A 62        LD      HL,(workRam+22A)    ; player 2's own place in the board order
0A0C: 7E              LD      A,(HL)              
0A0D: 32 27 62        LD      (workRam+227),A     ; cache the board it names
0A10: 3E 78           LD      A,$78               
0A12: 32 09 60        LD      (workRam+9),A       ; hold 120 frames before player 2's turn
0A15: 3E 04           LD      A,$04               
0A17: 32 0A 60        LD      (workRam+A),A       ; then sub-state 4
0A1A: C9              RET                         

loc_0a1b:
0A1B: AF              XOR     A                   
0A1C: 32 86 7D        LD      (paletteBank0),A    ; 
0A1F: 32 87 7D        LD      (paletteBank1),A    ; clearing both bits selects palette bank 0
0A22: 11 03 03        LD      DE,$0303            ; post this step's two draw tasks
0A25: CD 9F 30        CALL    enqueueTask         ; 
0A28: 11 01 02        LD      DE,$0201            
0A2B: CD 9F 30        CALL    enqueueTask         ; 
0A2E: CD EE 09        CALL    draw2UpLabel        ; 
0A31: 3E 05           LD      A,$05               
0A33: 32 0A 60        LD      (workRam+A),A       ; chain the setup cascade on to sub-state 5
0A36: C9              RET                         

; One step of the pre-gameplay screen build that runs before each board. It posts a
; fixed four-message batch of (opcode, argument) pairs onto the task ring, which the
; main loop drains and dispatches as deferred draw work; advances gameSubstate by one so
; the next frame selects the following step; and stamps player 1's static "1UP" score
; marker into three tilemap cells. It takes no input at all.
composeScreenAndAdvanceSubstate:
0A37: 11 04 03        LD      DE,$0304            ; post the four fixed screen-build tasks
0A3A: CD 9F 30        CALL    enqueueTask         ; 
0A3D: 11 02 02        LD      DE,$0202            
0A40: CD 9F 30        CALL    enqueueTask         ; 
0A43: 11 00 02        LD      DE,$0200            
0A46: CD 9F 30        CALL    enqueueTask         ; 
0A49: 11 00 06        LD      DE,$0600            
0A4C: CD 9F 30        CALL    enqueueTask         ; 
0A4F: 21 0A 60        LD      HL,$600A            
0A52: 34              INC     (HL)                ; step to the next sub-state

; Stamp the three video-RAM cells of player 1's "1UP" score marker -- the player digit,
; then 'U', then 'P', climbing one tilemap row per cell. This is the one-shot static
; draw, done while the top-of-screen furniture is built; a separate per-frame routine
; blinks the same three cells afterwards. It force-writes all three whatever they held,
; and every value is an immediate. The digit tile is corroborated by that blink routine,
; which writes the player number plus one into the same cell; the two letter codes are
; the conventional font reading.
draw1UpLabel:
0A53: 3E 01           LD      A,$01               
0A55: 32 40 77        LD      (videoRam+340),A    ; the player digit of the 1UP marker
0A58: 3E 25           LD      A,$25               
0A5A: 32 20 77        LD      (videoRam+320),A    ; 'U', one tilemap row up
0A5D: 3E 20           LD      A,$20               
0A5F: 32 00 77        LD      (videoRam+300),A    ; 'P', one row further up
0A62: C9              RET                         

; The board-start step that wipes the previous scene and decides whether the opening
; cutscene plays. Dispatched once a frame on gameSubstate, it opens with the shared
; sub-state timer tick and is skipped entirely until that timer reaches 0. Then it blanks
; the tilemap playfield, the two side columns and the 384-byte sprite shadow buffer;
; reloads substateTimer to 1 so the sub-state selected next proceeds on the very next
; frame; and steps gameSubstate forward -- by 1 into the opening Kong-climb cutscene, or
; by 2 straight past it to the "how high can you get?" interlude when playIntro is clear,
; which is why a board replayed after a death skips the intro (both death handlers zero
; that flag).
clearScreenAndSelectIntro:
0A63: DF              RST     $18                 ; nothing happens until the sub-state timer expires
0A64: CD 74 08        CALL    clearPlayfieldAndSprites; 
0A67: 21 09 60        LD      HL,$6009            
0A6A: 36 01           LD      (HL),$01            ; re-arm for one frame so the next step runs at once
0A6C: 2C              INC     L                   
0A6D: 34              INC     (HL)                ; advance to the opening cutscene
0A6E: 11 2C 62        LD      DE,$622C            ; the play-intro flag
0A71: 1A              LD      A,(DE)              
0A72: A7              AND     A                   
0A73: C0              RET     NZ                  ; the intro is wanted, so stop here
0A74: 34              INC     (HL)                ; intro suppressed after a death: skip to the how-high screen
0A75: C9              RET                         

; The per-frame dispatcher for the opening Kong-climb cutscene, the short animated intro
; at the head of a board. It reads introStep and vectors through the 8-entry inline
; table: 0 seeds the cutscene's walk pointers; 1 and 2 advance Kong's climb; 3 and 5 are
; the shared gated tick, a metered pause that bumps the step once substateTimer expires;
; 4 and 6 are further beats; 7 is the final beat, which fires the roar. introStep walks
; 0 to 7 across the cutscene, each arm normally advancing it for the next frame.
; Doubling the index into a table offset is 8-BIT, so a selector of 0x80 wraps the
; offset back to the table's start.
dispatchIntroCutsceneStep:
0A76: 3A 85 63        LD      A,(workRam+385)     ; which beat of the cutscene this frame runs
0A79: EF              RST     $28                 ; vector through the eight-entry table that follows

; ---- $0A7A-$0A89: jump table ----
0A7A: 8A 0A BF 0A E8 0A 69 30 06 0B 69 30 68 0B B3 0B

; Step 0 of the opening Kong-climb cutscene -- the one-time setup, run while
; gameSubstate is the cutscene value and introStep is still 0. Selects palette bank 1,
; draws the cutscene's static playfield by walking the terminated girder-and-ladder
; segment table into video RAM, stamps three fixed tiles, clears the cutscene
; bookkeeping byte, seeds introWalkPtrA and introWalkPtrB for the later steps, arms
; substateTimer to a 64-frame countdown the following step gates on, and steps introStep
; to 1 so the next dispatch runs that phase instead of re-running this setup.
; Straight-line, with no work-RAM inputs.
setupIntroCutsceneStep:
0A8A: AF              XOR     A                   
0A8B: 32 86 7D        LD      (paletteBank0),A    ; 
0A8E: 3C              INC     A                   
0A8F: 32 87 7D        LD      (paletteBank1),A    ; palette bank 1 for the cutscene
0A92: 11 0D 38        LD      DE,$380D            ; the cutscene's girder-and-ladder segment table
0A95: CD A7 0D        CALL    drawBoardLayout     ; 
0A98: 3E 10           LD      A,$10               ; the blank tile, into two cells of the drawn layout
0A9A: 32 A3 76        LD      (videoRam+2A3),A    ; 
0A9D: 32 63 76        LD      (videoRam+263),A    ; 
0AA0: 3E D4           LD      A,$D4               ; one more fixed tile for the cutscene screen
0AA2: 32 AA 75        LD      (videoRam+1AA),A    ; 
0AA5: AF              XOR     A                   
0AA6: 32 AF 62        LD      (workRam+2AF),A     ; clear the cutscene's tick counter
0AA9: 21 B4 38        LD      HL,$38B4            
0AAC: 22 C2 63        LD      (workRam+3C2),HL    ; seed the first walk pointer
0AAF: 21 CB 38        LD      HL,$38CB            
0AB2: 22 C4 63        LD      (workRam+3C4),HL    ; and the second
0AB5: 3E 40           LD      A,$40               
0AB7: 32 09 60        LD      (workRam+9),A       ; arm the 64-frame gate the next beat waits on
0ABA: 21 85 63        LD      HL,$6385            
0ABD: 34              INC     (HL)                ; step the cutscene to beat 1
0ABE: C9              RET                         

; One phase of the opening Kong-climb cutscene that plays at the head of every board. A
; one-shot timer gate -- an earlier phase armed the cutscene countdown, and every frame
; this ticks it and does nothing else until it expires. On the single expiry frame it
; copies this phase's 40-byte sprite-object template over spriteObjBlock, nudges all ten
; records into scene position with two strided add passes, seeds introScrollIndex and
; record 1's first field (which must come after both add passes, since it overwrites what
; they wrote), queues the intro tune as a three-frame priority pulse, and steps the
; cutscene on.
runIntroClimbStep:
0ABF: DF              RST     $18                 ; do nothing until that 64-frame gate expires
0AC0: 21 8C 38        LD      HL,$388C            ; the ten-record sprite template for this beat
0AC3: CD 4E 00        CALL    loadSpriteObjectBlock; 
0AC6: 21 08 69        LD      HL,$6908            
0AC9: 0E 30           LD      C,$30               ; nudge every record's X into scene position
0ACB: FF              RST     $38                 
0ACC: 21 0B 69        LD      HL,$690B            
0ACF: 0E 99           LD      C,$99               ; and every record's Y
0AD1: FF              RST     $38                 
0AD2: 3E 1F           LD      A,$1F               
0AD4: 32 8E 63        LD      (workRam+38E),A     ; seed the climb's scroll index
0AD7: AF              XOR     A                   
0AD8: 32 0C 69        LD      (workRam+90C),A     ; overwrite what the X pass left in record 1
0ADB: 21 8A 60        LD      HL,$608A            
0ADE: 36 01           LD      (HL),$01            ; queue the intro tune
0AE0: 23              INC     HL                  
0AE1: 36 03           LD      (HL),$03            ; held for three frames
0AE3: 21 85 63        LD      HL,$6385            
0AE6: 34              INC     (HL)                ; step the cutscene on
0AE7: C9              RET                         

; Step 2 of the opening Kong-climb cutscene: run the climb, one frame at a time. Every
; frame it advances the ten-record sprite-object block one animation frame -- which
; scrolls the climbing figures up 4px on every eighth call -- and bumps the cutscene's
; own tick counter; on every sixteenth tick it also slides the climb-graphic strip up one
; tilemap row. It then reads record 0's Y, which walks upward as the sprite scrolls, and
; returns while it is still at or above the top-out row. On the frame that Y passes the
; row the climb is finished: arm substateTimer to 32 frames, step introStep from 2 to 3,
; and point seqAdvancePtr at introStep so the shared gated tick advances it again once
; that timer expires.
animateIntroClimbStep:
0AE8: CD 6F 30        CALL    animateSpriteObjectBlock; 
0AEB: 3A AF 62        LD      A,(workRam+2AF)     ; the cutscene's own tick counter
0AEE: E6 0F           AND     $0F                 ; every sixteenth tick
0AF0: CC 4A 30        CALL    Z,scrollClimbGraphicStep; slide the climb strip up a row
0AF3: 3A 0B 69        LD      A,(workRam+90B)     ; the climbing figure's Y, which walks upward
0AF6: FE 5D           CP      $5D                 ; has it passed the top-out row?
0AF8: D0              RET     NC                  ; not yet, so climb again next frame
0AF9: 3E 20           LD      A,$20               
0AFB: 32 09 60        LD      (workRam+9),A       ; topped out: arm a 32-frame pause
0AFE: 21 85 63        LD      HL,$6385            
0B01: 34              INC     (HL)                ; step the cutscene on
0B02: 22 C0 63        LD      (workRam+3C0),HL    ; let the shared gated tick advance it next
0B05: C9              RET                         

loc_0b06:
0B06: 3A 1A 60        LD      A,(workRam+1A)      ; 
0B09: 0F              RRCA                        
0B0A: D8              RET     C                   ; odd frames idle, so the walk runs at half rate
0B0B: 2A C2 63        LD      HL,(workRam+3C2)    ; the walk cursor into the delta table
0B0E: 7E              LD      A,(HL)              
0B0F: FE 7F           CP      $7F                 ; the sentinel that ends the walk
0B11: CA 1E 0B        JP      Z,loc_0b1e          ; 
0B14: 23              INC     HL                  
0B15: 22 C2 63        LD      (workRam+3C2),HL    ; advance the cursor one byte
0B18: 4F              LD      C,A                 ; the signed Y delta
0B19: 21 0B 69        LD      HL,$690B            ; add it into every record's Y
0B1C: FF              RST     $38                 
0B1D: C9              RET                         

loc_0b1e:
0B1E: 21 5C 38        LD      HL,$385C            ; walk done: load the next sprite template
0B21: CD 4E 00        CALL    loadSpriteObjectBlock; 
0B24: 11 00 69        LD      DE,$6900            ; copy eight more bytes into the sprite buffer's head
0B27: 01 08 00        LD      BC,$0008            
0B2A: ED B0           LDIR                        
0B2C: 21 08 69        LD      HL,$6908            
0B2F: 0E 50           LD      C,$50               ; shift the fresh row along X
0B31: FF              RST     $38                 
0B32: 21 0B 69        LD      HL,$690B            
0B35: 0E FC           LD      C,$FC               ; and up four on Y
0B37: FF              RST     $38                 

loc_0b38:
0B38: CD 4A 30        CALL    scrollClimbGraphicStep; 
0B3B: 3A 8E 63        LD      A,(workRam+38E)     ; 
0B3E: FE 0A           CP      $0A                 ; scroll the climb strip until the index reaches ten
0B40: C2 38 0B        JP      NZ,loc_0b38         ; 
0B43: 3E 03           LD      A,$03               
0B45: 32 82 60        LD      (workRam+82),A      ; assert this beat's sound for three frames
0B48: 11 2C 39        LD      DE,$392C            ; draw the next layout segment table
0B4B: CD A7 0D        CALL    drawBoardLayout     ; 
0B4E: 3E 10           LD      A,$10               ; the blank tile, into two cells of the drawn layout
0B50: 32 AA 74        LD      (videoRam+AA),A     ; 
0B53: 32 8A 74        LD      (videoRam+8A),A     ; 
0B56: 3E 05           LD      A,$05               
0B58: 32 8D 63        LD      (workRam+38D),A     ; five bands still to stamp
0B5B: 3E 20           LD      A,$20               
0B5D: 32 09 60        LD      (workRam+9),A       ; arm the 32-frame pause
0B60: 21 85 63        LD      HL,$6385            
0B63: 34              INC     (HL)                ; step the cutscene on
0B64: 22 C0 63        LD      (workRam+3C0),HL    ; let the gated tick advance it
0B67: C9              RET                         

loc_0b68:
0B68: 3A 1A 60        LD      A,(workRam+1A)      ; 
0B6B: 0F              RRCA                        
0B6C: D8              RET     C                   ; odd frames idle, so the scroll runs every other frame
0B6D: 2A C4 63        LD      HL,(workRam+3C4)    ; the walk cursor into the delta table
0B70: 7E              LD      A,(HL)              
0B71: FE 7F           CP      $7F                 ; the sentinel means the scroll path has wrapped
0B73: CA 86 0B        JP      Z,loc_0b86          ; 
0B76: 23              INC     HL                  
0B77: 22 C4 63        LD      (workRam+3C4),HL    ; advance the cursor one byte
0B7A: 21 0B 69        LD      HL,$690B            
0B7D: 4F              LD      C,A                 
0B7E: FF              RST     $38                 ; add the signed delta into every record's Y
0B7F: 21 08 69        LD      HL,$6908            
0B82: 0E FF           LD      C,$FF               
0B84: FF              RST     $38                 ; and slide every record one pixel left
0B85: C9              RET                         

loc_0b86:
0B86: 21 CB 38        LD      HL,$38CB            
0B89: 22 C4 63        LD      (workRam+3C4),HL    ; rewind the cursor to the table start
0B8C: 3E 03           LD      A,$03               
0B8E: 32 82 60        LD      (workRam+82),A      ; assert the band's sound for three frames
0B91: 21 DC 38        LD      HL,$38DC            ; the band record table
0B94: 3A 8D 63        LD      A,(workRam+38D)     ; 
0B97: 3D              DEC     A                   
0B98: 07              RLCA                        ; scale the remaining band count by sixteen
0B99: 07              RLCA                        
0B9A: 07              RLCA                        
0B9B: 07              RLCA                        
0B9C: 5F              LD      E,A                 
0B9D: 16 00           LD      D,$00               
0B9F: 19              ADD     HL,DE               
0BA0: EB              EX      DE,HL               
0BA1: CD A7 0D        CALL    drawBoardLayout     ; draw that band's girders and ladders
0BA4: 21 8D 63        LD      HL,$638D            
0BA7: 35              DEC     (HL)                ; one band placed
0BA8: C0              RET     NZ                  ; more bands go down on later wraps
0BA9: 3E B0           LD      A,$B0               
0BAB: 32 09 60        LD      (workRam+9),A       ; all bands placed: hold 176 frames
0BAE: 21 85 63        LD      HL,$6385            
0BB1: 34              INC     (HL)                ; step on to the final beat
0BB2: C9              RET                         

; The last step of the opening cutscene. Everything keys off substateTimer as the frame
; begins: at 144 frames remaining Kong ROARS -- sndPriority takes the roar tune and
; sndPriorityFrames holds it for 3 frames -- and one of the cutscene's sprite-object
; bytes is bumped up by one; at 24 remaining that same byte is bumped back down; at any
; other count neither cue is touched. The countdown is then ticked every frame, and on
; the frame it expires the cutscene ENDS: introStep wraps back to its first step and
; gameSubstate moves on to the how-high screen.
runIntroRoarStep:
0BB3: 21 8A 60        LD      HL,$608A            ; the sound-priority pair
0BB6: 3A 09 60        LD      A,(workRam+9)       ; the phase countdown as the frame begins
0BB9: FE 90           CP      $90                 ; at 144 frames left, Kong roars
0BBB: 20 0B           JR      NZ,loc_0bc8         ; 
0BBD: 36 0F           LD      (HL),$0F            ; the roar tune
0BBF: 23              INC     HL                  
0BC0: 36 03           LD      (HL),$03            ; held for three frames
0BC2: 21 19 69        LD      HL,$6919            
0BC5: 34              INC     (HL)                ; bump the cutscene sprite byte up
0BC6: 18 09           JR      loc_0bd1            ; 

loc_0bc8:
0BC8: FE 18           CP      $18                 ; at 24 frames left
0BCA: 20 05           JR      NZ,loc_0bd1         ; 
0BCC: 21 19 69        LD      HL,$6919            
0BCF: 35              DEC     (HL)                ; bump that same byte back down
0BD0: 00              NOP                         

loc_0bd1:
0BD1: DF              RST     $18                 ; tick the countdown; nothing more until it expires
0BD2: AF              XOR     A                   
0BD3: 32 85 63        LD      (workRam+385),A     ; the cutscene is over: wrap the step sequence
0BD6: 34              INC     (HL)                ; re-arm the timer to 1 for the next screen
0BD7: 23              INC     HL                  
0BD8: 34              INC     (HL)                ; move the sub-state on to the how-high screen
0BD9: C9              RET                         

; Build the "HOW HIGH CAN YOU GET?" screen -- a diagonal stack of girders with a small
; climbing figure on each, one girder taller each time the player advances a board,
; which is what makes it read as a height. The sound channels are silenced first,
; unconditionally, then everything below is gated on substateTimer expiring. On that
; frame: clear the playfield and sprite buffer, post the lives-marker redraw, seed
; palette bank 1 and the level-start tune, then set the height -- howHighIndex is
; clamped to at most 5 and raised by one if boardSeqPtr has moved since howHighLastSeq,
; which is exactly the case where a board was cleared. That index IS the girder count.
; Each row lays six 4-tile girder groups backwards through video RAM and copies a 3-byte
; climb-figure record into the next sprite slot. The row loop tests at the bottom, so a
; height of 0 paints 256 rows, not none.
buildHowHighScreen:
0BDA: CD 1C 01        CALL    silenceSound        ; unconditional, ahead of the frame gate
0BDD: DF              RST     $18                 ; everything below runs only on the expiry frame
0BDE: CD 74 08        CALL    clearPlayfieldAndSprites; 
0BE1: 16 06           LD      D,$06               ; task 6 redraws the lives markers
0BE3: 3A 00 62        LD      A,(workRam+200)     ; passed as the count of lives in play
0BE6: 5F              LD      E,A                 
0BE7: CD 9F 30        CALL    enqueueTask         ; 
0BEA: 21 86 7D        LD      HL,$7D86            
0BED: 36 01           LD      (HL),$01            ; palette bank 1 for the how-high screen
0BEF: 23              INC     HL                  
0BF0: 36 00           LD      (HL),$00            
0BF2: 21 8A 60        LD      HL,$608A            
0BF5: 36 02           LD      (HL),$02            ; the level-start tune
0BF7: 23              INC     HL                  
0BF8: 36 03           LD      (HL),$03            ; held for three frames
0BFA: 21 A7 63        LD      HL,$63A7            
0BFD: 36 00           LD      (HL),$00            ; reset the climb-figure record index
0BFF: 21 DC 76        LD      HL,$76DC            
0C02: 22 A8 63        LD      (workRam+3A8),HL    ; seed the sprite-slot walk pointer
0C05: 3A 2E 62        LD      A,(workRam+22E)     ; 
0C08: FE 06           CP      $06                 ; the height is clamped to five girders
0C0A: 38 05           JR      C,loc_0c11          ; 
0C0C: 3E 05           LD      A,$05               
0C0E: 32 2E 62        LD      (workRam+22E),A     ; write the clamp back

loc_0c11:
0C11: 3A 2F 62        LD      A,(workRam+22F)     ; the board-order pointer as it was last time
0C14: 47              LD      B,A                 
0C15: 3A 2A 62        LD      A,(workRam+22A)     ; and where it is now
0C18: B8              CP      B                   
0C19: 28 04           JR      Z,loc_0c1f          ; unchanged, so the same height as before
0C1B: 21 2E 62        LD      HL,$622E            
0C1E: 34              INC     (HL)                ; a board was cleared: one girder taller

loc_0c1f:
0C1F: 32 2F 62        LD      (workRam+22F),A     ; remember it for next time
0C22: 3A 2E 62        LD      A,(workRam+22E)     ; 
0C25: 47              LD      B,A                 ; the height index is the girder-row count
0C26: 21 BC 75        LD      HL,$75BC            ; the first girder cell

loc_0c29:
0C29: 0E 50           LD      C,$50               ; the first girder tile code of the row

loc_0c2b:
0C2B: 71              LD      (HL),C              
0C2C: 0C              INC     C                   
0C2D: 2B              DEC     HL                  
0C2E: 71              LD      (HL),C              
0C2F: 0C              INC     C                   
0C30: 2B              DEC     HL                  
0C31: 71              LD      (HL),C              
0C32: 0C              INC     C                   
0C33: 2B              DEC     HL                  
0C34: 71              LD      (HL),C              
0C35: 79              LD      A,C                 
0C36: FE 67           CP      $67                 ; the last girder tile code ends the row
0C38: CA 43 0C        JP      Z,loc_0c43          ; 
0C3B: 0C              INC     C                   
0C3C: 11 23 00        LD      DE,$0023            ; step to the next four-tile group
0C3F: 19              ADD     HL,DE               
0C40: C3 2B 0C        JP      loc_0c2b            ; 

loc_0c43:
0C43: 3A A7 63        LD      A,(workRam+3A7)     ; the climb-figure record index
0C46: 3C              INC     A                   
0C47: 32 A7 63        LD      (workRam+3A7),A     ; step it for the next row
0C4A: 3D              DEC     A                   
0C4B: CB 27           SLA     A                   ; four bytes per figure record
0C4D: CB 27           SLA     A                   
0C4F: E5              PUSH    HL                  ; keep the girder cursor across the figure copy
0C50: 21 F0 3C        LD      HL,$3CF0            ; the climb-figure record table
0C53: C5              PUSH    BC                  
0C54: DD 2A A8 63     LD      IX,(workRam+3A8)    ; the sprite slot this row's figure goes in
0C58: 4F              LD      C,A                 
0C59: 06 00           LD      B,$00               
0C5B: 09              ADD     HL,BC               
0C5C: 7E              LD      A,(HL)              
0C5D: DD 77 60        LD      (IX+$60),A          ; copy the record's three bytes into the sprite slot
0C60: 23              INC     HL                  
0C61: 7E              LD      A,(HL)              
0C62: DD 77 40        LD      (IX+$40),A          
0C65: 23              INC     HL                  
0C66: 7E              LD      A,(HL)              
0C67: DD 77 20        LD      (IX+$20),A          
0C6A: DD 36 E0 8B     LD      (IX-$20),$8B        ; a fixed tile just below the figure
0C6E: C1              POP     BC                  
0C6F: DD E5           PUSH    IX                  
0C71: E1              POP     HL                  
0C72: 11 FC FF        LD      DE,$FFFC            ; the next figure sits four slots back
0C75: 19              ADD     HL,DE               
0C76: 22 A8 63        LD      (workRam+3A8),HL    ; 
0C79: E1              POP     HL                  
0C7A: 11 5F FF        LD      DE,$FF5F            ; back up to the next girder row
0C7D: 19              ADD     HL,DE               
0C7E: 05              DEC     B                   
0C7F: C2 29 0C        JP      NZ,loc_0c29         ; one row per unit of height
0C82: 11 07 03        LD      DE,$0307            ; post the screen's composition task
0C85: CD 9F 30        CALL    enqueueTask         ; 
0C88: 21 09 60        LD      HL,$6009            
0C8B: 36 A0           LD      (HL),$A0            ; hold the finished screen 160 frames
0C8D: 23              INC     HL                  
0C8E: 34              INC     (HL)                
0C8F: 34              INC     (HL)                ; advance the sub-state by two
0C90: C9              RET                         

; The gated board build. Every frame it counts substateTimer down by one and, while the
; timer is still above zero, does nothing else; only on the tick that brings it to zero
; does the full board build run -- which includes writing the palette-bank latch the
; display reads to pick its colour set. The polarity matters: the build fires on EXPIRY,
; not while counting. The board builder also has a second, ungated entry, used for the
; timed advance into 25m.
buildBoardWhenTimerExpires:
0C91: DF              RST     $18                 ; build the board only on the frame the timer expires

; Build a board. Clear the tilemap playfield and the sprite shadow buffer, reset
; bonusDisplay to 0, post the opening deferred task (opcode 5, argument 1), and select
; palette bank 2 by clearing bit 0 and setting bit 1 of the two-bit palette-bank latch.
; Then read board and hand off to the matching per-board setup arm: 1 = 25m girders, 2 =
; 50m conveyors, 3 = 75m elevators. Any other value -- in play, board 4 -- falls into the
; inline 100m rivet arm, which clears the sprite rows, raises the palette bank to 3,
; queues the rivet background tune into sndBgm, points at the rivet layout table, and
; runs the same shared draw/setup tail the other three arms converge on.
buildBoard:
0C92: CD 74 08        CALL    clearPlayfieldAndSprites; wipe the previous board
0C95: AF              XOR     A                   
0C96: 32 8C 63        LD      (workRam+38C),A     ; start the bonus readout at zero
0C99: 11 01 05        LD      DE,$0501            ; the opening task: opcode 5, argument 1
0C9C: CD 9F 30        CALL    enqueueTask         ; post it onto the deferred-work queue
0C9F: 21 86 7D        LD      HL,$7D86            ; point at the two palette-bank latches
0CA2: 36 00           LD      (HL),$00            ; bit 0 clear
0CA4: 23              INC     HL                  
0CA5: 36 01           LD      (HL),$01            ; bit 1 set: colour bank 2 for the build
0CA7: 3A 27 62        LD      A,(workRam+227)     ; which board is being built
0CAA: 3D              DEC     A                   
0CAB: CA D4 0C        JP      Z,setup25mGirderBoard; board 1: the 25m girders
0CAE: 3D              DEC     A                   
0CAF: CA DF 0C        JP      Z,setup50mConveyorBoard; board 2: the 50m conveyors
0CB2: 3D              DEC     A                   
0CB3: CA F2 0C        JP      Z,setUp75mBoard     ; board 3: the 75m elevators
0CB6: CD 43 0D        CALL    stampRivetBoardBands; board 4 falls through: the rivets
0CB9: 21 86 7D        LD      HL,$7D86            ; back to the palette-bank latches
0CBC: 36 01           LD      (HL),$01            ; raise bit 0: colour bank 3 for the rivets
0CBE: 3E 0B           LD      A,$0B               
0CC0: 32 89 60        LD      (workRam+89),A      ; start the rivet-board background tune
0CC3: 11 8B 3C        LD      DE,$3C8B            ; select the 100m rivet layout table

loc_0cc6:
0CC6: CD A7 0D        CALL    drawBoardLayout     ; walk the layout table into the playfield
0CC9: 3A 27 62        LD      A,(workRam+227)     ; 
0CCC: FE 04           CP      $04                 ; the rivet board has extra decoration
0CCE: CC 00 0D        CALL    Z,stampRivetBoardTiles; only on the rivet board
0CD1: C3 A0 3F        JP      loc_3fa0            ; on into the rest of board setup

; The 25m arm of the four-way board-setup dispatch. It points the layout selector at the
; 25m girder layout -- the sentinel-terminated list of girder and ladder segment records
; the shared tail walks into the playfield -- queues tune 8 into sndBgm, then falls into
; the tail all four arms share, whose return is this routine's return.
setup25mGirderBoard:
0CD4: 11 E4 3A        LD      DE,$3AE4            ; select the 25m girder layout table
0CD7: 3E 08           LD      A,$08               
0CD9: 32 89 60        LD      (workRam+89),A      ; start the 25m background tune
0CDC: C3 C6 0C        JP      loc_0cc6            ; join the shared board-setup tail

; The board-setup arm for 50m, the conveyor board. Selects the 50m colour set by writing
; 1 and 0 to the two hardware palette-bank latches the display reads, selects background
; tune 0x09 (the boards take consecutive tune slots and 50m takes the middle one), then
; points at the 50m conveyor layout table and runs the shared tail that walks it into
; video RAM and finishes board setup.
setup50mConveyorBoard:
0CDF: 11 5D 3B        LD      DE,$3B5D            ; select the 50m conveyor layout table
0CE2: 21 86 7D        LD      HL,$7D86            
0CE5: 36 01           LD      (HL),$01            ; bit 0 set
0CE7: 23              INC     HL                  
0CE8: 36 00           LD      (HL),$00            ; bit 1 clear: colour bank 1 for 50m
0CEA: 3E 09           LD      A,$09               
0CEC: 32 89 60        LD      (workRam+89),A      ; the 50m tune, the middle of the four slots
0CEF: C3 C6 0C        JP      loc_0cc6            ; join the shared board-setup tail

; The 75m (elevators) board-setup arm, one of four the board build branches to on board.
; Each arm makes its board's three fixed choices and hands off to the shared draw tail.
; This one plants the elevator board's fixed decorative tile motifs into the background
; tilemap -- the extra step the flat 25m and 50m boards do not have -- selects the 75m
; background tune (the four arms write consecutive tune slots, and this is the third),
; then points at the 75m elevator layout table and runs the tail, which walks that table
; into video RAM.
setUp75mBoard:
0CF2: CD 27 0D        CALL    stamp75mBoardTiles  ; the elevator board's fixed scenery
0CF5: 3E 0A           LD      A,$0A               
0CF7: 32 89 60        LD      (workRam+89),A      ; start the 75m background tune
0CFA: 11 E5 3B        LD      DE,$3BE5            ; select the 75m elevator layout table
0CFD: C3 C6 0C        JP      loc_0cc6            ; join the shared board-setup tail

; Stamp a fixed two-tile motif into eight video-RAM cells during 100m rivet-board setup.
; It walks an eight-entry little-endian pointer table and writes tile codes 0xB8 then
; 0xB7 into each destination pair -- two groups of four cells at a stride of five
; columns. It takes no argument and every stored value is a constant, so the sixteen
; writes are identical on every call.
stampRivetBoardTiles:
0D00: 06 08           LD      B,$08               ; eight destinations to stamp
0D02: 21 17 0D        LD      HL,$0D17            ; the table of video-RAM destinations

loc_0d05:
0D05: 3E B8           LD      A,$B8               ; the first of the two tile codes
0D07: 0E 02           LD      C,$02               ; two cells per destination
0D09: 5E              LD      E,(HL)              ; read the little-endian destination pointer
0D0A: 23              INC     HL                  
0D0B: 56              LD      D,(HL)              
0D0C: 23              INC     HL                  

loc_0d0d:
0D0D: 12              LD      (DE),A              ; stamp the tile
0D0E: 3D              DEC     A                   ; the second cell takes the next code down
0D0F: 13              INC     DE                  
0D10: 0D              DEC     C                   
0D11: C2 0D 0D        JP      NZ,loc_0d0d         ; 
0D14: 10 EF           DJNZ    loc_0d05            ; on to the next destination
0D16: C9              RET                         

; ---- $0D17-$0D26: data ----
0D17: CA 76 CF 76 D4 76 D9 76 2A 75 2F 75 34 75 39 75

; Part of the static background of the elevator board, planted before its layout table
; is chosen. It calls the shared two-row filler at two hard-coded tilemap positions,
; each laying 17 cells of one tile code along a row and 17 cells of a second code on the
; row directly below; the second motif sits eight tilemap rows above the first, and the
; two are disjoint -- 68 background cells in all. Every value and destination is a baked
; constant, so it reads no memory and repaints identically on every call. The writes
; land in the background tilemap, not the sprite buffer, and they set tile codes rather
; than blanking anything.
stamp75mBoardTiles:
0D27: 21 0D 77        LD      HL,$770D            ; top-left cell of the first two-row motif
0D2A: CD 30 0D        CALL    fillTileRowPair     ; 
0D2D: 21 0D 76        LD      HL,$760D            ; the second motif, eight tilemap rows on

; Lay a fixed two-row scenery motif into the background tilemap from the caller's
; top-left cell: 17 consecutive cells of one tile, then skip 15, then 17 cells of a
; second tile directly beneath the first run. The tilemap is 32 cells wide, so 17
; written plus 15 skipped is exactly one row and the walk lands on the same column one
; row on. Both tile codes and both counts are fixed here; only the start cell is the
; caller's.
fillTileRowPair:
0D30: 06 11           LD      B,$11               ; 17 cells in each row of the motif

loc_0d32:
0D32: 36 FD           LD      (HL),$FD            ; row one's tile code
0D34: 23              INC     HL                  
0D35: 10 FB           DJNZ    loc_0d32            ; 
0D37: 11 0F 00        LD      DE,$000F            ; the 15 cells left in the 32-wide row
0D3A: 19              ADD     HL,DE               ; land on the same column, one row on
0D3B: 06 11           LD      B,$11               

loc_0d3d:
0D3D: 36 FC           LD      (HL),$FC            ; row two's tile, directly beneath row one
0D3F: 23              INC     HL                  
0D40: 10 FB           DJNZ    loc_0d3d            ; 
0D42: C9              RET                         

; Stamp the two-band tile motif into two fixed tilemap rows during 100m setup. A thin
; wrapper over the shared two-band filler, run once per row base: each pass lays tile
; 0xFD across four consecutive cells, steps over a 28-cell gap, and lays tile 0xFC across
; four more -- sixteen cells in all, eight per row. Both row bases are constants and it
; takes no argument.
stampRivetBoardBands:
0D43: 21 87 76        LD      HL,$7687            ; the first of two tilemap row bases
0D46: CD 4C 0D        CALL    stampTwoTileBands   ; 
0D49: 21 47 75        LD      HL,$7547            ; the second row base, then fall through

; Lay two four-cell tile bands into a tilemap row. From the row base in HL it writes tile
; 0xFD across four consecutive cells, steps forward over a 28-cell gap, and writes tile
; 0xFC across four more -- eight cells in all, ending 36 past the base. Both run lengths
; and both tile codes are constants, so the only input is the base pointer. Called twice
; from the 100m rivet-board setup, at two fixed row bases.
stampTwoTileBands:
0D4C: 06 04           LD      B,$04               ; four cells in the first band

loc_0d4e:
0D4E: 36 FD           LD      (HL),$FD            ; the first band's tile code
0D50: 23              INC     HL                  
0D51: 10 FB           DJNZ    loc_0d4e            ; 
0D53: 11 1C 00        LD      DE,$001C            ; the 28-cell gap between the two bands
0D56: 19              ADD     HL,DE               
0D57: 06 04           LD      B,$04               

loc_0d59:
0D59: 36 FC           LD      (HL),$FC            ; the second band's tile code
0D5B: 23              INC     HL                  
0D5C: 10 FB           DJNZ    loc_0d59            ; 
0D5E: C9              RET                         

loc_0d5f:
0D5F: CD 56 0F        CALL    initBoardState      ; reset the board's work RAM and objects

loc_0d62:
0D62: CD 41 24        CALL    loadBoardObjectRecords; 

loc_0d65:
0D65: 21 09 60        LD      HL,$6009            
0D68: 36 40           LD      (HL),$40            ; hold the built board for 64 frames
0D6A: 23              INC     HL                  
0D6B: 34              INC     (HL)                ; then step on to the next sub-state
0D6C: 21 5C 38        LD      HL,$385C            ; the sprite-object template in program data
0D6F: CD 4E 00        CALL    loadSpriteObjectBlock; it leaves its source pointer advanced
0D72: 11 00 69        LD      DE,$6900            ; the head of the sprite shadow buffer
0D75: 01 08 00        LD      BC,$0008            ; two more records from the same stream
0D78: ED B0           LDIR                        
0D7A: 3A 27 62        LD      A,(workRam+227)     ; 
0D7D: FE 04           CP      $04                 ; the rivet board shifts its sprites
0D7F: 28 0A           JR      Z,loc_0d8b          ; 
0D81: 0F              RRCA                        
0D82: 0F              RRCA                        ; test bit 1 of the board number
0D83: D8              RET     C                   ; 50m and 75m need no offset at all
0D84: 21 0B 69        LD      HL,$690B            ; the Y byte of the first sprite record
0D87: 0E FC           LD      C,$FC               ; shift the whole Y column up by four
0D89: FF              RST     $38                 ; apply it across all ten records
0D8A: C9              RET                         

loc_0d8b:
0D8B: 21 08 69        LD      HL,$6908            ; the X byte of the first sprite record
0D8E: 0E 44           LD      C,$44               ; shift the whole X column right by 0x44
0D90: FF              RST     $38                 ; apply it across all ten records
0D91: 11 04 00        LD      DE,$0004            ; stride four: one byte per sprite record
0D94: 01 10 02        LD      BC,$0210            ; two records, each nudged up by 0x10
0D97: 21 00 69        LD      HL,$6900            ; from the first sprite record's X
0D9A: CD 3D 00        CALL    addStrided          ; 
0D9D: 01 F8 02        LD      BC,$02F8            ; two more, each nudged down by eight
0DA0: 21 03 69        LD      HL,$6903            ; from the first sprite record's Y
0DA3: CD 3D 00        CALL    addStrided          ; 
0DA6: C9              RET                         

; The head of the playfield walk: draw the whole static board from its segment table.
; Each record is at least five bytes -- kind, then the y and x of the first point and
; the y and x of the second -- and the table ends on the terminator kind 0xAA. Per
; record it converts the first point to a tile address into segAddr1, saves the sub-tile
; remainders the conversion discards into segSubtileY1 and segSubtile1, stashes the kind
; in segKind, computes the height as the ABSOLUTE difference of the two y values (an
; unsigned extent, so either ordering gives the same magnitude), and hands the second
; point and that height to the per-segment step. That step converts the second point and
; dispatches the girder or ladder drawer, whose renderer tail advances the table pointer
; past the record and comes back, so the walk continues to the terminator.
drawBoardLayout:
0DA7: 1A              LD      A,(DE)              ; read the next record's kind byte
0DA8: 32 B3 63        LD      (workRam+3B3),A     ; stash it for the drawers to dispatch on
0DAB: FE AA           CP      $AA                 ; the table's terminator
0DAD: C8              RET     Z                   ; no records left, the board is drawn
0DAE: 13              INC     DE                  
0DAF: 1A              LD      A,(DE)              ; the first point's y
0DB0: 67              LD      H,A                 
0DB1: 44              LD      B,H                 ; keep y for the sub-tile remainder
0DB2: 13              INC     DE                  
0DB3: 1A              LD      A,(DE)              ; the first point's x
0DB4: 6F              LD      L,A                 
0DB5: 4D              LD      C,L                 ; keep x for the run and the remainder
0DB6: D5              PUSH    DE                  ; the conversion clobbers the table cursor
0DB7: CD F0 2F        CALL    tileAddrForPixel    ; 
0DBA: D1              POP     DE                  
0DBB: 22 AB 63        LD      (workRam+3AB),HL    ; the cell the first point falls in
0DBE: 78              LD      A,B                 
0DBF: E6 07           AND     $07                 ; the low bits the conversion dropped
0DC1: 32 B4 63        LD      (workRam+3B4),A     ; 
0DC4: 79              LD      A,C                 
0DC5: E6 07           AND     $07                 ; the same for x
0DC7: 32 AF 63        LD      (workRam+3AF),A     ; 
0DCA: 13              INC     DE                  
0DCB: 1A              LD      A,(DE)              ; the second point's y
0DCC: 67              LD      H,A                 
0DCD: 90              SUB     B                   ; the segment's height, either ordering
0DCE: D2 D3 0D        JP      NC,loc_0dd3         ; 
0DD1: ED 44           NEG                         ; make it an unsigned extent

loc_0dd3:
0DD3: 32 B1 63        LD      (workRam+3B1),A     ; 
0DD6: 13              INC     DE                  
0DD7: 1A              LD      A,(DE)              ; the second point's x
0DD8: 6F              LD      L,A                 
0DD9: 91              SUB     C                   ; the horizontal run between the points
0DDA: 32 B2 63        LD      (workRam+3B2),A     ; 
0DDD: 1A              LD      A,(DE)              
0DDE: E6 07           AND     $07                 ; the second point's sub-tile x
0DE0: 32 B0 63        LD      (workRam+3B0),A     ; 
0DE3: D5              PUSH    DE                  ; save the cursor across the conversion
0DE4: CD F0 2F        CALL    tileAddrForPixel    ; 
0DE7: D1              POP     DE                  
0DE8: 22 AD 63        LD      (workRam+3AD),HL    ; the cell the far end falls in
0DEB: 3A B3 63        LD      A,(workRam+3B3)     ; 
0DEE: FE 02           CP      $02                 ; kinds are compared as signed here
0DF0: F2 4F 0E        JP      P,drawGirderSpan    ; kind 2 and up: the girder drawer
0DF3: 3A B2 63        LD      A,(workRam+3B2)     ; kinds 0 and 1: the ladders
0DF6: D6 10           SUB     $10                 
0DF8: 47              LD      B,A                 
0DF9: 3A AF 63        LD      A,(workRam+3AF)     ; 
0DFC: 80              ADD     A,B                 ; fold the sub-tile offset into the run
0DFD: 32 B2 63        LD      (workRam+3B2),A     ; 
0E00: 3A AF 63        LD      A,(workRam+3AF)     ; 
0E03: C6 F0           ADD     A,$F0               ; the near-end cap tile for that offset
0E05: 2A AB 63        LD      HL,(workRam+3AB)    ; 
0E08: 77              LD      (HL),A              ; stamp it at the first point
0E09: 2C              INC     L                   
0E0A: D6 30           SUB     $30                 
0E0C: 77              LD      (HL),A              ; and its partner in the next cell
0E0D: 3A B3 63        LD      A,(workRam+3B3)     ; 
0E10: FE 01           CP      $01                 
0E12: C2 19 0E        JP      NZ,drawLadder       ; kind 0 keeps its body run
0E15: AF              XOR     A                   
0E16: 32 B2 63        LD      (workRam+3B2),A     ; kind 1 is caps only: no body run

; Fill a board-layout segment's body run with the uniform ladder tile 0xC0, then draw
; its end cap. The layout walk hands over a write pointer already aimed at the segment's
; first cell and segRun holding the run's pixel extent; each step stamps one cell and
; pays segRun down by 8 pixels, storing the final borrowed value too. The pointer
; advances along the raw tilemap COLUMN axis, which the quarter-turn display makes the
; DISPLAYED VERTICAL -- so these are short vertical runs on screen, the opposite of what
; the raw axis suggests. What it lays down is the ladders: the two full-height ones
; beside Kong plus eight shorter segments. It touches no girder pixel. The pointer
; advances by its low byte only, so the walk wraps inside the 256-cell page and never
; crosses into the next row's high byte.
drawLadder:
0E19: 3A B2 63        LD      A,(workRam+3B2)     ; 
0E1C: D6 08           SUB     $08                 ; pay one tile, eight pixels, off the run
0E1E: 32 B2 63        LD      (workRam+3B2),A     ; 
0E21: DA 2A 0E        JP      C,drawSegmentEndCap ; run spent: cap the far end
0E24: 2C              INC     L                   ; next cell along the ladder
0E25: 36 C0           LD      (HL),$C0            ; the uniform ladder body tile
0E27: C3 19 0E        JP      drawLadder          ; 

; Close a layout segment at its far endpoint, then step the table cursor on. The
; board-layout renderer walks a table of segment records -- girders and the like --
; filling each run with body tile 0xC0; this is the end cap that runs right after that
; fill. At the pointer in segAddr2 it always writes segSubtile2 + 0xD0. If segKind is 1,
; a single-cell segment, it also writes 0xC0 one cell BACK; and if the sub-tile remainder
; is non-zero, a partial-cell overhang, it writes remainder + 0xE0 one cell FORWARD. Both
; neighbours move the LOW byte only, so a segment ending at a page edge stays on its row.
drawSegmentEndCap:
0E2A: 3A B0 63        LD      A,(workRam+3B0)     ; 
0E2D: C6 D0           ADD     A,$D0               ; the far-end tile for that sub-tile offset
0E2F: 2A AD 63        LD      HL,(workRam+3AD)    ; 
0E32: 77              LD      (HL),A              ; always stamped, at the second point
0E33: 3A B3 63        LD      A,(workRam+3B3)     ; 
0E36: FE 01           CP      $01                 ; a single-cell segment closes behind too
0E38: C2 3F 0E        JP      NZ,loc_0e3f         ; 
0E3B: 2D              DEC     L                   
0E3C: 36 C0           LD      (HL),$C0            ; the ladder body tile one cell back
0E3E: 2C              INC     L                   

loc_0e3f:
0E3F: 3A B0 63        LD      A,(workRam+3B0)     ; 
0E42: FE 00           CP      $00                 ; a non-zero remainder overhangs a cell
0E44: CA 4B 0E        JP      Z,loc_0e4b          ; 
0E47: C6 E0           ADD     A,$E0               ; the partial-cell tile for the overhang
0E49: 2C              INC     L                   
0E4A: 77              LD      (HL),A              ; stamp it one cell forward

loc_0e4b:
0E4B: 13              INC     DE                  ; step the cursor past this record
0E4C: C3 A7 0D        JP      drawBoardLayout     ; back to the walk for the next one

; Draw a girder: stamp a run of slope-band tiles for a kind-2 board-layout record.
; Stepping the write pointer by 32 walks the raw tilemap row axis, but the screen is
; turned a quarter turn, so on the glass this lays long sloped HORIZONTAL runs, about 25
; cells long and 2 to 4 thick; the ladders are the layout walk's other drawer. segHeight
; is paid down 8px per row until it borrows, and each row stamps segTile (the segment's
; 0-7 sub-tile offset biased by -0x10, one tile per phase of the 0xF0-0xF7 band) plus the
; paired half-tile beside it, skipping the pair when
; the cell wrapped off the row's right edge or, on the leading row, on the 0xF0 sentinel.
; segRun selects the slant: 0 redraws the same column, positive walks the tile code up
; through the 0xF0-0xF8 band and shifts a column on at the wrap, negative the reverse.
; Any other kind goes to the capped-column drawer.
drawGirderSpan:
0E4F: 3A B3 63        LD      A,(workRam+3B3)     ; 
0E52: FE 02           CP      $02                 ; only kind 2 belongs to this drawer
0E54: C2 E8 0E        JP      NZ,drawCappedTileColumn; kinds 3 and up go elsewhere
0E57: 3A AF 63        LD      A,(workRam+3AF)     ; 
0E5A: C6 F0           ADD     A,$F0               ; the slope tile for that sub-tile offset
0E5C: 32 B5 63        LD      (workRam+3B5),A     ; the live tile code lives in this cell
0E5F: 2A AB 63        LD      HL,(workRam+3AB)    ; start at the girder's first cell

loc_0e62:
0E62: 3A B5 63        LD      A,(workRam+3B5)     ; 
0E65: 77              LD      (HL),A              ; stamp this row's slope tile
0E66: 23              INC     HL                  
0E67: 7D              LD      A,L                 
0E68: E6 1F           AND     $1F                 ; did that run off the row's edge?
0E6A: CA 78 0E        JP      Z,loc_0e78          ; 
0E6D: 3A B5 63        LD      A,(workRam+3B5)     ; 
0E70: FE F0           CP      $F0                 ; the band's first tile has no partner
0E72: CA 78 0E        JP      Z,loc_0e78          ; 
0E75: D6 10           SUB     $10                 
0E77: 77              LD      (HL),A              ; the paired half-tile beside it

loc_0e78:
0E78: 01 1F 00        LD      BC,$001F            
0E7B: 09              ADD     HL,BC               ; on to the next tilemap row
0E7C: 3A B1 63        LD      A,(workRam+3B1)     ; 
0E7F: D6 08           SUB     $08                 ; pay eight pixels of height per row
0E81: DA CF 0E        JP      C,loc_0ecf          ; height spent: the girder is drawn
0E84: 32 B1 63        LD      (workRam+3B1),A     ; 
0E87: 3A B2 63        LD      A,(workRam+3B2)     ; 
0E8A: FE 00           CP      $00                 ; a zero run means a level girder
0E8C: CA 62 0E        JP      Z,loc_0e62          ; 
0E8F: 3A B5 63        LD      A,(workRam+3B5)     ; 
0E92: 77              LD      (HL),A              ; sloping: a second row at the same code
0E93: 23              INC     HL                  
0E94: 7D              LD      A,L                 
0E95: E6 1F           AND     $1F                 
0E97: CA A0 0E        JP      Z,loc_0ea0          ; 
0E9A: 3A B5 63        LD      A,(workRam+3B5)     ; 
0E9D: D6 10           SUB     $10                 
0E9F: 77              LD      (HL),A              ; its paired half-tile

loc_0ea0:
0EA0: 01 1F 00        LD      BC,$001F            
0EA3: 09              ADD     HL,BC               ; on to the next row
0EA4: 3A B1 63        LD      A,(workRam+3B1)     ; 
0EA7: D6 08           SUB     $08                 
0EA9: DA CF 0E        JP      C,loc_0ecf          ; 
0EAC: 32 B1 63        LD      (workRam+3B1),A     ; 
0EAF: 3A B2 63        LD      A,(workRam+3B2)     ; 
0EB2: CB 7F           BIT     7,A                 ; which way does the girder slope?
0EB4: C2 D3 0E        JP      NZ,loc_0ed3         ; a negative run slopes the other way
0EB7: 3A B5 63        LD      A,(workRam+3B5)     ; 
0EBA: 3C              INC     A                   ; step one tile up the slope band
0EBB: 32 B5 63        LD      (workRam+3B5),A     ; 
0EBE: FE F8           CP      $F8                 ; stepped past the end of the band?
0EC0: C2 C9 0E        JP      NZ,loc_0ec9         ; 
0EC3: 23              INC     HL                  ; shift a column on
0EC4: 3E F0           LD      A,$F0               ; and wrap back to the band's first tile
0EC6: 32 B5 63        LD      (workRam+3B5),A     ; 

loc_0ec9:
0EC9: 7D              LD      A,L                 
0ECA: E6 1F           AND     $1F                 ; stop if that shift left the row
0ECC: C2 62 0E        JP      NZ,loc_0e62         ; 

loc_0ecf:
0ECF: 13              INC     DE                  ; step the cursor past this record
0ED0: C3 A7 0D        JP      drawBoardLayout     ; back to the walk

loc_0ed3:
0ED3: 3A B5 63        LD      A,(workRam+3B5)     ; 
0ED6: 3D              DEC     A                   ; step one tile down the slope band
0ED7: 32 B5 63        LD      (workRam+3B5),A     ; 
0EDA: FE F0           CP      $F0                 ; dropped below the band?
0EDC: F2 E5 0E        JP      P,loc_0ee5          ; 
0EDF: 2B              DEC     HL                  ; shift a column back
0EE0: 3E F7           LD      A,$F7               ; and re-seat on the band's last tile
0EE2: 32 B5 63        LD      (workRam+3B5),A     ; 

loc_0ee5:
0EE5: C3 62 0E        JP      loc_0e62            ; 

; The kind-3 arm of the board-layout drawer chain: lay a CAPPED vertical tile run down
; the tilemap from the record's converted address segAddr1, sized by segHeight. A top
; cap 0xB3 at the address, then body tiles 0xB1 each one whole tilemap row (0x20)
; further down, then a bottom cap 0xB2 on the row where the extent subtraction borrows.
; The extent is paid down 16 pixels on the first step -- the top cap is two tiles tall
; -- and 8 per body row after, and it lives IN segHeight, reloaded, decremented and
; stored every row rather than in a hidden local. Kind 4 and up is handed straight on to
; the uniform column fill, which draws one tile with a flat 8-pixel step and no caps.
; Either way the record pointer ends one past this record and the walk resumes at the
; next one.
drawCappedTileColumn:
0EE8: 3A B3 63        LD      A,(workRam+3B3)     ; 
0EEB: FE 03           CP      $03                 ; only kind 3 belongs to this one
0EED: C2 1B 0F        JP      NZ,fillTileColumn   ; kind 4 and up: the plain column fill
0EF0: 2A AB 63        LD      HL,(workRam+3AB)    ; start at the cell the record falls in
0EF3: 3E B3           LD      A,$B3               
0EF5: 77              LD      (HL),A              ; the top cap tile
0EF6: 01 20 00        LD      BC,$0020            
0EF9: 09              ADD     HL,BC               ; step one whole tilemap row
0EFA: 3A B1 63        LD      A,(workRam+3B1)     ; 
0EFD: D6 10           SUB     $10                 ; the top cap is two tiles tall

loc_0eff:
0EFF: DA 14 0F        JP      C,loc_0f14          ; extent spent: lay the bottom cap
0F02: 32 B1 63        LD      (workRam+3B1),A     ; 
0F05: 3E B1           LD      A,$B1               
0F07: 77              LD      (HL),A              ; a body tile
0F08: 01 20 00        LD      BC,$0020            
0F0B: 09              ADD     HL,BC               
0F0C: 3A B1 63        LD      A,(workRam+3B1)     ; 
0F0F: D6 08           SUB     $08                 ; each body row spends eight pixels
0F11: C3 FF 0E        JP      loc_0eff            ; 

loc_0f14:
0F14: 3E B2           LD      A,$B2               
0F16: 77              LD      (HL),A              ; the bottom cap tile
0F17: 13              INC     DE                  ; step the cursor past this record
0F18: C3 A7 0D        JP      drawBoardLayout     ; 

; The board-layout renderer's arm for segment records of kind 4, 5 and 6: draw a solid
; vertical run of one tile. The kind picks the fill code -- 4 and 5 each have their own,
; 6 and anything else take the default -- and it is stashed in segTile. The fill runs
; down from segAddr1, the cell the record's corner fell in, laying a tile, stepping one
; whole tilemap row, and paying segHeight down by one tile's worth of pixels; at least
; one tile is always laid. Kind 7 or more bails without drawing. Both exits step the
; record pointer one byte past this record, and control returns to the walk stepping
; through them.
fillTileColumn:
0F1B: 3A B3 63        LD      A,(workRam+3B3)     ; 
0F1E: FE 07           CP      $07                 ; a sign test, not an unsigned compare
0F20: F2 CF 0E        JP      P,loc_0ecf          ; kind 7 and up draws nothing at all
0F23: FE 04           CP      $04                 
0F25: CA 4C 0F        JP      Z,loc_0f4c          ; kind 4 has its own fill tile
0F28: FE 05           CP      $05                 
0F2A: CA 51 0F        JP      Z,loc_0f51          ; so does kind 5
0F2D: 3E FE           LD      A,$FE               ; kind 6 takes the default fill tile

loc_0f2f:
0F2F: 32 B5 63        LD      (workRam+3B5),A     ; 
0F32: 2A AB 63        LD      HL,(workRam+3AB)    ; start at the cell the record falls in

; Fill a tilemap column downward from the layout cursor, then resume the walk. It lays
; segTile at the cursor, steps one whole tilemap row (the map is 32 cells wide) and pays
; segHeight down 8 pixels -- one tile -- per row, laying at least one tile and continuing
; while the height holds out. It then steps the record pointer past this record and draws
; the remaining ones.
fillColumnAndContinueWalk:
0F35: 3A B5 63        LD      A,(workRam+3B5)     ; 
0F38: 77              LD      (HL),A              ; lay the fill tile
0F39: 01 20 00        LD      BC,$0020            
0F3C: 09              ADD     HL,BC               ; step one whole tilemap row
0F3D: 3A B1 63        LD      A,(workRam+3B1)     ; 
0F40: D6 08           SUB     $08                 ; pay one tile of height
0F42: 32 B1 63        LD      (workRam+3B1),A     ; 
0F45: D2 35 0F        JP      NC,fillColumnAndContinueWalk; while height remains
0F48: 13              INC     DE                  ; step the cursor past this record
0F49: C3 A7 0D        JP      drawBoardLayout     ; 

loc_0f4c:
0F4C: 3E E0           LD      A,$E0               ; kind 4's fill tile
0F4E: C3 2F 0F        JP      loc_0f2f            ; 

loc_0f51:
0F51: 3E B0           LD      A,$B0               ; kind 5's fill tile
0F53: C3 2F 0F        JP      loc_0f2f            ; 

; Reset the per-board work RAM, compute the board's bonus and timer values, seed the
; shared top sprites, then dispatch to the board's own object setup. It zeroes the
; 39-byte player/motion block and the whole object-record plus sprite-buffer span, then
; copies a 64-byte board-object template out of program data over the head of it.
; bonusStart = bonus = bonusEventMark = min(level*10 + 40, 80), all at byte width -- the
; multiply was never widened, which is what gives the level-22 kill screen, where 260
; wraps to 4. bonusPeriod = bonusTick = max(220 - 2*bonus, 40). Two constant hit-box
; copies are set to 4 and 8. Unless board is the rivet board it seeds three 4-byte sprite
; records near the top of the sprite buffer. Finally it tail-dispatches on board to the
; per-board object seeding, 1 = 25m through 4 = 100m.
initBoardState:
0F56: 06 27           LD      B,$27               ; 39 bytes of player and motion state
0F58: 21 00 62        LD      HL,$6200            
0F5B: AF              XOR     A                   

loc_0f5c:
0F5C: 77              LD      (HL),A              ; wiped for the fresh board
0F5D: 2C              INC     L                   
0F5E: 10 FC           DJNZ    loc_0f5c            ; 
0F60: 0E 11           LD      C,$11               ; 17 blocks to clear
0F62: 16 80           LD      D,$80               ; 128 bytes each
0F64: 21 80 62        LD      HL,$6280            ; the object records and the sprite buffer

loc_0f67:
0F67: 42              LD      B,D                 

loc_0f68:
0F68: 77              LD      (HL),A              
0F69: 23              INC     HL                  
0F6A: 10 FC           DJNZ    loc_0f68            ; 
0F6C: 0D              DEC     C                   
0F6D: 20 F8           JR      NZ,loc_0f67         ; 
0F6F: 21 9C 3D        LD      HL,$3D9C            ; the 64-byte board-object template
0F72: 11 80 62        LD      DE,$6280            
0F75: 01 40 00        LD      BC,$0040            
0F78: ED B0           LDIR                        ; laid over the head of the cleared span
0F7A: 3A 29 62        LD      A,(workRam+229)     ; the bonus scales with the level
0F7D: 47              LD      B,A                 
0F7E: A7              AND     A                   ; clear the carry before each shift
0F7F: 17              RLA                         
0F80: A7              AND     A                   
0F81: 17              RLA                         
0F82: A7              AND     A                   
0F83: 17              RLA                         ; level times eight
0F84: 80              ADD     A,B                 
0F85: 80              ADD     A,B                 ; level times ten
0F86: C6 28           ADD     A,$28               ; plus 40; the readout shows 100 times this
0F88: FE 51           CP      $51                 
0F8A: 38 02           JR      C,loc_0f8e          ; 
0F8C: 3E 50           LD      A,$50               ; clamped at 80

loc_0f8e:
0F8E: 21 B0 62        LD      HL,$62B0            ; the three bonus cells sit together
0F91: 06 03           LD      B,$03               

loc_0f93:
0F93: 77              LD      (HL),A              ; start, current and event mark, all alike
0F94: 2C              INC     L                   
0F95: 10 FC           DJNZ    loc_0f93            ; 
0F97: 87              ADD     A,A                 ; twice the bonus
0F98: 47              LD      B,A                 
0F99: 3E DC           LD      A,$DC               
0F9B: 90              SUB     B                   ; 220 - 2*bonus: the bonus tick period
0F9C: FE 28           CP      $28                 
0F9E: 30 02           JR      NC,loc_0fa2         ; 
0FA0: 3E 28           LD      A,$28               ; floored at 40 frames

loc_0fa2:
0FA2: 77              LD      (HL),A              ; the tick period
0FA3: 2C              INC     L                   
0FA4: 77              LD      (HL),A              ; and the live countdown, seeded alike
0FA5: 21 09 62        LD      HL,$6209            
0FA8: 36 04           LD      (HL),$04            ; the two constant hit-box sizes
0FAA: 2C              INC     L                   
0FAB: 36 08           LD      (HL),$08            
0FAD: 3A 27 62        LD      A,(workRam+227)     ; 
0FB0: 4F              LD      C,A                 ; kept for the dispatch below
0FB1: CB 57           BIT     2,A                 ; only the rivet board has bit 2 set
0FB3: 20 16           JR      NZ,loc_0fcb         ; the rivet board skips these three
0FB5: 21 00 6A        LD      HL,$6A00            ; three decorative sprite records
0FB8: 3E 4F           LD      A,$4F               ; the first one's X
0FBA: 06 03           LD      B,$03               

loc_0fbc:
0FBC: 77              LD      (HL),A              
0FBD: 2C              INC     L                   
0FBE: 36 3A           LD      (HL),$3A            ; tile code
0FC0: 2C              INC     L                   
0FC1: 36 0F           LD      (HL),$0F            ; colour attribute
0FC3: 2C              INC     L                   
0FC4: 36 18           LD      (HL),$18            ; Y, the same for all three
0FC6: 2C              INC     L                   
0FC7: C6 10           ADD     A,$10               ; space the next one 16 across
0FC9: 10 F1           DJNZ    loc_0fbc            ; 

loc_0fcb:
0FCB: 79              LD      A,C                 
0FCC: EF              RST     $28                 ; vector to this board's own object setup

; ---- $0FCD-$0FD6: jump table ----
0FCD: 00 00 D7 0F 1F 10 87 10 31 11

; Build the 25m board's initial object records and their sprite shadows from fixed
; templates. It runs after the board-setup head has cleared that whole region, so it is
; pure initialisation: every byte comes from a template and none of its inputs come from
; prior work RAM. Straight-line, seven steps, no branches -- copy a 16-byte
; sprite-buffer block; stamp a 4-byte group into 5 strided records on the first object
; page; scatter a 6-byte template into one object record and a 4-byte sprite array; copy
; a 4-byte sprite record; seed an object pair from a position table and emit their two
; sprite records; stamp a 4-byte group into 8 strided records on the barrel page; then
; the SAME group into 2 records on the next page with only the destination and count
; reloaded. That last reuse is the one subtlety: the group replicator preserves its
; source pointer and stride across a call, and this relies on it.
seed25mBoardObjects:
0FD7: 21 DC 3D        LD      HL,$3DDC            ; a 16-byte sprite-record template
0FDA: 11 A8 69        LD      DE,$69A8            
0FDD: 01 10 00        LD      BC,$0010            
0FE0: ED B0           LDIR                        
0FE2: 21 EC 3D        LD      HL,$3DEC            ; a 4-byte group to broadcast
0FE5: 11 07 64        LD      DE,$6407            ; field +7 of the first fire record
0FE8: 0E 1C           LD      C,$1C               ; records 0x20 apart
0FEA: 06 05           LD      B,$05               ; five of them
0FEC: CD 2A 12        CALL    replicateGroupStrided; 
0FEF: 21 F4 3D        LD      HL,$3DF4            ; a 6-byte record template
0FF2: CD FA 11        CALL    loc_11fa            ; 
0FF5: 21 00 3E        LD      HL,$3E00            ; a single 4-byte sprite record
0FF8: 11 FC 69        LD      DE,$69FC            
0FFB: 01 04 00        LD      BC,$0004            
0FFE: ED B0           LDIR                        
1000: 21 0C 3E        LD      HL,$3E0C            ; the pair's two start positions
1003: CD A6 11        CALL    seedSpriteObjectPair; 
1006: 21 1B 10        LD      HL,$101B            ; the group is the four bytes just below
1009: 11 07 67        LD      DE,$6707            ; field +7 of the first barrel record
100C: 01 1C 08        LD      BC,$081C            ; eight records, 0x20 apart
100F: CD 2A 12        CALL    replicateGroupStrided; 
1012: 11 07 68        LD      DE,$6807            ; the same group into the next page
1015: 06 02           LD      B,$02               ; two records; source and stride carry over
1017: CD 2A 12        CALL    replicateGroupStrided; 
101A: C9              RET                         

; ---- $101B-$101E: data ----
101B: 00 00 02 02

; Board-object setup for 50m, tail-jumped to when board is 2. A one-shot with no inputs
; of its own -- every pointer, count and stride is an immediate, so it always seeds the
; same board -- and straight-line: five setup helpers, four block copies of fixed
; tables, and a close that sets the board-object marker to 1. It stamps 4-byte groups
; into records of the objArray64 and objArray65a0 blocks, seeds a ten-record block's
; shared sprite field from a template and gathers each record's permuted (X, code,
; attribute, Y) into the sprite areas, scatters a 6-byte record, copies three fixed
; tables into the shadow buffer, seeds an object pair from a position table, and copies
; the last table into the collision sprite records. What the individual records depict
; is not established; the mechanism is.
seed50mBoardObjects:
101F: 21 EC 3D        LD      HL,$3DEC            ; a 4-byte group to broadcast
1022: 11 07 64        LD      DE,$6407            ; field +7 of the first fire record
1025: 01 1C 05        LD      BC,$051C            ; five records, 0x20 apart
1028: CD 2A 12        CALL    replicateGroupStrided; 
102B: CD 86 11        CALL    seedObjectBlockSprites; 
102E: 21 18 3E        LD      HL,$3E18            ; another 4-byte group
1031: 11 A7 65        LD      DE,$65A7            ; field +7 of the first 50m object
1034: 01 0C 06        LD      BC,$060C            ; six records, 0x10 apart
1037: CD 2A 12        CALL    replicateGroupStrided; 
103A: DD 21 A0 65     LD      IX,$65A0            ; read those same six records back
103E: 21 B8 69        LD      HL,$69B8            ; their six hardware sprite records
1041: 11 10 00        LD      DE,$0010            ; records 0x10 apart
1044: 06 06           LD      B,$06               
1046: CD D3 11        CALL    gatherSpriteRecords ; 
1049: 21 FA 3D        LD      HL,$3DFA            ; a 6-byte record template
104C: CD FA 11        CALL    loc_11fa            ; 
104F: 21 04 3E        LD      HL,$3E04            ; a 4-byte sprite record
1052: 11 FC 69        LD      DE,$69FC            
1055: 01 04 00        LD      BC,$0004            
1058: ED B0           LDIR                        
105A: 21 1C 3E        LD      HL,$3E1C            ; an 8-byte sprite block
105D: 11 44 69        LD      DE,$6944            
1060: 01 08 00        LD      BC,$0008            
1063: ED B0           LDIR                        
1065: 21 24 3E        LD      HL,$3E24            ; a 24-byte sprite block
1068: 11 E4 69        LD      DE,$69E4            
106B: 01 18 00        LD      BC,$0018            
106E: ED B0           LDIR                        
1070: 21 10 3E        LD      HL,$3E10            ; the pair's two start positions
1073: CD A6 11        CALL    seedSpriteObjectPair; 
1076: 21 3C 3E        LD      HL,$3E3C            ; the three collision sprite records
1079: 11 0C 6A        LD      DE,$6A0C            
107C: 01 0C 00        LD      BC,$000C            
107F: ED B0           LDIR                        
1081: 3E 01           LD      A,$01               ; mark this board's objects set up
1083: 32 B9 62        LD      (workRam+2B9),A     ; 
1086: C9              RET                         

; Lay down the 75m board's object records and their sprite mirror from fixed templates.
; It takes no inputs -- every pointer, count and constant is an immediate -- so the
; elevator board always starts from the same object and sprite state. Ten steps:
; broadcast a 4-byte template into the +7 field of 5 records of objArray64; seed the
; actor array's sprite field and build its ten sprite records; mark 6 records of
; objArray66 active and put the first three into the spawn state; scatter six X/Y pairs
; into those records and gather their six sprite records; copy a 12-byte template into
; the object-collision sprite slot; activate the board's two fires at fixed coordinates;
; and copy a 16-byte table sitting immediately after this routine's own code into a
; further sprite-buffer slot.
seed75mBoardObjects:
1087: 21 EC 3D        LD      HL,$3DEC            ; a 4-byte group to broadcast
108A: 11 07 64        LD      DE,$6407            ; field +7 of the first fire record
108D: 01 1C 05        LD      BC,$051C            ; five records, 0x20 apart
1090: CD 2A 12        CALL    replicateGroupStrided; 
1093: CD 86 11        CALL    seedObjectBlockSprites; 
1096: 21 00 66        LD      HL,$6600            ; the board's own six-record array
1099: 11 10 00        LD      DE,$0010            ; records 0x10 apart
109C: 3E 01           LD      A,$01               
109E: 06 06           LD      B,$06               

loc_10a0:
10A0: 77              LD      (HL),A              ; mark each one active
10A1: 19              ADD     HL,DE               
10A2: 10 FC           DJNZ    loc_10a0            ; 
10A4: 0E 02           LD      C,$02               ; the state fill runs twice
10A6: 3E 08           LD      A,$08               ; the spawn state

loc_10a8:
10A8: 06 03           LD      B,$03               ; the first three records only
10AA: 21 0D 66        LD      HL,$660D            ; their state field

loc_10ad:
10AD: 77              LD      (HL),A              
10AE: 19              ADD     HL,DE               
10AF: 10 FC           DJNZ    loc_10ad            ; 
10B1: 3E 08           LD      A,$08               
10B3: 0D              DEC     C                   
10B4: C2 A8 10        JP      NZ,loc_10a8         ; the second pass rewrites the same cells
10B7: 21 64 3E        LD      HL,$3E64            ; six X/Y pairs from a fixed table
10BA: 11 03 66        LD      DE,$6603            ; into fields +3 and +5 of each record
10BD: 01 0E 06        LD      BC,$060E            ; six records, 0x10 apart
10C0: CD EC 11        CALL    copyBytePairsStrided; 
10C3: 21 60 3E        LD      HL,$3E60            ; a 4-byte appearance group
10C6: 11 07 66        LD      DE,$6607            ; field +7 of those same six records
10C9: 01 0C 06        LD      BC,$060C            
10CC: CD 2A 12        CALL    replicateGroupStrided; 
10CF: DD 21 00 66     LD      IX,$6600            ; read those six records back
10D3: 21 58 69        LD      HL,$6958            ; their six hardware sprite records
10D6: 06 06           LD      B,$06               
10D8: 11 10 00        LD      DE,$0010            
10DB: CD D3 11        CALL    gatherSpriteRecords ; 
10DE: 21 48 3E        LD      HL,$3E48            ; a 12-byte collision-sprite template
10E1: 11 0C 6A        LD      DE,$6A0C            
10E4: 01 0C 00        LD      BC,$000C            
10E7: ED B0           LDIR                        
10E9: DD 21 00 64     LD      IX,$6400            ; the fire records
10ED: DD 36 00 01     LD      (IX+$00),$01        ; the first fire, marked active
10F1: DD 36 03 58     LD      (IX+$03),$58        ; its X, written to both X fields
10F5: DD 36 0E 58     LD      (IX+$0E),$58        
10F9: DD 36 05 80     LD      (IX+$05),$80        ; its Y, written to both Y fields
10FD: DD 36 0F 80     LD      (IX+$0F),$80        
1101: DD 36 20 01     LD      (IX+$20),$01        ; the second fire, one record on
1105: DD 36 23 EB     LD      (IX+$23),$EB        ; its X, both fields
1109: DD 36 2E EB     LD      (IX+$2E),$EB        
110D: DD 36 25 60     LD      (IX+$25),$60        ; its Y, both fields
1111: DD 36 2F 60     LD      (IX+$2F),$60        
1115: 11 70 69        LD      DE,$6970            
1118: 21 21 11        LD      HL,$1121            ; a 16-byte table sitting just below
111B: 01 10 00        LD      BC,$0010            
111E: ED B0           LDIR                        ; into a slot in the sprite buffer
1120: C9              RET                         

; ---- $1121-$1130: data ----
1121: 37 45 0F 60 37 45 8F F7 77 45 0F 60 77 45 8F F7

; Build the 100m rivet board's object records and their hardware sprite mirror from fixed
; templates. Like its three sibling arms it takes no input -- every pointer, count and
; stride is a constant. It broadcasts a four-byte template into five object records at a
; 32-byte stride; seeds a sprite-object pair from a position table and emits their two
; sprite records; block-copies a twelve-byte table into objectCollisionSprites; scatters
; a position table into the X and Y fields of two further records and broadcasts a
; four-byte group into those two records' appearance fields; then marks both active and
; gathers each one's permuted fields into a hardware sprite record. Those two extra
; records are FIRES, which is why the rivet board's collision arm sweeps seven records
; where every spawn and service path still walks five.
seed100mBoardObjects:
1131: 21 F0 3D        LD      HL,$3DF0            ; a 4-byte group to broadcast
1134: 11 07 64        LD      DE,$6407            ; field +7 of the first fire record
1137: 01 1C 05        LD      BC,$051C            ; five records, 0x20 apart
113A: CD 2A 12        CALL    replicateGroupStrided; 
113D: 21 14 3E        LD      HL,$3E14            ; the pair's two start positions
1140: CD A6 11        CALL    seedSpriteObjectPair; 
1143: 21 54 3E        LD      HL,$3E54            ; a 12-byte collision-sprite template
1146: 11 0C 6A        LD      DE,$6A0C            
1149: 01 0C 00        LD      BC,$000C            
114C: ED B0           LDIR                        
114E: 21 82 11        LD      HL,$1182            ; two X/Y pairs from the table just below
1151: 11 A3 64        LD      DE,$64A3            ; into fields +3 and +5 of two records
1154: 01 1E 02        LD      BC,$021E            ; two records, 0x20 apart
1157: CD EC 11        CALL    copyBytePairsStrided; 
115A: 21 7E 11        LD      HL,$117E            ; their shared appearance group
115D: 11 A7 64        LD      DE,$64A7            ; field +7 of those same two records
1160: 01 1C 02        LD      BC,$021C            
1163: CD 2A 12        CALL    replicateGroupStrided; 
1166: DD 21 A0 64     LD      IX,$64A0            ; these two records are the rivet fires
116A: DD 36 00 01     LD      (IX+$00),$01        ; the first one, marked active
116E: DD 36 20 01     LD      (IX+$20),$01        ; the second, one record on
1172: 21 50 69        LD      HL,$6950            ; their two hardware sprite records
1175: 06 02           LD      B,$02               
1177: 11 20 00        LD      DE,$0020            
117A: CD D3 11        CALL    gatherSpriteRecords ; 
117D: C9              RET                         

; ---- $117E-$1185: data ----
117E: 3F 0C 08 08 73 50 8D 50

; Board setup for one 10-record object block, with no inputs of its own -- every pointer
; and count is a baked immediate. First it stamps a 4-byte template into the sprite-code
; field (+7) of the ten objArray65 records, each 0x10 apart, seeding every record with
; the common sprite code and attribute. Then it gathers the permuted fields +3/+7/+8/+5
; out of those same records into ten consecutive 4-byte hardware sprite records at
; actorSprites: X <- +3, code <- +7, attribute <- +8, Y <- +5. Only the record count is
; reloaded between the two. The name describes the MECHANISM; what the ten objects are
; is not established here.
seedObjectBlockSprites:
1186: 21 A2 11        LD      HL,$11A2            ; the 4-byte template just below
1189: 11 07 65        LD      DE,$6507            ; field +7 of the first of ten records
118C: 01 0C 0A        LD      BC,$0A0C            ; ten records, 0x10 apart
118F: CD 2A 12        CALL    replicateGroupStrided; 
1192: DD 21 00 65     LD      IX,$6500            ; read those same ten records back
1196: 21 80 69        LD      HL,$6980            ; ten consecutive sprite records
1199: 06 0A           LD      B,$0A               
119B: 11 10 00        LD      DE,$0010            
119E: CD D3 11        CALL    gatherSpriteRecords ; 
11A1: C9              RET                         

; ---- $11A2-$11A5: data ----
11A2: 3B 00 02 02

; Place a pair of sprite objects at two caller-given positions and emit their hardware
; sprite records. It scatters the caller's four-byte position table as [record 0 X,
; record 0 Y, record 1 X, record 1 Y], stamps the SAME 4-byte appearance template into
; both records so the two always look alike, marks both active, and gathers (X, code,
; attribute, Y) from each into two consecutive sprite-buffer records. Every count is a
; fixed two; only the position table varies, and the three call sites pass three
; adjacent tables. What the two objects ARE is not established -- the tables themselves
; were not decoded.
seedSpriteObjectPair:
11A6: 11 83 66        LD      DE,$6683            ; the caller's table fills X and Y here
11A9: 01 0E 02        LD      BC,$020E            ; two records, 0x10 apart
11AC: CD EC 11        CALL    copyBytePairsStrided; 
11AF: 21 08 3E        LD      HL,$3E08            ; one shared look for both objects
11B2: 11 87 66        LD      DE,$6687            ; their sprite-code field
11B5: 01 0C 02        LD      BC,$020C            
11B8: CD 2A 12        CALL    replicateGroupStrided; 
11BB: DD 21 80 66     LD      IX,$6680            ; the pair of object records
11BF: DD 36 00 01     LD      (IX+$00),$01        ; mark the first record active
11C3: DD 36 10 01     LD      (IX+$10),$01        ; and the second
11C7: 21 18 6A        LD      HL,$6A18            ; the two hardware sprite records
11CA: 06 02           LD      B,$02               
11CC: 11 10 00        LD      DE,$0010            
11CF: CD D3 11        CALL    gatherSpriteRecords ; 
11D2: C9              RET                         

; Build a run of hardware sprite records by gathering four permuted fields out of each
; object record. For each of a caller-supplied count of records -- base, base + stride,
; and so on -- it reads offsets +3, +7, +8, +5 IN THAT ORDER and stores them into four
; consecutive destination bytes, so the sprite record's X comes from the object's +3, its
; code from +7, its attribute from +8 and its Y from +5. A permuting gather, not a block
; copy: +4 and +6 are never read. The destination advances in its low byte only, so it
; wraps within its 256-byte page, and a count of zero means 256 records rather than none.
gatherSpriteRecords:
11D3: DD 7E 03        LD      A,(IX+$03)          ; the object's X
11D6: 77              LD      (HL),A              
11D7: 2C              INC     L                   
11D8: DD 7E 07        LD      A,(IX+$07)          ; its sprite code
11DB: 77              LD      (HL),A              
11DC: 2C              INC     L                   
11DD: DD 7E 08        LD      A,(IX+$08)          ; its colour attribute
11E0: 77              LD      (HL),A              
11E1: 2C              INC     L                   
11E2: DD 7E 05        LD      A,(IX+$05)          ; its Y, read last
11E5: 77              LD      (HL),A              
11E6: 2C              INC     L                   
11E7: DD 19           ADD     IX,DE               ; on to the next object record
11E9: 10 E8           DJNZ    gatherSpriteRecords ; 
11EB: C9              RET                         

; Scatter a run of consecutive source byte-pairs into strided destination records. Each
; pass reads two adjacent source bytes and stores them at offsets +0 and +2 of the
; current record; +1 is never written, the destination pointer stepping straight past it.
; The source advances cumulatively, two bytes a pass, as a full 16-bit walk that may
; cross a page; the destination advance touches the low byte only, so it wraps inside its
; own 256 bytes and can never carry out. A pass count of zero means 256 passes, not none.
; Its near-twin copies groups of four and re-reads the same group every pass, which makes
; that one a broadcast where this is a scatter.
copyBytePairsStrided:
11EC: 7E              LD      A,(HL)              ; the first byte of the pair
11ED: 12              LD      (DE),A              ; into the record's +0
11EE: 23              INC     HL                  
11EF: 1C              INC     E                   
11F0: 1C              INC     E                   ; step past +1, which is never written
11F1: 7E              LD      A,(HL)              
11F2: 12              LD      (DE),A              ; the second byte, into +2
11F3: 23              INC     HL                  
11F4: 7B              LD      A,E                 
11F5: 81              ADD     A,C                 ; the record gap, low byte only
11F6: 5F              LD      E,A                 
11F7: 10 F3           DJNZ    copyBytePairsStrided; 
11F9: C9              RET                         

loc_11fa:
11FA: DD 21 A0 66     LD      IX,$66A0            ; the record this fills
11FE: 11 28 6A        LD      DE,$6A28            ; and a 4-byte array in the sprite buffer
1201: DD 36 00 01     LD      (IX+$00),$01        ; mark it active
1205: 7E              LD      A,(HL)              
1206: DD 77 03        LD      (IX+$03),A          ; X
1209: 12              LD      (DE),A              ; mirrored into the array
120A: 1C              INC     E                   
120B: 23              INC     HL                  
120C: 7E              LD      A,(HL)              
120D: DD 77 07        LD      (IX+$07),A          ; sprite code
1210: 12              LD      (DE),A              
1211: 1C              INC     E                   
1212: 23              INC     HL                  
1213: 7E              LD      A,(HL)              
1214: DD 77 08        LD      (IX+$08),A          ; colour attribute
1217: 12              LD      (DE),A              
1218: 1C              INC     E                   
1219: 23              INC     HL                  
121A: 7E              LD      A,(HL)              
121B: DD 77 05        LD      (IX+$05),A          ; Y, written out of order
121E: 12              LD      (DE),A              
121F: 23              INC     HL                  
1220: 7E              LD      A,(HL)              
1221: DD 77 09        LD      (IX+$09),A          ; two more fields, not mirrored
1224: 23              INC     HL                  
1225: 7E              LD      A,(HL)              
1226: DD 77 0A        LD      (IX+$0A),A          
1229: C9              RET                         

; A struct-field initialiser, NOT a blitter: stamp ONE 4-byte source group into a run of
; strided destination records, seeding them all with a common header or value. The
; source is re-read from the same place on every pass and never advances, so the last
; pass copies exactly what the first did, and both it and the gap size survive the call
; unchanged -- which is what lets a caller run it again by reloading only the
; destination and the count. Destination addressing is 8-BIT and confined to one page:
; the low half of the pointer is what steps, so the offset WRAPS inside its own 256
; bytes rather than crossing pages. A near-identical routine copies PAIRS and lets its
; source advance cumulatively; the two look mergeable and are not.
replicateGroupStrided:
122A: E5              PUSH    HL                  ; the group is re-read on every pass
122B: C5              PUSH    BC                  
122C: 06 04           LD      B,$04               ; four bytes to a group

loc_122e:
122E: 7E              LD      A,(HL)              
122F: 12              LD      (DE),A              
1230: 23              INC     HL                  
1231: 1C              INC     E                   ; the destination steps its low byte only
1232: 10 FA           DJNZ    loc_122e            ; 
1234: C1              POP     BC                  
1235: E1              POP     HL                  ; restore the source: it never advances
1236: 7B              LD      A,E                 
1237: 81              ADD     A,C                 ; step over the gap to the next record
1238: 5F              LD      E,A                 
1239: 10 EF           DJNZ    replicateGroupStrided; 
123B: C9              RET                         

; Spawn Mario's actor record for the board. Gated on substateTimer, so the body fires
; only on the frame that countdown expires. The start position comes from board: on 75m
; X = 0x16 and Y = 0xE0, on every other board X = 0x3F and Y = 0xF0. It seeds his actor
; record (active flag, X, Y, sprite code 0x80, attribute 0x02, move-step timer 0x01) and
; its 4-byte sprite mirror, advances gameSubstate so the next vblank dispatches the
; following setup step, and enqueues the follow-up task [opcode 6, argument 1].
seedMarioActorRecord:
123C: DF              RST     $18                 ; act only when the countdown expires
123D: 3A 27 62        LD      A,(workRam+227)     ; 
1240: FE 03           CP      $03                 ; 75m starts Mario somewhere else
1242: 01 16 E0        LD      BC,$E016            ; the 75m start: X 0x16, Y 0xE0
1245: CA 4B 12        JP      Z,loc_124b          ; 
1248: 01 3F F0        LD      BC,$F03F            ; every other board: X 0x3F, Y 0xF0

loc_124b:
124B: DD 21 00 62     LD      IX,$6200            ; Mario's actor record
124F: 21 4C 69        LD      HL,$694C            ; and its hardware sprite record
1252: DD 36 00 01     LD      (IX+$00),$01        ; he is alive and being processed
1256: DD 71 03        LD      (IX+$03),C          ; his start X
1259: 71              LD      (HL),C              
125A: 2C              INC     L                   
125B: DD 36 07 80     LD      (IX+$07),$80        ; his standing sprite code
125F: 36 80           LD      (HL),$80            
1261: 2C              INC     L                   
1262: DD 36 08 02     LD      (IX+$08),$02        ; his colour attribute
1266: 36 02           LD      (HL),$02            
1268: 2C              INC     L                   
1269: DD 70 05        LD      (IX+$05),B          ; his start Y
126C: 70              LD      (HL),B              
126D: DD 36 0F 01     LD      (IX+$0F),$01        ; arm the move-step timer
1271: 21 0A 60        LD      HL,$600A            
1274: 34              INC     (HL)                ; on to the next setup step
1275: 11 01 06        LD      DE,$0601            ; the follow-up task: opcode 6, argument 1
1278: CD 9F 30        CALL    enqueueTask         ; 
127B: C9              RET                         

; The sub-state that plays Mario's death animation. It does exactly two things, back to
; back: service the effect-sprite state machine, routing effectState to its per-state
; handler, then vector deathAnimPhase through its jump table to the current phase --
; seed, step, or hand-off. One episode runs a fixed 296 frames. It sits in BOTH sub-state
; tables: in the attract table it is the death the demo ends on, in the credited-game
; table it is what gameplay hands over to on the frame Mario dies. It does not decrement
; the life count -- the next sub-state does. And it is named by effect, not by cause: the
; bonus-timer death reaches the same sequence with marioActive still set, entering past
; the alive test entirely.
runDeathAnimationSubstate:
127C: CD BD 1D        CALL    dispatchEffectState ; service the effect sprites first

; Vector Mario's death animation to its current-phase handler, through a four-entry table
; of little-endian targets indexed by deathAnimPhase. Phase 0 seeds it: rewrite
; marioSpriteCode to the first death tile keeping its old high bit, prime
; deathAnimTicksLeft to 13, step the phase, re-arm the frame gate to 8, clear the sprite
; runs and fire the death sound line. Phase 1 runs on each expiry of that 8-frame gate,
; ticking the counter down and stepping the sprite through four orientations; at 0 it
; settles the final tile, steps to phase 2 and re-arms the gate to 0x80. Phase 2 advances
; gameSubstate -- by 2 for player 2, 1 for player 1 -- handing off to the life-loss
; handler. Slot 3 is padding: the selector has three writers and none can produce 3. The
; index is doubled in 8 bits, so the address is base + (2*phase & 0xff).
dispatchDeathAnimationPhase:
127F: 3A 9D 63        LD      A,(workRam+39D)     ; which phase of the death animation
1282: EF              RST     $28                 ; vector through the table just below

; ---- $1283-$128A: jump table ----
1283: 8B 12 AC 12 DE 12 00 00

; The seed arm of Mario's death animation, gated so it acts only on the frame
; substateTimer expires. It rewrites marioSpriteCode to the fixed tile 0x78 while
; preserving bit 7, his facing -- the first of the four orientations the next arm
; cycles. It advances deathAnimPhase 0 -> 1, primes deathAnimTicksLeft to 13 and re-arms
; substateTimer to 8, the tick cadence the next arm runs on; clears four disjoint runs
; of sprite records; and fires the death sound by holding sndIrqTrigger for three
; frames. "Begin" names the START of the animation, not the cause of the death: nothing
; here detects or decides a kill and the life is not taken -- that happens in the
; following sub-state. The animation is not conditional on marioActive being clear; the
; bonus-expiry death reaches the same sequence with it set.
beginMarioDeathAnimation:
128B: DF              RST     $18                 ; act only when the countdown expires
128C: 21 4D 69        LD      HL,$694D            ; Mario's sprite-code byte
128F: 3E F0           LD      A,$F0               ; the first death tile, pre-shifted
1291: CB 16           RL      (HL)                ; carry out his old facing bit
1293: 1F              RRA                         ; shift it back in: tile 0x78, facing kept
1294: 77              LD      (HL),A              
1295: 21 9D 63        LD      HL,$639D            
1298: 34              INC     (HL)                ; on to the cycling phase
1299: 3E 0D           LD      A,$0D               
129B: 32 9E 63        LD      (workRam+39E),A     ; thirteen ticks to run
129E: 3E 08           LD      A,$08               
12A0: 32 09 60        LD      (workRam+9),A       ; the eight-frame tick cadence
12A3: CD BD 30        CALL    clearSpriteColumns  ; 
12A6: 3E 03           LD      A,$03               
12A8: 32 88 60        LD      (workRam+88),A      ; fire the death sound, held three frames
12AB: C9              RET                         

; The cycling arm of Mario's death animation. It is gated by substateTimer, which
; reloads to 8 on each expiry -- a fixed eight-frame tick. On each tick it takes one off
; deathAnimTicksLeft and steps his sprite: bit 0 of marioSpriteCode flips every tick, so
; the tile alternates between two codes, and on the tick that bit falls back to 0 it
; also flips bit 7 of marioSpriteCode and bit 7 of marioSpriteAttr together. Those two
; are the vertical and horizontal mirror flags, so flipping both is a 180-degree
; ROTATION. When the count reaches zero it instead writes a fixed settle tile,
; preserving the old mirror bit, bumps deathAnimPhase, and reloads the gate long -- 128
; ticks -- so the next arm waits. Primed at 13 ticks, the record takes four (code,
; attribute) pairs cycled three times. This arm does not end the life; the following
; sub-state does.
stepMarioDeathAnimation:
12AC: DF              RST     $18                 ; act only on the frames the gate expires
12AD: 3E 08           LD      A,$08               
12AF: 32 09 60        LD      (workRam+9),A       ; re-arm the eight-frame tick
12B2: 21 9E 63        LD      HL,$639E            
12B5: 35              DEC     (HL)                ; one tick off the thirteen
12B6: CA CB 12        JP      Z,loc_12cb          ; the cycle is over: settle him
12B9: 21 4D 69        LD      HL,$694D            ; Mario's sprite-code byte
12BC: 7E              LD      A,(HL)              
12BD: 1F              RRA                         ; carry out its old bit 0
12BE: 3E 02           LD      A,$02               
12C0: 1F              RRA                         ; a mask: bit 0 always, bit 7 if it was set
12C1: 47              LD      B,A                 
12C2: AE              XOR     (HL)                ; flip the tile, and the mirror every other
12C3: 77              LD      (HL),A              
12C4: 2C              INC     L                   ; on to the attribute byte
12C5: 78              LD      A,B                 
12C6: E6 80           AND     $80                 
12C8: AE              XOR     (HL)                ; flip its mirror bit on the same tick
12C9: 77              LD      (HL),A              
12CA: C9              RET                         

loc_12cb:
12CB: 21 4D 69        LD      HL,$694D            
12CE: 3E F4           LD      A,$F4               ; the settle tile, pre-shifted
12D0: CB 16           RL      (HL)                ; carry out his old mirror bit
12D2: 1F              RRA                         ; shift it back in: tile 0x7A, mirror kept
12D3: 77              LD      (HL),A              
12D4: 21 9D 63        LD      HL,$639D            
12D7: 34              INC     (HL)                ; on to the hand-off phase
12D8: 3E 80           LD      A,$80               
12DA: 32 09 60        LD      (workRam+9),A       ; a long pause before the hand-off
12DD: C9              RET                         

loc_12de:
12DE: DF              RST     $18                 ; wait out that pause
12DF: CD DB 30        CALL    loc_30db            ; clear the sprite scratch this state used

loc_12e2:
12E2: 21 0A 60        LD      HL,$600A            
12E5: 3A 0E 60        LD      A,(workRam+E)       ; which player just died
12E8: A7              AND     A                   
12E9: CA ED 12        JP      Z,loc_12ed          ; 
12EC: 34              INC     (HL)                ; player 2 skips on to his own handler

loc_12ed:
12ED: 34              INC     (HL)                ; on to the life-loss handler
12EE: 2B              DEC     HL                  
12EF: 36 01           LD      (HL),$01            ; so it runs on the very next frame
12F1: C9              RET                         

; Spend one of player 1's lives and decide what happens next, in a fixed order: silence
; every sound; clear playIntro, so the next life starts without the opening cutscene;
; take one off lives; then copy the live 8-byte player context into p1Context, which the
; next turn restores from. The two clears happen BEFORE the copy, so the saved slot
; carries the reduced life count and the cleared intro flag. Then it branches. Lives
; remaining go to the between-turns interlude -- a different sub-state in a one-player
; game than in a two-player one, where it is the player-alternation screen. None
; remaining means player 1 is finished: format the final score for display and ranking,
; stamp the game-over banner across the screen, queue the render task, and arm a
; 0xC0-frame wait before the game-over sub-state.
losePlayer1Life:
12F2: CD 1C 01        CALL    silenceSound        ; everything goes quiet as the life ends
12F5: AF              XOR     A                   
12F6: 32 2C 62        LD      (workRam+22C),A     ; the next life skips the opening cutscene
12F9: 21 28 62        LD      HL,$6228            ; the life count, first of the context
12FC: 35              DEC     (HL)                ; spend one life
12FD: 7E              LD      A,(HL)              ; how many are left
12FE: 11 40 60        LD      DE,$6040            ; player 1's save slot
1301: 01 08 00        LD      BC,$0008            
1304: ED B0           LDIR                        ; saved with the life already gone
1306: A7              AND     A                   
1307: C2 34 13        JP      NZ,loc_1334         ; lives remain: back for another turn
130A: 3E 01           LD      A,$01               ; player 1's slot for the ranking
130C: 21 B2 60        LD      HL,$60B2            ; his final score
130F: CD CA 13        CALL    loc_13ca            ; format it and rank it in the table
1312: 21 D4 76        LD      HL,$76D4            ; where the game-over banner starts
1315: 3A 0F 60        LD      A,(workRam+F)       ; 
1318: A7              AND     A                   
1319: 28 07           JR      Z,loc_1322          ; 
131B: 11 02 03        LD      DE,$0302            ; a two-player game queues an extra render
131E: CD 9F 30        CALL    enqueueTask         ; 
1321: 2B              DEC     HL                  ; and starts the banner one column left

loc_1322:
1322: CD 26 18        CALL    fillTileBlock       ; stamp the banner across the screen
1325: 11 00 03        LD      DE,$0300            ; queue the game-over render task
1328: CD 9F 30        CALL    enqueueTask         ; 
132B: 21 09 60        LD      HL,$6009            
132E: 36 C0           LD      (HL),$C0            ; hold it for 192 frames
1330: 23              INC     HL                  
1331: 36 10           LD      (HL),$10            ; then run the game-over sub-state
1333: C9              RET                         

loc_1334:
1334: 0E 08           LD      C,$08               ; one player: the between-turns interlude
1336: 3A 0F 60        LD      A,(workRam+F)       ; 
1339: A7              AND     A                   
133A: CA 3F 13        JP      Z,loc_133f          ; 
133D: 0E 17           LD      C,$17               ; two players: the alternation screen

loc_133f:
133F: 79              LD      A,C                 
1340: 32 0A 60        LD      (workRam+A),A       ; that is the next sub-state
1343: C9              RET                         

loc_1344:
1344: CD 1C 01        CALL    silenceSound        ; everything goes quiet as the life ends
1347: AF              XOR     A                   
1348: 32 2C 62        LD      (workRam+22C),A     ; the next life skips the opening cutscene
134B: 21 28 62        LD      HL,$6228            ; the life count, first of the context
134E: 35              DEC     (HL)                ; spend one life; unguarded, so 0 wraps
134F: 7E              LD      A,(HL)              ; how many are left
1350: 11 48 60        LD      DE,$6048            ; player 2's save slot
1353: 01 08 00        LD      BC,$0008            
1356: ED B0           LDIR                        ; saved with the life already gone
1358: A7              AND     A                   
1359: C2 7F 13        JP      NZ,loc_137f         ; lives remain: back for another turn
135C: 3E 03           LD      A,$03               ; player 2's slot for the ranking
135E: 21 B5 60        LD      HL,$60B5            ; his final score
1361: CD CA 13        CALL    loc_13ca            ; format it and rank it in the table
1364: 11 03 03        LD      DE,$0303            ; two game-over render tasks
1367: CD 9F 30        CALL    enqueueTask         ; 
136A: 11 00 03        LD      DE,$0300            
136D: CD 9F 30        CALL    enqueueTask         ; 
1370: 21 D3 76        LD      HL,$76D3            ; where the game-over banner starts
1373: CD 26 18        CALL    fillTileBlock       ; stamp it across the screen
1376: 21 09 60        LD      HL,$6009            
1379: 36 C0           LD      (HL),$C0            ; hold it for 192 frames
137B: 23              INC     HL                  
137C: 36 11           LD      (HL),$11            ; then run the game-over sub-state
137E: C9              RET                         

loc_137f:
137F: 0E 17           LD      C,$17               ; the player-alternation screen
1381: 3A 40 60        LD      A,(workRam+40)      ; player 1's saved life count
1384: A7              AND     A                   
1385: C2 8A 13        JP      NZ,loc_138a         ; 
1388: 0E 08           LD      C,$08               ; player 1 is out too: the plain interlude

loc_138a:
138A: 79              LD      A,C                 
138B: 32 0A 60        LD      (workRam+A),A       ; that is the next sub-state
138E: C9              RET                         

loc_138f:
138F: DF              RST     $18                 ; hold this sub-state until the frame timer expires
1390: 0E 17           LD      C,$17               ; the sub-state to run if player 2 still has a game
1392: 3A 48 60        LD      A,(workRam+48)      ; read player 2's saved life count

loc_1395:
1395: 34              INC     (HL)                ; re-arm the just-expired timer to one frame
1396: A7              AND     A                   
1397: C2 9C 13        JP      NZ,loc_139c         ; a game still in progress keeps the first choice
139A: 0E 14           LD      C,$14               ; none left, so take the other sub-state

loc_139c:
139C: 79              LD      A,C                 
139D: 32 0A 60        LD      (workRam+A),A       ; publish the chosen follow-on sub-state
13A0: C9              RET                         

loc_13a1:
13A1: DF              RST     $18                 ; hold this sub-state until the frame timer expires
13A2: 0E 17           LD      C,$17               ; the sub-state to run if player 1 still has a game
13A4: 3A 40 60        LD      A,(workRam+40)      ; read player 1's saved life count
13A7: C3 95 13        JP      loc_1395            ; share the twin handler's re-arm and choice

loc_13aa:
13AA: 3A 26 60        LD      A,(workRam+26)      ; read the cabinet orientation switch
13AD: 32 82 7D        LD      (flipScreen),A      ; mirror it into the flip-screen latch
13B0: AF              XOR     A                   
13B1: 32 0A 60        LD      (workRam+A),A       ; restart the in-state sub-sequence
13B4: 21 01 01        LD      HL,$0101            ; 1 into both halves of the player-index pair
13B7: 22 0D 60        LD      (workRam+D),HL      ; currentPlayer and activePlayerIndex together
13BA: C9              RET                         

; Reset the live player/display context to player 1. Four fixed stores and no reads:
; currentPlayer = 0 and activePlayerIndex = 0, so player 1 is up -- that second byte is
; the low half of a 16-bit pair and doubles as the "one-player start" marker a later
; reader tests for zero, and it is NOT the two-player flag, which is the pair's high byte
; and is left untouched; gameSubstate = 0, restarting the in-state sub-sequence; and the
; flip-screen latch = 1, upright, since player 1 never sees the cocktail mirror. The
; player-2 counterpart writes 1 into the two player bytes and takes the flip latch from
; the cabinet orientation switch.
selectPlayer1Context:
13BB: AF              XOR     A                   
13BC: 32 0D 60        LD      (workRam+D),A       ; player 1 is up
13BF: 32 0E 60        LD      (workRam+E),A       ; player 1 is the active player
13C2: 32 0A 60        LD      (workRam+A),A       ; restart the in-game sub-state sequence
13C5: 3C              INC     A                   
13C6: 32 82 7D        LD      (flipScreen),A      ; upright: player 1 never sees the mirror
13C9: C9              RET                         

loc_13ca:
13CA: 11 C6 61        LD      DE,$61C6            ; the score staging area
13CD: 12              LD      (DE),A              ; stash the caller's player tag
13CE: CF              RST     $08                 ; in attract, stop here
13CF: 13              INC     DE                  
13D0: 01 03 00        LD      BC,$0003            
13D3: ED B0           LDIR                        ; copy in the player's 3 packed-BCD score bytes
13D5: 06 03           LD      B,$03               ; three bytes to unpack
13D7: 21 B1 61        LD      HL,$61B1            ; where the six display digits go

loc_13da:
13DA: 1B              DEC     DE                  ; walk the score back to front, top pair first
13DB: 1A              LD      A,(DE)              
13DC: 0F              RRCA                        ; rotate the high digit down
13DD: 0F              RRCA                        
13DE: 0F              RRCA                        
13DF: 0F              RRCA                        
13E0: E6 0F           AND     $0F                 ; isolate the high digit
13E2: 77              LD      (HL),A              ; write it as the first display digit
13E3: 23              INC     HL                  
13E4: 1A              LD      A,(DE)              
13E5: E6 0F           AND     $0F                 ; isolate the low digit
13E7: 77              LD      (HL),A              ; write it as the next display digit
13E8: 23              INC     HL                  
13E9: 10 EF           DJNZ    loc_13da            ; on to the next score byte
13EB: 06 0E           LD      B,$0E               ; fourteen blanks pad the field out

loc_13ed:
13ED: 36 10           LD      (HL),$10            ; a blank tile
13EF: 23              INC     HL                  
13F0: 10 FB           DJNZ    loc_13ed            ; 
13F2: 36 3F           LD      (HL),$3F            ; terminator closing the 21-byte field
13F4: 06 05           LD      B,$05               ; at most five records to bubble past
13F6: 21 A5 61        LD      HL,$61A5            ; the key of the record above
13F9: 11 C7 61        LD      DE,$61C7            ; the new record's own key

loc_13fc:
13FC: 1A              LD      A,(DE)              
13FD: 96              SUB     (HL)                ; compare the two 3-byte keys, low byte first
13FE: 23              INC     HL                  
13FF: 13              INC     DE                  
1400: 1A              LD      A,(DE)              
1401: 9E              SBC     A,(HL)              ; middle byte of the same compare
1402: 23              INC     HL                  
1403: 13              INC     DE                  
1404: 1A              LD      A,(DE)              
1405: 9E              SBC     A,(HL)              ; top byte of the same compare
1406: D8              RET     C                   ; the new key is smaller: its slot is found
1407: C5              PUSH    BC                  
1408: 06 19           LD      B,$19               ; 25 bytes, one whole record

loc_140a:
140A: 4E              LD      C,(HL)              
140B: 1A              LD      A,(DE)              
140C: 77              LD      (HL),A              ; exchange the two records byte by byte
140D: 79              LD      A,C                 
140E: 12              LD      (DE),A              
140F: 2B              DEC     HL                  
1410: 1B              DEC     DE                  
1411: 10 F7           DJNZ    loc_140a            ; 
1413: 01 F5 FF        LD      BC,$FFF5            ; step both keys back to the next-higher pair
1416: 09              ADD     HL,BC               
1417: EB              EX      DE,HL               
1418: 09              ADD     HL,BC               
1419: EB              EX      DE,HL               
141A: C1              POP     BC                  
141B: 10 DF           DJNZ    loc_13fc            ; try the record above this one
141D: C9              RET                         

; The game-over sub-state handler. Each frame it redraws the "CREDIT nn" line and counts
; substateTimer down, doing nothing else while that is still running. When it expires
; the screen is blanked, currentPlayer and activePlayerIndex are both cleared, and the
; five playerSlotRecords (stride 0x22) are scanned to decide what comes up next: a
; record holding 1 means player 1 is still up, so compose player 1's screen with the
; flip key set; else a record holding 3 means player 2 is still up, so select player 2
; and compose with flip key 0; else neither is left in play and the machine goes back to
; attract. The scan stops at the FIRST match and which of the five slots matched is
; never read. What the record values 1 and 3 mean in themselves is read back from the
; three outcomes, not established on its own.
selectPlayerScreenOrAttract:
141E: CD 16 06        CALL    drawCreditDisplay   ; repaint the CREDIT line
1421: DF              RST     $18                 ; hold the game-over screen until it expires
1422: CD 74 08        CALL    clearPlayfieldAndSprites; wipe the screen before composing
1425: 3E 00           LD      A,$00               
1427: 32 0E 60        LD      (workRam+E),A       ; clear both bytes of the player index
142A: 32 0D 60        LD      (workRam+D),A       ; 
142D: 21 1C 61        LD      HL,$611C            ; base of the five player slot records
1430: 11 22 00        LD      DE,$0022            ; record stride
1433: 06 05           LD      B,$05               ; five records to scan
1435: 3E 01           LD      A,$01               ; look for a record still marked player 1

loc_1437:
1437: BE              CP      (HL)                
1438: CA 59 14        JP      Z,configureFlipScreenAndComposeScreen; player 1 is still up
143B: 19              ADD     HL,DE               
143C: 10 F9           DJNZ    loc_1437            ; on to the next record
143E: 21 1C 61        LD      HL,$611C            ; rescan from the first record
1441: 06 05           LD      B,$05               
1443: 3E 03           LD      A,$03               ; now look for one still marked player 2

loc_1445:
1445: BE              CP      (HL)                
1446: CA 4F 14        JP      Z,selectPlayer2AndComposeScreen; player 2 is still up
1449: 19              ADD     HL,DE               
144A: 10 F9           DJNZ    loc_1445            ; 
144C: C3 75 14        JP      enterAttractMode    ; neither player is left in play

; The player-2 arm of the game-over scan over the player slot records. Its own
; contribution is exactly "select player 2": write 1 to both currentPlayer and
; activePlayerIndex, which is what makes player 2 the player up -- score awards and the
; per-player context slot key off it. It then delegates to the shared compose tail with
; the key 0. The tail ORs that key into the flip-screen latch, so 0 lets the cabinet
; orientation switch alone decide flip, where the player-1 arm forces flip on with a key
; of 1. The tail then clears substateTimer, advances gameSubstate, and posts the twelve
; screen-draw tasks.
selectPlayer2AndComposeScreen:
144F: 3E 01           LD      A,$01               
1451: 32 0E 60        LD      (workRam+E),A       ; player 2 is the active player
1454: 32 0D 60        LD      (workRam+D),A       ; player 2 is up; scoring keys off this
1457: 3E 00           LD      A,$00               ; compose with key 0, letting the cabinet decide

; Orient the display for the player who is up, then compose their screen. It is entered
; with a player key -- 1 for the player whose screen is the flipped one, 0 for the other
; -- and writes that key OR'd with dipUpright to the flip-screen latch. So the key alone
; can force the flip on, and an upright cabinet forces it on regardless; only key 0 on a
; cocktail leaves it off, which is the mechanism that mirrors the screen for the second
; player of a cocktail game. It then clears substateTimer (the "wait zero" form, so the
; new sub-state proceeds next frame), advances gameSubstate by one, and posts twelve
; screen-text messages onto the task ring with the argument counting up across a fixed
; range. Which string each argument selects is not established here.
configureFlipScreenAndComposeScreen:
1459: 21 26 60        LD      HL,$6026            ; the cabinet orientation switch
145C: B6              OR      (HL)                ; an upright cabinet forces the flip on too
145D: 32 82 7D        LD      (flipScreen),A      ; orient the display for the player up
1460: 3E 00           LD      A,$00               
1462: 32 09 60        LD      (workRam+9),A       ; no wait: the new sub-state runs next frame
1465: 21 0A 60        LD      HL,$600A            
1468: 34              INC     (HL)                ; advance the sub-state selector
1469: 11 0D 03        LD      DE,$030D            ; screen-text task, first of twelve arguments
146C: 06 0C           LD      B,$0C               ; twelve draw messages to post

loc_146e:
146E: CD 9F 30        CALL    enqueueTask         ; post one screen-text message
1471: 13              INC     DE                  ; next argument in the range
1472: 10 FA           DJNZ    loc_146e            ; 
1474: C9              RET                         

; Put the cabinet back into its idle attract demo. It is the tail of the player-record
; search: that scan looks over the player slot records for one still marked as a game in
; play, and when neither is found it falls through here for four unconditional stores --
; the flip-screen latch := 1, gameState := 1 so the next vblank dispatches the attract
; handler, attract := 1, and gameSubstate := 0. It reads nothing at all, so its effect is
; the same whatever state it is entered from; it is the same attract-entry write set the
; power-on path issues.
enterAttractMode:
1475: 3E 01           LD      A,$01               
1477: 32 82 7D        LD      (flipScreen),A      ; attract display orientation
147A: 32 05 60        LD      (workRam+5),A       ; state 1 is attract, dispatched next vblank
147D: 32 07 60        LD      (workRam+7),A       ; no credited game in progress
1480: 3E 00           LD      A,$00               
1482: 32 0A 60        LD      (workRam+A),A       ; clear the sub-dispatch index
1485: C9              RET                         

; The per-frame handler of a sub-state given over to an on-screen item: its position
; walk, its animated sprite, and a point value counting down beside it. substateTimer is
; re-used here as a three-way MODE LATCH, not a countdown. At 0 it does one-shot setup
; -- clear paletteBank0 and paletteBank1, mark running, seed the item's state block
; (value 30, the timers, the position index), point a video cursor at the top of the
; value's column, find the item's row in playerSlotRecords keyed on
; 2*activePlayerIndex+1, and draw it -- then falls through. Running, each frame: tick
; the display timer and on its wrap tick the value and stamp its two digits, a value of
; 0 exiting; step the position from p1Input, bit 7 held running a video-column walk and
; bit 7 clear a frame-divided move through the 0..0x1D wrap; then animate. Whether the
; item is the prize a player collects is NOT settled here.
runBonusItemValueDisplay:
1486: CD 16 06        CALL    drawCreditDisplay   ; repaint the CREDIT line
1489: 21 09 60        LD      HL,$6009            
148C: 7E              LD      A,(HL)              
148D: A7              AND     A                   ; a three-way mode latch here, not a countdown
148E: C2 DC 14        JP      NZ,loc_14dc         ; already running: skip the one-shot setup
1491: 32 86 7D        LD      (paletteBank0),A    ; clear both palette-bank latches
1494: 32 87 7D        LD      (paletteBank1),A    ; 
1497: 36 01           LD      (HL),$01            ; mark the display running
1499: 21 30 60        LD      HL,$6030            ; seed the item's state block
149C: 36 0A           LD      (HL),$0A            ; frame divider between position steps
149E: 23              INC     HL                  
149F: 36 00           LD      (HL),$00            ; digit-source toggle
14A1: 23              INC     HL                  
14A2: 36 10           LD      (HL),$10            ; frames to the next sprite animate
14A4: 23              INC     HL                  
14A5: 36 1E           LD      (HL),$1E            ; the point value starts at thirty
14A7: 23              INC     HL                  
14A8: 36 3E           LD      (HL),$3E            ; frames between value ticks
14AA: 23              INC     HL                  
14AB: 36 00           LD      (HL),$00            ; grid position index starts at zero
14AD: 21 E8 75        LD      HL,$75E8            ; top of the value's video column
14B0: 22 36 60        LD      (workRam+36),HL     ; park the column cursor there
14B3: 21 1C 61        LD      HL,$611C            ; base of the player slot records
14B6: 3A 0E 60        LD      A,(workRam+E)       ; 
14B9: 07              RLCA                        
14BA: 3C              INC     A                   ; search key is twice the player index plus one
14BB: 4F              LD      C,A                 
14BC: 11 22 00        LD      DE,$0022            ; record stride
14BF: 06 04           LD      B,$04               ; four rows to scan

loc_14c1:
14C1: 7E              LD      A,(HL)              
14C2: B9              CP      C                   
14C3: CA C9 14        JP      Z,loc_14c9          ; found the item's row
14C6: 19              ADD     HL,DE               
14C7: 10 F8           DJNZ    loc_14c1            ; next row; an unmatched key stops at the last

loc_14c9:
14C9: 22 38 60        LD      (workRam+38),HL     ; remember the item's slot row
14CC: 11 F3 FF        LD      DE,$FFF3            ; thirteen bytes back
14CF: 19              ADD     HL,DE               
14D0: 22 3A 60        LD      (workRam+3A),HL     ; where the exit copy will land
14D3: 06 00           LD      B,$00               
14D5: 3A 35 60        LD      A,(workRam+35)      ; the item's grid position
14D8: 4F              LD      C,A                 
14D9: CD FA 15        CALL    positionBonusItemSprite; draw the item at its cell

loc_14dc:
14DC: 21 34 60        LD      HL,$6034            ; the value's display timer
14DF: 35              DEC     (HL)                
14E0: C2 FC 14        JP      NZ,loc_14fc         ; not time to tick the value yet
14E3: 36 3E           LD      (HL),$3E            ; reload the display timer
14E5: 2B              DEC     HL                  ; the point value sits just below
14E6: 35              DEC     (HL)                ; count the value down one
14E7: CA C6 15        JP      Z,loc_15c6          ; value exhausted: tear the item down
14EA: 7E              LD      A,(HL)              
14EB: 06 FF           LD      B,$FF               

loc_14ed:
14ED: 04              INC     B                   
14EE: D6 0A           SUB     $0A                 ; split the value into tens and ones
14F0: D2 ED 14        JP      NC,loc_14ed         ; 
14F3: C6 0A           ADD     A,$0A               ; recover the ones digit
14F5: 32 52 75        LD      (videoRam+152),A    ; stamp the ones digit on screen
14F8: 78              LD      A,B                 
14F9: 32 72 75        LD      (videoRam+172),A    ; stamp the tens digit on screen

loc_14fc:
14FC: 21 30 60        LD      HL,$6030            ; the position-step divider
14FF: 46              LD      B,(HL)              ; keep this frame's count
1500: 36 0A           LD      (HL),$0A            ; reload the divider
1502: 3A 10 60        LD      A,(workRam+10)      ; the item's walk is driven by the control word
1505: CB 7F           BIT     7,A                 
1507: C2 46 15        JP      NZ,loc_1546         ; bit 7 runs the video-column walk instead
150A: E6 03           AND     $03                 ; is any direction held?
150C: C2 14 15        JP      NZ,loc_1514         ; 
150F: 3C              INC     A                   ; idle: reset the divider to one
1510: 77              LD      (HL),A              
1511: C3 8A 15        JP      loc_158a            ; 

loc_1514:
1514: 05              DEC     B                   ; count the divider down
1515: CA 1D 15        JP      Z,loc_151d          ; expired, so the item moves this frame
1518: 78              LD      A,B                 
1519: 77              LD      (HL),A              ; still counting; no move this frame
151A: C3 8A 15        JP      loc_158a            ; 

loc_151d:
151D: CB 4F           BIT     1,A                 ; bit 1 steps the position backwards
151F: C2 39 15        JP      NZ,loc_1539         ; 
1522: 3A 35 60        LD      A,(workRam+35)      ; the current grid position
1525: 3C              INC     A                   ; step forward one cell
1526: FE 1E           CP      $1E                 ; past the last cell?
1528: C2 2D 15        JP      NZ,loc_152d         ; 
152B: 3E 00           LD      A,$00               ; wrap round to the first

loc_152d:
152D: 32 35 60        LD      (workRam+35),A      ; publish the new position
1530: 4F              LD      C,A                 
1531: 06 00           LD      B,$00               
1533: CD FA 15        CALL    positionBonusItemSprite; redraw the item at the new cell
1536: C3 8A 15        JP      loc_158a            ; 

loc_1539:
1539: 3A 35 60        LD      A,(workRam+35)      ; 
153C: D6 01           SUB     $01                 ; step back one cell
153E: F2 2D 15        JP      P,loc_152d          ; still in range
1541: 3E 1D           LD      A,$1D               ; underflowed: wrap to the last cell
1543: C3 2D 15        JP      loc_152d            ; 

loc_1546:
1546: 3A 35 60        LD      A,(workRam+35)      ; the column walk keys off the same position
1549: FE 1C           CP      $1C                 ; at 0x1c the cursor advances a column
154B: CA 6D 15        JP      Z,loc_156d          ; 
154E: FE 1D           CP      $1D                 
1550: CA C6 15        JP      Z,loc_15c6          ; the walk has run out: tear the item down
1553: 2A 36 60        LD      HL,(workRam+36)     ; the column cursor
1556: 01 88 75        LD      BC,$7588            ; the column floor
1559: A7              AND     A                   
155A: ED 42           SBC     HL,BC               
155C: CA 8A 15        JP      Z,loc_158a          ; already at the floor: stamp nothing
155F: 09              ADD     HL,BC               ; restore the cursor
1560: C6 11           ADD     A,$11               ; turn the position into its glyph
1562: 77              LD      (HL),A              ; stamp it into the column
1563: 01 E0 FF        LD      BC,$FFE0            ; one tilemap row
1566: 09              ADD     HL,BC               ; retreat the cursor a column

loc_1567:
1567: 22 36 60        LD      (workRam+36),HL     ; store the cursor back
156A: C3 8A 15        JP      loc_158a            ; 

loc_156d:
156D: 2A 36 60        LD      HL,(workRam+36)     ; 
1570: 01 20 00        LD      BC,$0020            ; one tilemap row
1573: 09              ADD     HL,BC               ; advance the cursor a column
1574: A7              AND     A                   
1575: 01 08 76        LD      BC,$7608            ; the column ceiling
1578: ED 42           SBC     HL,BC               
157A: C2 86 15        JP      NZ,loc_1586         ; 
157D: 21 E8 75        LD      HL,$75E8            ; past it: wrap to the top of the column

loc_1580:
1580: 3E 10           LD      A,$10               ; a blank tile
1582: 77              LD      (HL),A              ; clear the cell the cursor now sits on
1583: C3 67 15        JP      loc_1567            ; 

loc_1586:
1586: 09              ADD     HL,BC               ; restore the advanced cursor
1587: C3 80 15        JP      loc_1580            ; 

loc_158a:
158A: 21 32 60        LD      HL,$6032            ; the sprite animation timer
158D: 35              DEC     (HL)                
158E: C2 F9 15        JP      NZ,loc_15f9         ; nothing to animate yet
1591: 3A 31 60        LD      A,(workRam+31)      ; which digit source was used last time
1594: A7              AND     A                   
1595: C2 B8 15        JP      NZ,loc_15b8         ; 
1598: 3E 01           LD      A,$01               
159A: 32 31 60        LD      (workRam+31),A      ; flip the source to the canned template
159D: 11 BF 01        LD      DE,$01BF            ; the canned digit template

loc_15a0:
15A0: FD 2A 38 60     LD      IY,(workRam+38)     ; the item's slot row
15A4: FD 6E 04        LD      L,(IY+$04)          ; the destination cell that row holds
15A7: FD 66 05        LD      H,(IY+$05)          
15AA: E5              PUSH    HL                  
15AB: DD E1           POP     IX                  
15AD: CD 7C 05        CALL    renderBcdColumn     ; paint the six value digits up the column
15B0: 3E 10           LD      A,$10               
15B2: 32 32 60        LD      (workRam+32),A      ; reload the animation timer
15B5: C3 F9 15        JP      loc_15f9            ; 

loc_15b8:
15B8: AF              XOR     A                   
15B9: 32 31 60        LD      (workRam+31),A      ; flip the source back to the slot record
15BC: ED 5B 38 60     LD      DE,(workRam+38)     ; 
15C0: 13              INC     DE                  ; the digits start three bytes into it
15C1: 13              INC     DE                  
15C2: 13              INC     DE                  
15C3: C3 A0 15        JP      loc_15a0            ; 

loc_15c6:
15C6: ED 5B 38 60     LD      DE,(workRam+38)     ; the item's slot row
15CA: AF              XOR     A                   
15CB: 12              LD      (DE),A              ; clear the item's slot
15CC: 21 09 60        LD      HL,$6009            
15CF: 36 80           LD      (HL),$80            ; mark the display done
15D1: 23              INC     HL                  
15D2: 35              DEC     (HL)                ; step the sub-state back one, ending the phase
15D3: 06 0C           LD      B,$0C               ; twelve cells to copy
15D5: 21 E8 75        LD      HL,$75E8            ; from the top of the value's column
15D8: FD 2A 3A 60     LD      IY,(workRam+3A)     ; into the item's slot record
15DC: 11 E0 FF        LD      DE,$FFE0            ; walking one tilemap row up each time

loc_15df:
15DF: 7E              LD      A,(HL)              
15E0: FD 77 00        LD      (IY+$00),A          
15E3: FD 23           INC     IY                  
15E5: 19              ADD     HL,DE               
15E6: 10 F7           DJNZ    loc_15df            ; 
15E8: 06 05           LD      B,$05               ; five follow-up tasks
15EA: 11 14 03        LD      DE,$0314            ; first task argument

loc_15ed:
15ED: CD 9F 30        CALL    enqueueTask         ; post one
15F0: 13              INC     DE                  
15F1: 10 FA           DJNZ    loc_15ed            ; 
15F3: 11 1A 03        LD      DE,$031A            ; and one more task after them
15F6: CD 9F 30        CALL    enqueueTask         ; 

loc_15f9:
15F9: C9              RET                         

; Redraw the on-board prize sprite at its current grid cell -- once at spawn, then after
; every position step. The item's position index runs 0..0x1d over a stored grid of
; thirty two-byte (X, Y) entries laid out as 10 columns by 3 rows, X stepping 0x10
; across a row and Y taking 0x5C / 0x6C / 0x7C down them. The indexed pair is stamped
; into sprite record 29: +0 X and +3 Y from the table, +1 the fixed glyph 0x72, +2 the
; fixed attribute 0x0C. The vblank DMA carries that record to sprite RAM.
positionBonusItemSprite:
15FA: D5              PUSH    DE                  
15FB: E5              PUSH    HL                  
15FC: CB 21           SLA     C                   ; two bytes per grid entry
15FE: 21 0F 36        LD      HL,$360F            ; the stored 30-cell position grid
1601: 09              ADD     HL,BC               ; index the entry for this cell
1602: EB              EX      DE,HL               
1603: 21 74 69        LD      HL,$6974            ; the item's sprite record
1606: 1A              LD      A,(DE)              
1607: 13              INC     DE                  
1608: 77              LD      (HL),A              ; X from the grid entry
1609: 23              INC     HL                  
160A: 36 72           LD      (HL),$72            ; the item's tile code
160C: 23              INC     HL                  
160D: 36 0C           LD      (HL),$0C            ; its colour attribute
160F: 23              INC     HL                  
1610: 1A              LD      A,(DE)              
1611: 77              LD      (HL),A              ; Y from the grid entry
1612: E1              POP     HL                  
1613: D1              POP     DE                  
1614: C9              RET                         

; The top dispatcher for the board-advance state, keyed on the board type. It first parks
; the moving sprite groups off-screen, then routes boardAdvanceStep to the handler for
; the current board: bit 0 of board set (25m or 75m) vectors through a 6-entry table of
; targets; bit 1 set (50m) through a 5-entry table; neither bit (100m) falls through to
; the rivet-board interlude frame, which runs the effect-sprite machine first and then
; dispatches its own table. The "HOW HIGH CAN YOU GET?" screen is painted from the
; board-setup state, not from this one, and nothing here establishes what the arms
; depict.
dispatchBoardClearedInterlude:
1615: CD BD 30        CALL    clearSpriteColumns  ; park the moving sprite rows off-screen
1618: 3A 27 62        LD      A,(workRam+227)     ; route on the board just cleared
161B: 0F              RRCA                        ; bit 0 marks 25m and 75m
161C: D2 2F 16        JP      NC,loc_162f         ; 
161F: 3A 88 63        LD      A,(workRam+388)     ; which step of the interlude this frame is
1622: EF              RST     $28                 ; vector through the six-entry table below

; ---- $1623-$162E: jump table ----
1623: 54 16 70 16 8A 16 32 17 57 17 8E 17

loc_162f:
162F: 0F              RRCA                        ; bit 1 marks the 50m board
1630: D2 41 16        JP      NC,runRivetBoardInterludeFrame; neither bit set means 100m
1633: 3A 88 63        LD      A,(workRam+388)     ; which step of the interlude this frame is
1636: EF              RST     $28                 ; vector through the five-entry table below

; ---- $1637-$1640: jump table ----
1637: A3 16 BB 16 32 17 57 17 8E 17

; The rivet board's arm of the board-cleared interlude, run once a frame. It chains two
; independent things in order: one frame of the effect-sprite state machine -- a four-way
; router on effectState that either idles, arms and spawns the effect, or counts it down
; -- and then the rivet-board interlude dispatch, which reads boardAdvanceStep and
; vectors to the handler that paints or animates that step. Each callee reads its own
; inputs from memory, so nothing is threaded between them here.
runRivetBoardInterludeFrame:
1641: CD BD 1D        CALL    dispatchEffectState ; one frame of the effect-sprite machine

dispatchRivetBoardInterludeStep:
1644: 3A 88 63        LD      A,(workRam+388)     ; which step of the rivet finale runs now
1647: EF              RST     $28                 ; vector through the six-entry table below

; ---- $1648-$1653: jump table ----
1648: B6 17 69 30 39 18 6F 18 80 18 C6 18

; Step 0 of the interlude that plays once a board is cleared, on the odd boards, so it
; runs exactly once. It spawns the opening tableau -- sound silenced, the whole-heart
; sprite record and its companion blink code seeded, three tilemap cells blanked, the
; sound-priority pair set -- then stages the scene's first pose by copying a fixed
; 40-byte, ten-record template into spriteObjBlock, arms a 32-frame pose hold in
; substateTimer, and falls into the shared tail that steps the sequence on and, on 25m
; only, raises all ten records by 4 pixels. The template is one large figure rather than
; a row of props: four of its ten records are parked off to the side carrying the blank
; sprite code, and the six that draw sit in a single block roughly 40 by 32 pixels.
; "Kong" is read off the drawn figure; nothing here identifies whose figure is carried
; away, as the other character's records were never separated out.
beginKongRecaptureInterlude:
1654: CD 08 17        CALL    spawnInterludeHeart ; silence sound, put the heart on screen
1657: 21 5C 38        LD      HL,$385C            ; the scene's first ten-record pose template
165A: CD 4E 00        CALL    loadSpriteObjectBlock; stamp it over the object block
165D: 3E 20           LD      A,$20               
165F: 32 09 60        LD      (workRam+9),A       ; hold the pose 32 frames

; The shared tail two of the board-cleared interlude's step handlers end in.
; boardAdvanceStep is incremented unconditionally, whichever board is being played.
; Then, on the 25m board only, 4 is subtracted from the Y byte of all ten spriteObjBlock
; records at once, a strided walk four bytes apart. A smaller Y is higher up the screen,
; so the whole staged figure rises 4 pixels. The subtraction is 8-bit and wraps, and it
; touches only the Y column -- every record's X, sprite code and attribute are left
; exactly as staged.
advanceInterludeStepAndLiftKongFigure:
1662: 21 88 63        LD      HL,$6388            ; the interlude's step selector
1665: 34              INC     (HL)                ; move the sequence on, whatever the board
1666: 3E 01           LD      A,$01               ; mask bit 0: the 25m board only
1668: F7              RST     $30                 ; board gate; other boards stop here
1669: 21 0B 69        LD      HL,$690B            ; the Y byte of sprite-object record 0
166C: 0E FC           LD      C,$FC               ; minus four
166E: FF              RST     $38                 ; raise all ten records four pixels
166F: C9              RET                         

; One timer-gated step of the board-cleared interlude on the odd boards, 25m and 75m:
; hold the current pose for a fixed number of frames, then stage the next one. Every
; frame it ticks substateTimer and returns. On the single expiry frame it copies this
; step's 40-byte sprite-object template over spriteObjBlock, re-arms substateTimer to
; 0x20 to hold the new pose 32 frames, advances boardAdvanceStep, and -- on 75m only --
; adds +4 to the Y column of all ten records, dropping the whole figure 4 pixels. What is
; in the bytes is that the new template's silhouette is wider (48 px against 40) and its
; records sit 4 px higher; reading a particular pose into that is a reading of the
; screen.
stageNextKongPoseWhenHoldExpires:
1670: DF              RST     $18                 ; hold the current pose until the timer expires
1671: 21 32 39        LD      HL,$3932            ; this step's ten-record pose template
1674: CD 4E 00        CALL    loadSpriteObjectBlock; swap it in over the staged pose
1677: 3E 20           LD      A,$20               
1679: 32 09 60        LD      (workRam+9),A       ; hold the new pose 32 frames
167C: 21 88 63        LD      HL,$6388            
167F: 34              INC     (HL)                ; advance the interlude's step selector
1680: 3E 04           LD      A,$04               ; mask bit 2: the 75m board only
1682: F7              RST     $30                 ; board gate; other boards stop here
1683: 21 0B 69        LD      HL,$690B            ; the Y byte of sprite-object record 0
1686: 0E 04           LD      C,$04               ; plus four
1688: FF              RST     $38                 ; drop all ten records four pixels
1689: C9              RET                         

; A timer-gated step of the board-cleared interlude: hold the pose, then re-seed the
; sprite-object block from a fixed template. Every frame it ticks substateTimer and
; returns while it is still counting, so the pose is HELD. On the single expiry frame it
; copies a 40-byte template -- ten 4-byte sprite records -- over spriteObjBlock,
; re-stamps record 1's X back to 0x66, and clears three bytes: the X of records 7 and 9,
; parking those two sprites for the whole of the next step, and the byte the next step
; uses as its climb animation phase counter. Zeroing that counter is what makes this the
; frame that STAGES a climb rather than one that animates it. It then tails into the
; shared board-advance tail, which steps the interlude's step selector, runs the
; per-board gate, and on 25m subtracts 4 from field 3 of every sprite-object record.
stageKongClimbPose:
168A: DF              RST     $18                 ; hold the pose until the timer expires
168B: 21 8C 38        LD      HL,$388C            ; this step's ten-record template
168E: CD 4E 00        CALL    loadSpriteObjectBlock; stamp it over the object block
1691: 3E 66           LD      A,$66               
1693: 32 0C 69        LD      (workRam+90C),A     ; record 1's X back to its fixed value
1696: AF              XOR     A                   
1697: 32 24 69        LD      (workRam+924),A     ; park record 7 for the whole next step
169A: 32 2C 69        LD      (workRam+92C),A     ; park record 9 with it
169D: 32 AF 62        LD      (workRam+2AF),A     ; zero the counter the climb animates on
16A0: C3 62 16        JP      advanceInterludeStepAndLiftKongFigure; the shared step tail

; Step 0 of the even-board branch of the same board-cleared interlude. It spawns the
; opening tableau, then RE-ANCHORS the figure: record 2's CURRENT X is read out of
; spriteObjBlock BEFORE the copy overwrites that byte, the fixed ten-record figure
; template is stamped over the block, and the difference between that old X and the
; template's own anchor is added into the X of all ten records -- so record 2 lands back
; on its previous X and the whole figure is carried with it. The re-anchor is here and
; not on the odd boards because a per-frame slide keeps shifting this same X column
; during 50m play, so a raw stamp would teleport the figure. Then boardAdvanceStep is
; bumped so the next frame runs the next step. Which record of the ten is which is not
; established; the name says who is re-stamped.
begin50mKongRecaptureInterlude:
16A3: CD 08 17        CALL    spawnInterludeHeart ; silence sound, put the heart on screen
16A6: 3A 10 69        LD      A,(workRam+910)     ; record 2's current X, read before the copy
16A9: D6 3B           SUB     $3B                 ; how far it sits from the template's anchor
16AB: 21 5C 38        LD      HL,$385C            ; the fixed ten-record figure template
16AE: CD 4E 00        CALL    loadSpriteObjectBlock; stamp it over the object block
16B1: 21 08 69        LD      HL,$6908            ; the X byte of sprite-object record 0
16B4: 4F              LD      C,A                 
16B5: FF              RST     $38                 ; carry all ten records back to that X
16B6: 21 88 63        LD      HL,$6388            
16B9: 34              INC     (HL)                ; move the sequence on
16BA: C9              RET                         

; The first-stage dispatcher for a horizontally moving group of ten sprites that walks
; back and forth between two rails. It runs first every frame and clears
; m50Obj1ReverseTimer, which is the "keep travelling" default: on an even frame the
; motion tick then decrements it from 0 to 0xFF, which is non-zero, so no reversal is
; taken. Only the bounce arm writes 1 there, so that the same decrement lands on ZERO --
; the case that reloads the period and REVERSES -- in that very frame. It then reads
; sprite record 2's X and the published signed step m50Obj1Step and routes: X at or
; above 90 has reached the rail, so hand to the second-stage dispatcher; X below the
; rail with a NEGATIVE step schedules a reversal and slides, which is the bounce; X
; below with a positive step just slides. A reversal is scheduled only while the group
; is still travelling INTO the edge nearest it.
dispatchKongWalkFrame:
16BB: AF              XOR     A                   
16BC: 32 A0 62        LD      (workRam+2A0),A     ; clear the reversal flag: keep going
16BF: 3A A3 63        LD      A,(workRam+3A3)     ; the published signed per-frame step
16C2: 4F              LD      C,A                 
16C3: 3A 10 69        LD      A,(workRam+910)     ; record 2's X, the group's leading edge
16C6: FE 5A           CP      $5A                 ; has it reached the rail region?
16C8: D2 E1 16        JP      NC,endKongWalkAndAdvanceInterlude; at the rail region
16CB: CB 79           BIT     7,C                 ; is the step heading into the near edge?
16CD: CA D5 16        JP      Z,stepKongWalk      ; heading away, so just slide

loc_16d0:
16D0: 3E 01           LD      A,$01               
16D2: 32 A0 62        LD      (workRam+2A0),A     ; schedule a reversal on the next tick

; Slide the interlude's ten-record figure one step along X. Three acts: advance the first
; timed board object -- ticking its even-frame countdown, reloading it and reversing its
; step direction on the decrement that REACHES zero, republishing its signed per-frame
; step, and every 32nd frame advancing its mirrored sprite-animation pair -- take that
; freshly published step, and add it to the X field of all ten records of spriteObjBlock.
; The step is 0 on even frames, so the figure holds still while the object's own state
; advances, and slides one pixel either way on odd frames. The block it slides is the
; figure the preceding interlude step stamped there; nothing in the motion identifies who
; that figure is.
stepKongWalk:
16D5: CD 02 26        CALL    loc_2602            ; advance the object's timers and direction
16D8: 3A A3 63        LD      A,(workRam+3A3)     ; take the freshly published step
16DB: 4F              LD      C,A                 
16DC: 21 08 69        LD      HL,$6908            ; the X byte of sprite-object record 0
16DF: FF              RST     $38                 ; slide all ten records one step along X
16E0: C9              RET                         

; The second stage of the pair that walks a horizontally-moving group of ten sprites back
; and forth during a between-boards interlude. The first stage runs every frame and, once
; the group's leading record X has climbed to the rail region, hands that X and the
; object's published signed per-frame step over here. Three outcomes. X still short of
; 93: the group has arrived, so reinitialise its object block -- recopy the sprite
; template, clear the per-object scratch -- and advance the interlude's step counter. X
; at or past 93 with a POSITIVE step, still travelling into the rail: schedule a
; direction reversal for the next tick and slide this frame, which is the bounce. X at or
; past 93 with a NEGATIVE step, already moving away: just slide. This stage touches no
; memory; it tests its two inputs and tail-calls the chosen handler.
endKongWalkAndAdvanceInterlude:
16E1: FE 5D           CP      $5D                 ; short of the rail threshold?
16E3: DA EE 16        JP      C,reloadObjectBlockAndAdvanceStep; arrived, so reinitialise
16E6: CB 79           BIT     7,C                 ; still travelling into the rail?
16E8: CA D0 16        JP      Z,loc_16d0          ; yes: bounce off it
16EB: C3 D5 16        JP      stepKongWalk        ; already moving away, just slide

; One phase of the board-cleared interlude. It reloads the 40-byte (ten-record)
; sprite-object block from its stored template -- the same template the opening climb
; cutscene loads -- then patches three record field-0 bytes AFTER the copy: record 1 <-
; 0x66, records 7 and 9 <- 0. Each target sits inside the block the copy just filled, so
; the copy must run first or the patch is lost, and none of the three template bytes
; already holds the patched value. It then clears a board-object bookkeeping byte and
; advances boardAdvanceStep. A sibling handler does the identical reload and patch under
; a timer gate and a different tail.
reloadObjectBlockAndAdvanceStep:
16EE: 21 8C 38        LD      HL,$388C            ; the stored ten-record template
16F1: CD 4E 00        CALL    loadSpriteObjectBlock; reload the sprite-object block
16F4: 3E 66           LD      A,$66               
16F6: 32 0C 69        LD      (workRam+90C),A     ; patch record 1's X after the copy
16F9: AF              XOR     A                   
16FA: 32 24 69        LD      (workRam+924),A     ; park record 7
16FD: 32 2C 69        LD      (workRam+92C),A     ; park record 9
1700: 32 AF 62        LD      (workRam+2AF),A     ; clear the board-object bookkeeping byte
1703: 21 88 63        LD      HL,$6388            
1706: 34              INC     (HL)                ; advance the interlude's step selector
1707: C9              RET                         

; The board-cleared interlude's opening tableau. Straight-line and input-independent --
; it reads no memory and every store is a constant. It zeroes every sound output and its
; work RAM shadow, seeds a fixed 4-byte record in spriteBuffer to [X 0x80, code 0x76,
; attribute 0x09, Y 0x20] -- code 0x76 is the whole heart -- seeds the blink sprite's
; code byte to 0x13, the byte whose bit 7 the colour cycle toggles to make it blink,
; lays the descending codes 0x10 / 0x0F / 0x0E down three cells of one tilemap column,
; and finally sets sndPriority and sndPriorityFrames to 7 and 3, re-writing what the
; silencing zeroed. All three of those tile codes are blank glyphs, so that step BLANKS
; three cells rather than drawing anything; what it clears off the screen is not
; established.
spawnInterludeHeart:
1708: CD 1C 01        CALL    silenceSound        ; zero every sound output before the scene
170B: 21 20 6A        LD      HL,$6A20            ; the heart's sprite record
170E: 36 80           LD      (HL),$80            ; X
1710: 23              INC     HL                  
1711: 36 76           LD      (HL),$76            ; the whole-heart tile code
1713: 23              INC     HL                  
1714: 36 09           LD      (HL),$09            ; colour attribute
1716: 23              INC     HL                  
1717: 36 20           LD      (HL),$20            ; Y
1719: 21 05 69        LD      HL,$6905            ; the blink sprite's code byte
171C: 36 13           LD      (HL),$13            ; the colour cycle toggles bit 7 to blink it
171E: 21 C4 75        LD      HL,$75C4            ; one tilemap column
1721: 11 20 00        LD      DE,$0020            ; one row apart
1724: 3E 10           LD      A,$10               ; blank tiles, so the three cells are cleared
1726: CD 14 05        CALL    fillDescendingColumn; write 0x10, 0x0F and 0x0E down them
1729: 21 8A 60        LD      HL,$608A            ; the sound-priority pair
172C: 36 07           LD      (HL),$07            ; restore the priority the silencing zeroed
172E: 23              INC     HL                  
172F: 36 03           LD      (HL),$03            ; hold it three frames
1731: C9              RET                         

; Hold the interlude while the figure climbs, then break the heart and move the sequence
; on. Where its sibling steps hold on a frame timer, this one holds on a POSITION. It
; ticks the sprite-object block's animation, which on every eighth call scrolls the whole
; ten-record group up four pixels and animates its code bytes, and holds while the probed
; record's Y is still at or below the top threshold -- so most frames just tick the phase
; counter. Once that record clears the threshold the step finishes: park three sprite X
; bytes at zero, restore two more to their template X (an earlier step parked them for
; the climb), step the heart record's sprite code on by one -- the same shape with a
; jagged split through it -- and advance boardAdvanceStep. That one figure climbs and the
; heart breaks as it clears the top; which records of the block belong to which character
; is not established.
climbKongFigureAndBreakHeart:
1732: CD 6F 30        CALL    animateSpriteObjectBlock; every eighth call it climbs 4px
1735: 3A 13 69        LD      A,(workRam+913)     ; the probed record's Y
1738: FE 2C           CP      $2C                 ; has it cleared the top threshold?
173A: D0              RET     NC                  ; still climbing: hold this step
173B: AF              XOR     A                   
173C: 32 00 69        LD      (workRam+900),A     ; park a sprite X at the left edge
173F: 32 04 69        LD      (workRam+904),A     ; and its neighbour
1742: 32 0C 69        LD      (workRam+90C),A     ; and object record 1's X
1745: 3E 6B           LD      A,$6B               
1747: 32 24 69        LD      (workRam+924),A     ; record 7 back to its template X
174A: 3D              DEC     A                   
174B: 32 2C 69        LD      (workRam+92C),A     ; record 9 back to its template X
174E: 21 21 6A        LD      HL,$6A21            ; the heart's tile code
1751: 34              INC     (HL)                ; step it on one: the heart breaks
1752: 21 88 63        LD      HL,$6388            
1755: 34              INC     (HL)                ; move the interlude on
1756: C9              RET                         

; One arm of the board-advance sequence: march the sprite-object block off the top of the
; screen and, once it is empty, move the sequence on. Dispatched from inside the vblank
; interrupt while a board is being torn down. Each frame it runs the block animation --
; every eighth call, scroll the whole ten-record block up 4px and flip a few records --
; then zeroes the X of any record that has risen above the top line (Y < 0x19), then
; scans all ten record X bytes for zero. If a slot is still occupied the arm aborts and
; tries again next frame with boardAdvanceStep left alone. If every slot is clear it arms
; substateTimer to 64 frames and increments boardAdvanceStep, so the next interrupt
; dispatches the following arm.
advanceBoardStepWhenSpritesCleared:
1757: CD 6F 30        CALL    animateSpriteObjectBlock; march the block up the screen
175A: CD 6C 17        CALL    cullSpriteObjectsAtTop; park records risen off the top
175D: 23              INC     HL                  ; bump the returned pointer and stride into shape
175E: 13              INC     DE                  
175F: CD 83 17        CALL    allSlotsClear       ; abandon the frame unless all ten are clear
1762: 3E 40           LD      A,$40               
1764: 32 09 60        LD      (workRam+9),A       ; dwell 64 frames before the next sub-state
1767: 21 88 63        LD      HL,$6388            
176A: 34              INC     (HL)                ; step to the next arm of the sequence
176B: C9              RET                         

; Sweep the ten 4-byte spriteObjBlock records and, for each whose Y has risen above the
; top line (smaller Y is higher on this screen), zero that record's X, parking the
; sprite at the left edge. Nothing else in any record is touched and each is decided on
; its own, so the pass is order-independent. It also leaves behind a scan pointer and a
; record stride each ONE SHORT of the value that will be used: the caller bumps both by
; one before handing them to the follow-up scan that decides whether every sprite has
; now been swept off.
cullSpriteObjectsAtTop:
176C: 11 03 00        LD      DE,$0003            
176F: 21 2F 69        LD      HL,$692F            ; the Y byte of the last record
1772: 06 0A           LD      B,$0A               ; ten records to sweep

loc_1774:
1774: A7              AND     A                   
1775: 7E              LD      A,(HL)              ; read this record's Y
1776: ED 52           SBC     HL,DE               ; back up to the record's X
1778: FE 19           CP      $19                 ; has it risen above the top line?
177A: D2 7F 17        JP      NC,loc_177f         ; 
177D: 36 00           LD      (HL),$00            ; park it at the left edge

loc_177f:
177F: 2B              DEC     HL                  ; on to the record below
1780: 10 F2           DJNZ    loc_1774            ; 
1782: C9              RET                         

; Are ten object slots all empty? Walks ten cells from a caller-given base at a
; caller-given stride and reports whether every one is zero, stopping at the first
; non-zero. Only the count, ten, is hard-wired. Its caller drives it over spriteObjBlock
; with a stride of four, so in practice it answers "are all ten object sprites cleared?"
; before arming the next interlude phase. On a non-zero cell it discards the caller's
; return address and returns past it, aborting the caller mid-flow.
allSlotsClear:
1783: 06 0A           LD      B,$0A               ; ten slots to check

loc_1785:
1785: 7E              LD      A,(HL)              
1786: A7              AND     A                   
1787: C2 26 00        JP      NZ,loc_0026         ; an occupied slot aborts the caller
178A: 19              ADD     HL,DE               ; next slot
178B: 10 F8           DJNZ    loc_1785            ; 
178D: C9              RET                         ; all ten empty

; Move the board order on and enter the "HOW HIGH CAN YOU GET?" interlude that introduces
; the next board. It ticks substateTimer and does nothing at all while that is still
; running. On the frame it expires, it walks boardSeqPtr one entry forward through the
; board-order table and reads the board waiting there; the table ends in a terminator
; byte, and reaching it puts the pointer back to the start of the repeating group rather
; than stopping, which is how the game keeps handing out boards forever once a player is
; past the fixed opening order. The result is published to board. It then posts a
; deferred task, clears boardAdvanceStep so the next board-cleared run starts from its
; own first step, and arms a 48-frame wait before the how-high interlude.
advanceToNextBoard:
178E: DF              RST     $18                 ; hold until the sub-state timer expires
178F: 2A 2A 62        LD      HL,(workRam+22A)    ; the board-order pointer
1792: 23              INC     HL                  ; step one entry forward
1793: 7E              LD      A,(HL)              ; the board waiting there
1794: FE 7F           CP      $7F                 ; the table's end marker?
1796: C2 9D 17        JP      NZ,loc_179d         ; 
1799: 21 73 3A        LD      HL,$3A73            ; wrap to the start of the repeating group
179C: 7E              LD      A,(HL)              ; and take the board there instead

loc_179d:
179D: 22 2A 62        LD      (workRam+22A),HL    ; store the advanced pointer
17A0: 32 27 62        LD      (workRam+227),A     ; publish the board coming up
17A3: 11 00 05        LD      DE,$0500            
17A6: CD 9F 30        CALL    enqueueTask         ; post its deferred task
17A9: AF              XOR     A                   
17AA: 32 88 63        LD      (workRam+388),A     ; the next cleared-board run starts at 0
17AD: 21 09 60        LD      HL,$6009            
17B0: 36 30           LD      (HL),$30            ; wait 48 frames
17B2: 23              INC     HL                  
17B3: 36 08           LD      (HL),$08            ; then the how-high interlude sub-state
17B5: C9              RET                         

loc_17b6:
17B6: 00              NOP                         
17B7: CD 1C 01        CALL    silenceSound        ; cut every sound line before the scene
17BA: 21 8A 60        LD      HL,$608A            ; the sound-priority pair
17BD: 36 0E           LD      (HL),$0E            ; the tune this scene opens with
17BF: 23              INC     HL                  
17C0: 36 03           LD      (HL),$03            ; hold it three frames
17C2: 3E 10           LD      A,$10               ; the blank code the two runs count down from
17C4: 11 20 00        LD      DE,$0020            ; one tilemap row between cells
17C7: 21 23 76        LD      HL,$7623            ; three cells near the top of the screen
17CA: CD 14 05        CALL    fillDescendingColumn; blank them
17CD: 21 83 75        LD      HL,$7583            ; three more, the codes carrying on down from the first run
17D0: CD 14 05        CALL    fillDescendingColumn; 
17D3: 21 DA 76        LD      HL,$76DA            ; the first of four bands -- the board's structure is erased
17D6: CD 26 18        CALL    fillTileBlock       ; 
17D9: 11 47 3A        LD      DE,$3A47            ; a bare girder low on the screen
17DC: CD A7 0D        CALL    drawBoardLayout     ; 
17DF: 21 D5 76        LD      HL,$76D5            ; the next band up
17E2: CD 26 18        CALL    fillTileBlock       ; 
17E5: 11 4D 3A        LD      DE,$3A4D            ; a second girder eight pixels above it
17E8: CD A7 0D        CALL    drawBoardLayout     ; 
17EB: 21 D0 76        LD      HL,$76D0            ; and the next
17EE: CD 26 18        CALL    fillTileBlock       ; 
17F1: 11 53 3A        LD      DE,$3A53            ; a third girder
17F4: CD A7 0D        CALL    drawBoardLayout     ; 
17F7: 21 CB 76        LD      HL,$76CB            ; and the last
17FA: CD 26 18        CALL    fillTileBlock       ; 
17FD: 11 59 3A        LD      DE,$3A59            ; a fourth: the top of the heap Kong lands on
1800: CD A7 0D        CALL    drawBoardLayout     ; 
1803: 21 5C 38        LD      HL,$385C            ; Kong's ten-record figure, as the board build stamps it
1806: CD 4E 00        CALL    loadSpriteObjectBlock; 
1809: 21 08 69        LD      HL,$6908            ; the X byte of sprite-object record 0
180C: 0E 44           LD      C,$44               
180E: FF              RST     $38                 ; carry all ten records to where the board build puts them
180F: 21 05 69        LD      HL,$6905            ; the blink sprite's code byte
1812: 36 13           LD      (HL),$13            ; the code the colour cycle toggles to blink it
1814: 3E 20           LD      A,$20               
1816: 32 09 60        LD      (workRam+9),A       ; hold the tableau 32 frames
1819: 3E 80           LD      A,$80               
181B: 32 90 63        LD      (workRam+390),A     ; seed the counter the next step animates on
181E: 21 88 63        LD      HL,$6388            
1821: 34              INC     (HL)                ; move the finale on a step
1822: 22 C0 63        LD      (workRam+3C0),HL    ; aim the shared timed advancer at that byte
1825: C9              RET                         

; Stamp a fixed 5-wide by 14-tall block of tile 0x10 into the tilemap at the caller's
; address. Each row writes five cells and then steps the pointer back by 0x25 -- a net
; -0x20, one tilemap row up at the same left edge. The fill value, the width, the height
; and the row step are all constants baked in; the destination address is the only input,
; and no memory is read at all. Used by the game-over and player-switch render sequences
; and by the board-start intro setup.
fillTileBlock:
1826: 11 DB FF        LD      DE,$FFDB            ; row step: back to the left edge one row up
1829: 0E 0E           LD      C,$0E               ; fourteen rows tall
182B: 3E 10           LD      A,$10               ; the blank tile to stamp

loc_182d:
182D: 06 05           LD      B,$05               ; five cells across

loc_182f:
182F: 77              LD      (HL),A              
1830: 23              INC     HL                  
1831: 10 FC           DJNZ    loc_182f            ; 
1833: 19              ADD     HL,DE               ; step to the next row up
1834: 0D              DEC     C                   
1835: C2 2D 18        JP      NZ,loc_182d         ; 
1838: C9              RET                         

stepSpriteAnimationSequence:
1839: 21 90 63        LD      HL,$6390            ; the scene's frame counter, seeded to 0x80 by the opening step
183C: 34              INC     (HL)                
183D: CA 59 18        JP      Z,loc_1859          ; it wraps 128 frames later: the pose is done
1840: 7E              LD      A,(HL)              
1841: E6 07           AND     $07                 ; only every eighth frame changes the pose
1843: C0              RET     NZ                  
1844: 11 CF 39        LD      DE,$39CF            ; one of two poses
1847: CB 5E           BIT     3,(HL)              ; bit 3 turns over every eight frames, so the two alternate
1849: 20 03           JR      NZ,loc_184e         ; 
184B: 11 F7 39        LD      DE,$39F7            ; the other pose

loc_184e:
184E: EB              EX      DE,HL               
184F: CD 4E 00        CALL    loadSpriteObjectBlock; swap it in
1852: 21 08 69        LD      HL,$6908            
1855: 0E 44           LD      C,$44               
1857: FF              RST     $38                 ; and shift the fresh copy back into place
1858: C9              RET                         

loc_1859:
1859: 21 5C 38        LD      HL,$385C            ; back to the pose the scene opened with
185C: CD 4E 00        CALL    loadSpriteObjectBlock; 
185F: 21 08 69        LD      HL,$6908            
1862: 0E 44           LD      C,$44               
1864: FF              RST     $38                 ; shifted into place as before
1865: 3E 20           LD      A,$20               
1867: 32 09 60        LD      (workRam+9),A       ; hold it 32 frames
186A: 21 88 63        LD      HL,$6388            
186D: 34              INC     (HL)                ; then move the finale on
186E: C9              RET                         

loc_186f:
186F: DF              RST     $18                 ; hold the pose until the frame timer expires
1870: 21 1F 3A        LD      HL,$3A1F            ; the same figure inverted and 0x20 lower: Kong topples
1873: CD 4E 00        CALL    loadSpriteObjectBlock; 
1876: 3E 03           LD      A,$03               
1878: 32 84 60        LD      (workRam+84),A      ; cue the falling sound for three frames
187B: 21 88 63        LD      HL,$6388            
187E: 34              INC     (HL)                ; move the finale on to the fall itself
187F: C9              RET                         

loc_1880:
1880: 21 0B 69        LD      HL,$690B            ; the Y byte of sprite-object record 0
1883: 0E 01           LD      C,$01               
1885: FF              RST     $38                 ; drop the whole figure a pixel: the fall, one pixel a frame
1886: 3A 1B 69        LD      A,(workRam+91B)     ; record 4's Y, the probe for the landing
1889: FE D0           CP      $D0                 ; the row it lands on
188B: C0              RET     NZ                  ; still falling: nothing more this frame
188C: 3E 20           LD      A,$20               
188E: 32 19 69        LD      (workRam+919),A     ; record 4 takes its landed sprite code
1891: 21 24 6A        LD      HL,$6A24            ; a sprite record at the foot of the screen
1894: 36 7F           LD      (HL),$7F            ; X, mid-screen
1896: 2C              INC     L                   
1897: 36 39           LD      (HL),$39            ; its tile code
1899: 2C              INC     L                   
189A: 36 01           LD      (HL),$01            ; colour attribute
189C: 2C              INC     L                   
189D: 36 D8           LD      (HL),$D8            ; Y, level with the topmost girder of the heap
189F: 21 C6 76        LD      HL,$76C6            ; wipe the last band of playfield left standing
18A2: CD 26 18        CALL    fillTileBlock       ; 
18A5: 11 5F 3A        LD      DE,$3A5F            ; and draw the girder the scene ends on
18A8: CD A7 0D        CALL    drawBoardLayout     ; 
18AB: 11 04 00        LD      DE,$0004            
18AE: 01 28 02        LD      BC,$0228            ; two records, plus 0x28
18B1: 21 03 69        LD      HL,$6903            ; the Y byte of sprite records 0 and 1
18B4: CD 3D 00        CALL    addStrided          ; drop that pair onto the new girder
18B7: 3E 00           LD      A,$00               
18B9: 32 AF 62        LD      (workRam+2AF),A     ; zero the counter the last step winds down
18BC: 3E 03           LD      A,$03               
18BE: 32 82 60        LD      (workRam+82),A      ; ring the impact sound: Kong has landed
18C1: 21 88 63        LD      HL,$6388            
18C4: 34              INC     (HL)                ; on to the last step of the finale
18C5: C9              RET                         

runRivetBoardFinaleThenAdvanceLevel:
18C6: 21 AF 62        LD      HL,$62AF            ; the finale's countdown, left at zero by the landing step
18C9: 35              DEC     (HL)                ; the first tick wraps it to 0xFF, so the scene runs 256 frames
18CA: CA 3D 19        JP      Z,loc_193d          ; at zero the scene is over and the board moves on
18CD: 7E              LD      A,(HL)              
18CE: E6 07           AND     $07                 ; the animation below steps every eighth frame
18D0: C0              RET     NZ                  
18D1: 21 25 6A        LD      HL,$6A25            ; the code byte of the sprite at the foot of the screen
18D4: 7E              LD      A,(HL)              
18D5: EE 80           XOR     $80                 ; mirror it, so it rocks back and forth
18D7: 77              LD      (HL),A              
18D8: 21 19 69        LD      HL,$6919            ; record 4's sprite code
18DB: 46              LD      B,(HL)              
18DC: CB A8           RES     5,B                 ; mask off the bit the lookup must not see
18DE: AF              XOR     A                   
18DF: CD 09 30        CALL    nextAnimationStep   ; step the pose round its cycle
18E2: F6 20           OR      $20                 ; put the bit back
18E4: 77              LD      (HL),A              
18E5: 21 AF 62        LD      HL,$62AF            
18E8: 7E              LD      A,(HL)              
18E9: FE E0           CP      $E0                 ; 32 frames into the scene
18EB: C2 10 19        JP      NZ,loc_1910         ; 
18EE: 3E 50           LD      A,$50               
18F0: 32 4F 69        LD      (workRam+94F),A     ; stand Mario on the girder left at the top
18F3: 3E 00           LD      A,$00               
18F5: 32 4D 69        LD      (workRam+94D),A     ; the scene's pose, unmirrored
18F8: 3E 9F           LD      A,$9F               
18FA: 32 4C 69        LD      (workRam+94C),A     ; at a fixed X to one side of mid-screen
18FD: 3A 03 62        LD      A,(workRam+203)     ; unless he finished on the other side
1900: FE 80           CP      $80                 ; the screen midpoint
1902: D2 0F 19        JP      NC,loc_190f         ; 
1905: 3E 80           LD      A,$80               
1907: 32 4D 69        LD      (workRam+94D),A     ; then the mirrored pose
190A: 3E 5F           LD      A,$5F               
190C: 32 4C 69        LD      (workRam+94C),A     ; and the mirror-image X

loc_190f:
190F: 7E              LD      A,(HL)              ; recover the countdown the position test overwrote

loc_1910:
1910: FE C0           CP      $C0                 ; 64 frames in
1912: C0              RET     NZ                  
1913: 21 8A 60        LD      HL,$608A            ; the sound-priority pair again
1916: 36 0C           LD      (HL),$0C            ; the closing tune for an odd level number
1918: 3A 29 62        LD      A,(workRam+229)     ; which of the two depends on the level's low bit
191B: 0F              RRCA                        
191C: 38 02           JR      C,loc_1920          ; 
191E: 36 05           LD      (HL),$05            ; an even level gets the other one

loc_1920:
1920: 23              INC     HL                  
1921: 36 03           LD      (HL),$03            ; held three frames
1923: 21 23 6A        LD      HL,$6A23            ; the heart's sprite record, filled backwards
1926: 36 40           LD      (HL),$40            ; Y, a little above Mario
1928: 2B              DEC     HL                  
1929: 36 09           LD      (HL),$09            ; colour attribute
192B: 2B              DEC     HL                  
192C: 36 76           LD      (HL),$76            ; the whole-heart tile code
192E: 2B              DEC     HL                  
192F: 36 8F           LD      (HL),$8F            ; X, between him and mid-screen
1931: 3A 03 62        LD      A,(workRam+203)     ; which side he finished on, once more
1934: FE 80           CP      $80                 
1936: D0              RET     NC                  
1937: 3E 6F           LD      A,$6F               
1939: 32 20 6A        LD      (workRam+A20),A     ; the mirror-image X, keeping the heart inboard
193C: C9              RET                         

loc_193d:
193D: 2A 2A 62        LD      HL,(workRam+22A)    ; the board-order pointer
1940: 23              INC     HL                  ; step one entry forward
1941: 7E              LD      A,(HL)              ; the board waiting there
1942: FE 7F           CP      $7F                 ; the table's end marker
1944: C2 4B 19        JP      NZ,loc_194b         ; 
1947: 21 73 3A        LD      HL,$3A73            ; wrap back to the head of the repeating group
194A: 7E              LD      A,(HL)              ; and take the board there instead

loc_194b:
194B: 22 2A 62        LD      (workRam+22A),HL    ; store the advanced pointer
194E: 32 27 62        LD      (workRam+227),A     ; publish the board coming up
1951: 21 29 62        LD      HL,$6229            
1954: 34              INC     (HL)                ; one more level -- the only place the count moves
1955: 11 00 05        LD      DE,$0500            
1958: CD 9F 30        CALL    enqueueTask         ; post the new board's deferred task
195B: AF              XOR     A                   
195C: 32 2E 62        LD      (workRam+22E),A     ; the how-high height starts over
195F: 32 88 63        LD      (workRam+388),A     ; the next cleared-board run starts at 0
1962: 21 09 60        LD      HL,$6009            
1965: 36 E0           LD      (HL),$E0            ; wait 224 frames
1967: 23              INC     HL                  
1968: 36 08           LD      (HL),$08            ; then the how-high interlude sub-state
196A: C9              RET                         

; A phase hand-off arm of the in-game sub-state table: blank every tilemap cell and zero
; the 384-byte sprite shadow buffer, then re-point the sub-state machine into a later
; phase group with gameSubstate = activePlayerIndex + 0x12, an 8-bit add. The base is
; deliberately not folded into a single constant -- the target sub-state really is
; computed from the player index. It reads exactly that one input byte.
clearScreenAndSelectSubstate:
196B: CD 52 08        CALL    clearTilemapAndSprites; blank the screen and sprite shadow
196E: 3A 0E 60        LD      A,(workRam+E)       ; the player who is up
1971: C6 12           ADD     A,$12               ; select that player's later phase group
1973: 32 0A 60        LD      (workRam+A),A       ; hand the sub-state machine on
1976: C9              RET                         

; The attract demo's per-frame entry, and only two steps: write this frame's canned
; control word over the cooked player input, then run the same per-frame update cascade
; a played game runs, which reads that word as if a joystick had produced it. The order
; is the whole of it -- run the cascade first and it consumes the previous frame's input
; instead of this one's. A demo frame is an ordinary frame whose input was canned. The
; in-game path enters the same cascade one instruction later, skipping only the script
; step.
runAttractDemoFrame:
1977: CD EE 21        CALL    advanceAttractDemoInput; lay down this frame's canned input

; One frame of play. Twenty-four subsystem updates run in a fixed sequence with nothing
; passed between them; what this routine contributes is the ORDER, three gates that can
; abandon the rest of the frame, and the death hand-off at the end. The three gates are
; the effect latch (an effect is playing, so the frame belongs to it), the board-won
; check (the win path already committed the advance), and the bonus-expired step machine.
; The tail reads marioActive: Mario can be alive at entry and dead by the time the
; updates finish, since the collision and hazard updates above are what kill him. Still
; active and the frame is simply over; zero, and this silences every sound output, fires
; the death sound trigger, and steps gameSubstate on. The attract demo enters this same
; body one instruction earlier, after its input-script call.
runGameplayFrame:
197A: CD BD 1D        CALL    dispatchEffectState ; one frame of the effect-sprite machine

loc_197d:
197D: CD 8C 1E        CALL    runHitEffectInsteadOfPlay; an effect playing owns the frame

loc_1980:
1980: CD C3 1A        CALL    dispatchMarioMovement; Mario's movement for this frame
1983: CD 72 1F        CALL    update25mBarrels    ; walk the ten barrel slots, 25m only
1986: CD 8F 2C        CALL    driveBarrelRelease  ; step a barrel on its way out
1989: CD 03 2C        CALL    scheduleBarrelRelease; decide whether a release is due
198C: CD ED 30        CALL    updateFires         ; the fireballs
198F: CD 04 2E        CALL    update75mActorObjects; 75m only, while Mario is alive
1992: CD EA 24        CALL    update50mMovingObjects; 50m only: the travelling objects
1995: CD DB 2D        CALL    raisePeriodicObjectSpawnRequests; 50m and 100m only
1998: CD D4 2E        CALL    driveHammerSprite   ; the hammer object, and its tune
199B: CD 07 22        CALL    dispatch50mObjectState; 50m only: one of two, by parity
199E: CD 33 1A        CALL    collectEdgeRivet    ; 100m: a rivet he has stepped off
19A1: CD 85 2A        CALL    startMarioFallWhenGroundGivesWay; is his footing level?
19A4: CD 46 1F        CALL    beginMarioFall      ; and if it armed one, start the fall
19A7: CD FA 26        CALL    service75mBoard     ; 75m; the screen bottom kills him
19AA: CD F2 25        CALL    update50mConveyorObjects; and carry Mario along his row
19AD: CD DA 19        CALL    scanObjectsAtMarioX ; an object at Mario's exact X?
19B0: CD FB 03        CALL    slide50mSpriteRowAndServiceColorCycle; 
19B3: CD 08 28        CALL    killMarioOnObjectCollision; did a hazard just reach him?
19B6: CD 1D 28        CALL    recordHammerHitOnObject; the same test for his hammer
19B9: CD 57 1E        CALL    checkBoardWonByType ; has the board just been won?
19BC: CD 07 1A        CALL    dispatchBonusExpiredStep; the bonus-ran-out kill sequence
19BF: CD CB 2F        CALL    tickTimedBoardBonus ; count the bonus down (not on 25m)
19C2: 00              NOP                         ; three spare bytes -- exactly the width of one more call
19C3: 00              NOP                         
19C4: 00              NOP                         
19C5: 3A 00 62        LD      A,(workRam+200)     ; the death hand-off: did he survive the frame?
19C8: A7              AND     A                   
19C9: C0              RET     NZ                  ; still alive, and the frame is simply over
19CA: CD 1C 01        CALL    silenceSound        ; he did not: cut every sound output off
19CD: 21 82 60        LD      HL,$6082            ; the death sound trigger
19D0: 36 03           LD      (HL),$03            ; assert it for three frames, then run on into the sub-state step

; Advance to the next sub-state after a delay: bump gameSubstate by one, stepping the
; sub-state dispatch index to the next handler within the current game state, and reload
; substateTimer with 64 so the new sub-state waits 64 frames before it may proceed. This
; is the shared closing act of an in-state update -- the "wait N frames then go to state
; M" idiom with M = current + 1.
advanceSubstateAndArmTimer:
19D2: 21 0A 60        LD      HL,$600A            
19D5: 34              INC     (HL)                ; step to the next sub-state handler
19D6: 2B              DEC     HL                  ; the sub-state timer sits just below
19D7: 36 40           LD      (HL),$40            ; make the new sub-state wait 64 frames
19D9: C9              RET                         

; The broad phase of the per-frame object-collision check, run once a frame from the
; update cascade. It walks the three 4-byte objectCollisionSprites records comparing
; each record's X against marioX, and on the FIRST whose X matches it hands that record
; to the narrow phase -- which checks the Y alignment and the record's eligibility
; before registering a hit -- and stops scanning. With no match it touches nothing. The
; X test is exact equality, not a range, so a record has to line up with Mario to the
; pixel before the narrow phase is consulted.
scanObjectsAtMarioX:
19DA: 3A 03 62        LD      A,(workRam+203)     ; Mario's X, the key for the broad phase
19DD: 06 03           LD      B,$03               ; three collision records
19DF: 21 0C 6A        LD      HL,$6A0C            ; base of the collision records

loc_19e2:
19E2: BE              CP      (HL)                ; exact X equality, not a range
19E3: CA ED 19        JP      Z,confirmObjectHit  ; lined up: consult the narrow phase
19E6: 2C              INC     L                   ; on to the next record
19E7: 2C              INC     L                   
19E8: 2C              INC     L                   
19E9: 2C              INC     L                   
19EA: 10 F6           DJNZ    loc_19e2            ; 
19EC: C9              RET                         

; The confirm half of the object-slot collision scan. The scan walks a 3-entry, stride-4
; table of collision records comparing each record's X (+0) against marioX, and on a
; match branches here with that record's base. This finishes the test: marioY must equal
; the record's Y byte (+3), and bit 3 of its flag byte (+1) must be CLEAR, i.e. the
; object has not already been consumed. Either check failing returns having touched
; nothing. Both passing registers the hit for the effect machine -- effectParamPtr = the
; record base, effectSelect = 0, effectState = 1 -- which services it on later frames.
confirmObjectHit:
19ED: 3A 05 62        LD      A,(workRam+205)     ; the Y half of the test
19F0: 2C              INC     L                   ; step to the record's Y byte
19F1: 2C              INC     L                   
19F2: 2C              INC     L                   
19F3: BE              CP      (HL)                ; must match Mario's row exactly
19F4: C0              RET     NZ                  ; wrong row, no hit
19F5: 2D              DEC     L                   ; back to the record's flag byte
19F6: 2D              DEC     L                   
19F7: CB 5E           BIT     3,(HL)              ; bit 3 marks an object already consumed
19F9: C0              RET     NZ                  ; nothing left to collect here
19FA: 2D              DEC     L                   ; back to the record base
19FB: 22 43 63        LD      (workRam+343),HL    ; hand the record to the effect machine
19FE: AF              XOR     A                   
19FF: 32 42 63        LD      (workRam+342),A     ; effect kind 0
1A02: 3C              INC     A                   
1A03: 32 40 63        LD      (workRam+340),A     ; arm the effect for later frames
1A06: C9              RET                         

; Run whichever step of the bonus-expired sequence is current. When the on-screen bonus
; counter runs down to zero the player is killed for it, and that does not happen in one
; frame: bonusExpiredStep is the four-state machine that carries it out. Step 0 idle, the
; bonus has not expired; step 1 clears the delay counter and moves on; step 2 counts that
; delay down; step 3 holds until Mario is back on the ground and then takes the death
; exit. The answer it returns says whether the rest of the frame's gameplay work should
; still run -- only step 3 can ever say no, and only on the arm that takes the death
; exit.
dispatchBonusExpiredStep:
1A07: 3A 86 63        LD      A,(workRam+386)     ; which step of the sequence is current
1A0A: EF              RST     $28                 ; vector through the step table below

; ---- $1A0B-$1A14: jump table ----
1A0B: 1E 1A 15 1A 1F 1A 2A 1A 00 00

; Arm the DELAY phase of the bonus-expired death sequence. bonusExpiredStep is a
; four-state machine the per-frame gameplay cascade dispatches every frame: state 0 does
; nothing while bonus is still above 0; both bonus-decrement sites write 1 the moment the
; readout reaches 0; state 1 is this routine; state 2 counts bonusExpiredDelay down and
; at 0 goes to state 3; and state 3 waits until Mario is no longer airborne and then
; takes the death exit. As state 1 this does two constant stores -- clear
; bonusExpiredDelay to 0, and advance bonusExpiredStep to 2. State 2 decrements before it
; tests, so that zero wraps to 255 and the delay is 256 frames.
startBonusExpiredDelay:
1A15: AF              XOR     A                   
1A16: 32 87 63        LD      (workRam+387),A     ; clear it; the first tick wraps it to 255
1A19: 3E 02           LD      A,$02               
1A1B: 32 86 63        LD      (workRam+386),A     ; on to the counting step

; The idle arm of the bonus-expired sequence, selected while its step selector holds 0
; -- that is, for as long as the on-screen bonus has not yet counted down to zero. It
; takes no inputs, reads and writes no memory, and returns, so the sequence stays
; dormant on those frames and nothing on screen or in memory changes.
bonusExpiredIdle:
1A1E: C9              RET                         ; bonus still above zero: nothing to do

; The pause in the bonus-expired sequence, which runs once a board's bonus reaches zero.
; Every frame it takes one off bonusExpiredDelay; while that is still non-zero the
; sequence stays parked here, and only on the frame it reaches zero does
; bonusExpiredStep move on. The delay starts at zero, so the first decrement wraps it to
; 255 -- the pause is a full 256-frame roll, not a configured length.
advanceBonusExpiredStepWhenDelayExpires:
1A1F: 21 87 63        LD      HL,$6387            
1A22: 35              DEC     (HL)                ; one frame off the pause
1A23: C0              RET     NZ                  ; still pausing
1A24: 3E 03           LD      A,$03               
1A26: 32 86 63        LD      (workRam+386),A     ; on to the wait-for-landing step
1A29: C9              RET                         

; The wait-and-exit step of the bonus-expired sequence: hold this sub-state until Mario
; has landed, then advance. While marioAirborne is non-zero he is still jumping or
; falling, so this does nothing and the frame's update cascade runs to completion; the
; step waits again next frame. Once it is zero he has landed: step gameSubstate on to the
; next handler, re-arm its 64-frame wait, and report back that the rest of this frame's
; cascade must be abandoned.
advanceSubstateWhenGrounded:
1A2A: 3A 16 62        LD      A,(workRam+216)     ; wait until Mario has landed
1A2D: A7              AND     A                   
1A2E: C0              RET     NZ                  ; still airborne, try again next frame
1A2F: E1              POP     HL                  ; drop the rest of this frame's cascade
1A30: C3 D2 19        JP      advanceSubstateAndArmTimer; take the death exit

; The 100m rivet pickup, and it is a two-frame edge rather than a contact test. Its first
; act is the board gate with the 100m bit, so on the other three boards it does nothing
; at all. If Mario is standing on one of the two screen-edge rivet columns it merely
; raises edgeRivetArmed and stops -- nothing is collected on the frame he is on it. On a
; later frame, once he has stepped off, the latch is disarmed and the rivet removed: a
; 3-bit slot index is built from position bits (two bits of marioY pick the row band,
; with an extra seam case folded into the middle bit, and marioX's high bit picks the
; side); if that slot's rivetPresent flag is already clear it stops, otherwise it clears
; the flag, counts one off rivetsLeft, blanks the rivet's three tilemap cells, and raises
; effectState, effectSelect and itemCollected.
collectEdgeRivet:
1A33: 3E 08           LD      A,$08               ; mask bit 3: the 100m rivet board only
1A35: F7              RST     $30                 ; board gate; nothing to do elsewhere
1A36: 3A 03 62        LD      A,(workRam+203)     ; 
1A39: FE 4B           CP      $4B                 ; the left-edge rivet column
1A3B: CA 4B 1A        JP      Z,armEdgeRivetPickup; standing on it: only arm
1A3E: FE B3           CP      $B3                 ; the right-edge rivet column
1A40: CA 4B 1A        JP      Z,armEdgeRivetPickup; standing on it: only arm
1A43: 3A 91 62        LD      A,(workRam+291)     ; was he on an edge last frame?
1A46: 3D              DEC     A                   
1A47: CA 51 1A        JP      Z,loc_1a51          ; he has stepped off, so collect
1A4A: C9              RET                         

; Raise edgeRivetArmed. This is the "arm" half of a two-pass, set-then-consume latch:
; the edge-item pickup runs every in-game frame, reads Mario's X and, when he is
; standing on one of the two screen-EDGE columns, comes here and raises the flag
; unconditionally -- the edge test itself lives upstream. On a LATER frame, once he is
; no longer on the exact edge, the pickup takes its other path and collects only if the
; flag is up, and the collect handler's first act is to write 0 back. That is where a
; rivet slot is cleared and rivetsLeft decremented, so the latch is a rivet-scoped
; one-shot.
armEdgeRivetPickup:
1A4B: 3E 01           LD      A,$01               
1A4D: 32 91 62        LD      (workRam+291),A     ; arm; nothing is collected this frame
1A50: C9              RET                         

loc_1a51:
1A51: 32 91 62        LD      (workRam+291),A     ; disarm the latch
1A54: 47              LD      B,A                 ; start the slot index at zero
1A55: 3A 05 62        LD      A,(workRam+205)     ; 
1A58: 3D              DEC     A                   
1A59: FE D0           CP      $D0                 ; off the rivet field entirely?
1A5B: D0              RET     NC                  
1A5C: 07              RLCA                        
1A5D: D2 62 1A        JP      NC,loc_1a62         ; 
1A60: CB D0           SET     2,B                 ; top bit of the row band

loc_1a62:
1A62: 07              RLCA                        
1A63: 07              RLCA                        
1A64: D2 69 1A        JP      NC,loc_1a69         ; 
1A67: CB C8           SET     1,B                 ; middle bit of the row band

loc_1a69:
1A69: E6 07           AND     $07                 
1A6B: FE 06           CP      $06                 ; the band seam counts as that bit too
1A6D: C2 72 1A        JP      NZ,loc_1a72         ; 
1A70: CB C8           SET     1,B                 ; so set it as well

loc_1a72:
1A72: 3A 03 62        LD      A,(workRam+203)     ; 
1A75: 07              RLCA                        ; his X high bit picks the side
1A76: D2 7B 1A        JP      NC,loc_1a7b         ; 
1A79: CB C0           SET     0,B                 ; the right-hand half

loc_1a7b:
1A7B: 21 92 62        LD      HL,$6292            ; the eight rivet-present flags
1A7E: 78              LD      A,B                 
1A7F: 85              ADD     A,L                 ; index this slot's flag
1A80: 6F              LD      L,A                 
1A81: 7E              LD      A,(HL)              
1A82: A7              AND     A                   
1A83: C8              RET     Z                   ; that rivet is already gone
1A84: 36 00           LD      (HL),$00            ; clear the rivet's present flag
1A86: 21 90 62        LD      HL,$6290            ; the rivets-left count
1A89: 35              DEC     (HL)                ; one fewer rivet on the board
1A8A: 78              LD      A,B                 
1A8B: 01 05 00        LD      BC,$0005            ; five cells per row band
1A8E: 1F              RRA                         ; the low bit picks the half
1A8F: DA BD 1A        JP      C,loc_1abd          ; 
1A92: 21 CB 02        LD      HL,$02CB            ; the left half's column base

loc_1a95:
1A95: A7              AND     A                   
1A96: CA 9E 1A        JP      Z,loc_1a9e          ; 

loc_1a99:
1A99: 09              ADD     HL,BC               ; step down one band per row
1A9A: 3D              DEC     A                   
1A9B: C2 99 1A        JP      NZ,loc_1a99         ; 

loc_1a9e:
1A9E: 01 00 74        LD      BC,$7400            
1AA1: 09              ADD     HL,BC               ; into the tilemap
1AA2: 3E 10           LD      A,$10               ; the blank tile
1AA4: 77              LD      (HL),A              ; erase the rivet's middle cell
1AA5: 2D              DEC     L                   
1AA6: 77              LD      (HL),A              ; and the cell before it
1AA7: 2C              INC     L                   
1AA8: 2C              INC     L                   
1AA9: 77              LD      (HL),A              ; and the cell after it
1AAA: 3E 01           LD      A,$01               
1AAC: 32 40 63        LD      (workRam+340),A     ; arm the pickup effect
1AAF: 32 42 63        LD      (workRam+342),A     ; the pickup effect kind
1AB2: 32 25 62        LD      (workRam+225),A     ; flag the collection
1AB5: 3A 16 62        LD      A,(workRam+216)     ; 
1AB8: A7              AND     A                   
1AB9: CC 95 1D        CALL    Z,loc_1d95          ; on his feet: run the pickup follow-up
1ABC: C9              RET                         

loc_1abd:
1ABD: 21 2B 01        LD      HL,$012B            ; the right half's column base
1AC0: C3 95 1A        JP      loc_1a95            ; 

; The movement machine's router. It writes no memory of its own: it is five tests in a
; fixed priority order, and the ORDER IS THE MECHANIC -- the first that fires takes the
; frame and nothing below it is consulted.
;   1. marioAirborne set -> the airborne handler. A jump or fall owns the whole frame
;      and no input below is looked at, so a jump cannot be steered onto a ladder.
;   2. marioFreezeTimer non-zero -> the post-landing freeze tick, Mario unresponsive.
;   3. marioHammerActive set -> the ground walk. Note WHERE this enters: above both
;      the ladder and the jump test, so a held hammer cannot climb or jump at all.
;   4. marioOnLadder set -> the climb dispatch, the up/down half of the machine.
;   5. bit 7 of p1Input, the jump press-edge -> the jump launcher.
;   otherwise the ground walk: ordinary walking, and stepping onto a ladder.
; The three flag tests fire on the value 1 alone, not on any non-zero value.
dispatchMarioMovement:
1AC3: 3A 16 62        LD      A,(workRam+216)     ; 
1AC6: 3D              DEC     A                   ; airborne outranks everything
1AC7: CA B2 1B        JP      Z,advanceMarioAirborneFrame; a jump or fall owns the frame
1ACA: 3A 1E 62        LD      A,(workRam+21E)     ; 
1ACD: A7              AND     A                   
1ACE: C2 55 1B        JP      NZ,tickPostLandingFreeze; still frozen from the last landing
1AD1: 3A 17 62        LD      A,(workRam+217)     ; 
1AD4: 3D              DEC     A                   
1AD5: CA E6 1A        JP      Z,walkRightWhileHeld; hammer in hand means walking only
1AD8: 3A 15 62        LD      A,(workRam+215)     ; 
1ADB: 3D              DEC     A                   
1ADC: CA 38 1B        JP      Z,climbDownWhileHeld; on a ladder: the climb dispatch
1ADF: 3A 10 60        LD      A,(workRam+10)      ; 
1AE2: 17              RLA                         ; bit 7, the jump press-edge
1AE3: DA 6E 1B        JP      C,initMarioJump     ; a fresh jump press this frame

; The shared setup of Mario's on-foot movement dispatch, plus its RIGHT arm. Reached on
; any frame Mario is neither airborne, nor frozen, nor mid-jump-press. It asks the
; horizontal position gate for its two-flag verdict, one flag per limit, reads p1Input
; once for every arm downstream, and then spends the frame on a rightward walk step only
; if the right-limit flag is clear AND control bit 0 is held. The limit is tested BEFORE
; the button, so a player leaning on Right at the far edge simply gets no step. Anything
; else falls through to the LEFT arm, which asks the same two questions of bit 1 and then
; falls into the ladder/climb handler -- so movement priority is fixed: right beats left
; beats climb. The left arm recomputes neither the verdict nor the control word; it reads
; what this routine left behind.
walkRightWhileHeld:
1AE6: CD 1F 24        CALL    limitMarioHorizontalTravel; the horizontal limit flags
1AE9: 3A 10 60        LD      A,(workRam+10)      ; read once for every arm below
1AEC: 1D              DEC     E                   ; the right-limit verdict
1AED: CA F5 1A        JP      Z,walkLeftWhileHeld ; at the right limit, so no step
1AF0: CB 47           BIT     0,A                 ; is Right held?
1AF2: C2 8F 1C        JP      NZ,walkMarioRight   ; spend the frame on one step right

; The LEFT arm of Mario's ground-movement direction dispatch, reached every frame the
; movement machine has decided he is on his feet and has already declined the rightward
; step. Two things are handed down from the arm above: the frame's cooked control word
; p1Input (bit 0 right, bit 1 left, bit 2 up, bit 3 down, bit 7 jump press-edge) and the
; LEFT half of the horizontal position gate's verdict. Mario walks one frame left only
; when Left is held AND the gate has not blocked leftward motion -- and that block is not
; only the screen edge; the same verdict fires for an interior wall at the left end of
; the top platform on the odd boards. Any other frame falls THROUGH to the ladder/climb
; handler, where Up and Down are serviced -- which is what keeps a refused frame
; available to the climb path.
walkLeftWhileHeld:
1AF5: 15              DEC     D                   ; the left-limit verdict
1AF6: CA FE 1A        JP      Z,armMarioClimbAtLadderEnd; blocked left, so try the ladder
1AF9: CB 4F           BIT     1,A                 ; is Left held?
1AFB: C2 AB 1C        JP      NZ,walkMarioLeft    ; spend the frame on one step left

; At a ladder end, stamp the ladder-standing pose and this frame's climb-limit pair,
; ARMING a climb the movement code may then perform. It is gated on marioHammerActive:
; unless that differs from 1 nothing happens -- the hammer state only FORBIDS the arm
; here, it never starts one. Two probes come off Mario's position: a grid-aligned X (low
; two bits forced set, the 0x04 bit forced clear) as the search key, and (marioY + 8) as
; both discriminator and climb-limit value. The key is looked up in objParamTable0; a
; miss returns having done nothing. On a hit it stamps marioSpriteCode's low bits to the
; ladder-STANDING pose 0x06, keeping the facing bit -- the same code written when a
; climb ENDS, which is what makes "arms a climb" rather than "drives one" the honest
; reading -- and records whether the match sat among the last few entries scanned. Tag 0
; commits marioClimbLimitA and marioClimbLimitB the ordinary way and drives the
; up-climb; tag 1 commits the SAME pair in the OPPOSITE order and hands the frame to the
; down/up climb dispatch, unless the match was near the scan's end.
armMarioClimbAtLadderEnd:
1AFE: 3A 17 62        LD      A,(workRam+217)     ; 
1B01: 3D              DEC     A                   ; a held hammer forbids arming a climb
1B02: C8              RET     Z                   
1B03: 3A 05 62        LD      A,(workRam+205)     ; 
1B06: C6 08           ADD     A,$08               ; Y+8, the discriminator and climb limit
1B08: 57              LD      D,A                 
1B09: 3A 03 62        LD      A,(workRam+203)     ; 
1B0C: F6 03           OR      $03                 ; grid-align the X search key
1B0E: CB 97           RES     2,A                 
1B10: 01 15 00        LD      BC,$0015            ; at most twenty-one entries to scan
1B13: CD 6E 23        CALL    findOppositeLadderEnd; look the key up in the ladder table
1B16: F5              PUSH    AF                  ; keep which paired slot matched
1B17: 21 07 62        LD      HL,$6207            ; Mario's sprite code
1B1A: 7E              LD      A,(HL)              
1B1B: E6 80           AND     $80                 ; keep only the facing bit
1B1D: F6 06           OR      $06                 ; the ladder-standing pose
1B1F: 77              LD      (HL),A              
1B20: 21 1A 62        LD      HL,$621A            
1B23: 3E 04           LD      A,$04               
1B25: B9              CP      C                   ; did the match sit near the end of the scan?
1B26: 36 01           LD      (HL),$01            
1B28: D2 2C 1B        JP      NC,loc_1b2c         ; 
1B2B: 35              DEC     (HL)                ; no: clear the flag again

loc_1b2c:
1B2C: F1              POP     AF                  
1B2D: A7              AND     A                   
1B2E: CA 4E 1B        JP      Z,loc_1b4e          ; tag 0 commits the limits the ordinary way
1B31: 7E              LD      A,(HL)              
1B32: A7              AND     A                   
1B33: C0              RET     NZ                  ; near the end of the scan: nothing more
1B34: 2C              INC     L                   
1B35: 72              LD      (HL),D              ; commit the pair in the opposite order
1B36: 2C              INC     L                   
1B37: 70              LD      (HL),B              

; The Down half of the ladder-climb input dispatch. It tests bit 3 of p1Input first:
; with Down held it hands the frame to the climb-down driver and stops -- notably
; WITHOUT checking marioOnLadder, so holding Down drives the descent directly. With Down
; clear it falls through to the up-climb guard, which does insist on marioOnLadder and
; climbs only while Up is held. Why the two arms differ on that guard is not settled
; here.
climbDownWhileHeld:
1B38: 3A 10 60        LD      A,(workRam+10)      ; 
1B3B: CB 5F           BIT     3,A                 ; is Down held?
1B3D: C2 F2 1C        JP      NZ,climbMarioDown   ; drive the descent, no on-ladder check
1B40: 3A 15 62        LD      A,(workRam+215)     ; 
1B43: A7              AND     A                   
1B44: C8              RET     Z                   ; the up-climb needs him on a ladder

; The Up half of the ladder-climb input dispatch. It reads p1Input and tests bit 2; if Up
; is held this frame it hands off to the climb-up driver, which paces and advances
; Mario's ladder climb, and otherwise it does nothing. A pure input guard in front of
; that driver -- it writes no memory of its own. The Down half tests bit 3 and falls
; through to here.
climbUpWhileHeld:
1B45: 3A 10 60        LD      A,(workRam+10)      ; 
1B48: CB 57           BIT     2,A                 ; is Up held?
1B4A: C2 03 1D        JP      NZ,climbMarioUp     ; advance the upward climb this frame
1B4D: C9              RET                         

loc_1b4e:
1B4E: 2C              INC     L                   
1B4F: 70              LD      (HL),B              ; commit the first climb limit
1B50: 2C              INC     L                   
1B51: 72              LD      (HL),D              ; and the second
1B52: C3 45 1B        JP      climbUpWhileHeld    ; then climb if Up is held

; Count Mario's post-landing freeze down and unfreeze him when it expires. Landing from a
; jump loads marioFreezeTimer with four, so he is unresponsive for four frames; on each
; of those the routine steps the timer down and, while it is still non-zero, returns at
; once, so Mario stays put. On the frame it reaches zero the one-shot unfreeze runs:
; marioHammerActive takes marioHammerPending, so a hammer touched only during the
; airborne frames is committed now he is settled; marioSpriteCode keeps just its top bit,
; the facing flag, stripping the tile and animation bits so he resumes from a clean
; standing pose; marioWalkAnim is cleared, restarting the walk cycle; and his hardware
; sprite record is refreshed from that cleaned state.
tickPostLandingFreeze:
1B55: 21 1E 62        LD      HL,$621E            ; the post-landing freeze timer
1B58: 35              DEC     (HL)                
1B59: C0              RET     NZ                  ; still frozen, so Mario stays put
1B5A: 3A 18 62        LD      A,(workRam+218)     ; 
1B5D: 32 17 62        LD      (workRam+217),A     ; commit a hammer touched while airborne
1B60: 21 07 62        LD      HL,$6207            ; Mario's sprite code
1B63: 7E              LD      A,(HL)              
1B64: E6 80           AND     $80                 ; keep the facing bit, drop the pose bits
1B66: 77              LD      (HL),A              
1B67: AF              XOR     A                   
1B68: 32 02 62        LD      (workRam+202),A     ; restart the walk cycle
1B6B: C3 A6 1D        JP      writeMarioSpriteRecord; refresh his sprite record

; Begin Mario's jump, on the frame a jump press is accepted. It sets marioAirborne = 1,
; so the movement machine's first per-frame test now routes him through the ballistic
; handler instead of the ground handler, and picks the horizontal launch velocity from
; p1Input: bit 0 Right gives +128, bit 1 Left gives -128, holding neither gives zero --
; a straight-up jump -- and Right wins if both are held, being tested first. The
; velocity is a big-endian 16-bit pair: high byte 0x00 for Right and 0xFF for Left, low
; byte 0x80 in both, and both zero for neither. It then hands that to the launch tail,
; which writes the rest of the airborne record: velocities, cleared fractions and frame
; count, jump pose, take-off Y, jump sound.
initMarioJump:
1B6E: 3E 01           LD      A,$01               
1B70: 32 16 62        LD      (workRam+216),A     ; route him through the ballistic handler
1B73: 21 10 62        LD      HL,$6210            ; the airborne motion record
1B76: 3A 10 60        LD      A,(workRam+10)      ; 
1B79: 01 80 00        LD      BC,$0080            ; +128: a jump to the right
1B7C: 1F              RRA                         ; Right is tested first, so it wins
1B7D: DA 8A 1B        JP      C,launchMarioJump   ; launch with the rightward velocity
1B80: 01 80 FF        LD      BC,$FF80            ; -128: a jump to the left
1B83: 1F              RRA                         ; is Left held?
1B84: DA 8A 1B        JP      C,launchMarioJump   ; 
1B87: 01 00 00        LD      BC,$0000            ; neither held: a straight-up jump

; Commit Mario's ballistic jump. Jump-init has already flagged him airborne and chosen a
; horizontal launch velocity from the held direction -- +128 right, -128 left, 0
; straight up, in 1/256 pixel per frame -- and hands it over. This writes the whole
; airborne state the ballistic integrator then reads every frame: marioAirVxHi/Lo take
; that velocity; marioAirVyHi/Lo take 328, the fixed upward impulse from which gravity
; is later derived; marioAirFrames, marioXFrac and marioYFrac are cleared, so the arc
; starts at frame 0 with no accumulated remainder; marioSpriteCode keeps its facing bit
; and takes jump code 0x0E; marioAirStartY snapshots marioY as the take-off height the
; fall-fatality test measures against; and sndTrigger bit 1 is asserted for three frames
; for the jump sound.
launchMarioJump:
1B8A: AF              XOR     A                   
1B8B: 70              LD      (HL),B              ; stash the horizontal launch velocity, high byte
1B8C: 2C              INC     L                   
1B8D: 71              LD      (HL),C              ; and its low byte
1B8E: 2C              INC     L                   
1B8F: 36 01           LD      (HL),$01            ; the fixed upward impulse, 328, high byte
1B91: 2C              INC     L                   
1B92: 36 48           LD      (HL),$48            ; and its low byte
1B94: 2C              INC     L                   
1B95: 77              LD      (HL),A              ; start the arc at frame zero
1B96: 32 04 62        LD      (workRam+204),A     ; clear the accumulated horizontal remainder
1B99: 32 06 62        LD      (workRam+206),A     ; and the vertical one
1B9C: 3A 07 62        LD      A,(workRam+207)     ; 
1B9F: E6 80           AND     $80                 ; keep only the facing bit
1BA1: F6 0E           OR      $0E                 ; stamp in the jump pose
1BA3: 32 07 62        LD      (workRam+207),A     ; commit the jump sprite
1BA6: 3A 05 62        LD      A,(workRam+205)     ; 
1BA9: 32 0E 62        LD      (workRam+20E),A     ; the take-off height the fall test measures
1BAC: 21 81 60        LD      HL,$6081            
1BAF: 36 03           LD      (HL),$03            ; hold the jump sound three frames
1BB1: C9              RET                         

; The head of every airborne frame -- every frame of a jump or a fall. First it copies
; marioX and marioY into marioAirPrevX / marioAirPrevY, so this frame's STARTING position
; survives the motion below: the collision code reads that pair back to test the swept
; segment rather than only the new point. Then the ballistic step integrates one frame of
; the arc -- X drifts by the horizontal velocity, Y takes the vertical velocity plus the
; ramping gravity term. Then the horizontal travel limit classifies the new position into
; a two-flag verdict, which picks how he is steered: at the left limit -- an interior
; wall on 25m and 75m, not the screen edge -- the horizontal velocity is forced to +0.5
; px/frame and the sprite's facing bit is set; the other arm mirrors that at the
; far-right limit. Both converge on the landing / fatal-fall tail.
advanceMarioAirborneFrame:
1BB2: DD 21 00 62     LD      IX,$6200            ; point at Mario's motion record
1BB6: 3A 03 62        LD      A,(workRam+203)     ; 
1BB9: DD 77 0B        LD      (IX+$0B),A          ; save this frame's starting X for the swept collision test
1BBC: 3A 05 62        LD      A,(workRam+205)     ; 
1BBF: DD 77 0C        LD      (IX+$0C),A          ; and its starting Y
1BC2: CD 9C 23        CALL    stepBallisticMotion ; integrate one frame of the arc
1BC5: CD 1F 24        CALL    limitMarioHorizontalTravel; classify the new X position
1BC8: 15              DEC     D                   ; a verdict of 1 here is the left limit
1BC9: C2 F2 1B        JP      NZ,loc_1bf2         ; not the left limit, try the right one
1BCC: DD 36 10 00     LD      (IX+$10),$00        ; force a drift of +0.5 px/frame back inside
1BD0: DD 36 11 80     LD      (IX+$11),$80        
1BD4: DD CB 07 FE     SET     7,(IX+$07)          ; face him right, the way he is now pushed

; Re-base Mario's vertical arc where he is standing now, so a jump reflects off a
; playfield limit instead of carrying through it. Reached only from the airborne
; handler's two limit arms, which have already stamped a horizontal velocity pushing him
; back inside plus the matching facing bit; this is the vertical half. The ballistic
; integrator stores an arc as a CONSTANT launch velocity plus a count of elapsed frames,
; moving Mario down by (16*frames + 8 - velocity) each frame, so the velocity field is
; not the current speed and cannot simply be negated. Instead marioAirVyHi/Lo becomes
; 16*frames - velocity and marioAirFrames restarts at zero, re-basing the same parabola
; at his present position with its vertical step negated. A fall already latched lethal
; in marioFatalFall is exempt, so a killing fall keeps its arc.
reverseMarioVerticalArc:
1BD8: 3A 20 62        LD      A,(workRam+220)     ; is this fall already condemned?
1BDB: 3D              DEC     A                   
1BDC: CA EC 1B        JP      Z,loc_1bec          ; a killing fall keeps its arc, skip the re-base
1BDF: CD 07 24        CALL    loc_2407            ; reflect the arc: 16*frames minus the velocity
1BE2: DD 74 12        LD      (IX+$12),H          ; re-base the vertical velocity on that reflection
1BE5: DD 75 13        LD      (IX+$13),L          
1BE8: DD 36 14 00     LD      (IX+$14),$00        ; restart the frame count, re-basing the parabola here

loc_1bec:
1BEC: CD 9C 23        CALL    stepBallisticMotion ; step the re-based arc one frame
1BEF: C3 05 1C        JP      loc_1c05            ; 

loc_1bf2:
1BF2: 1D              DEC     E                   ; a verdict of 1 here is the right limit
1BF3: C2 05 1C        JP      NZ,loc_1c05         ; inside both limits, an ordinary airborne frame
1BF6: DD 36 10 FF     LD      (IX+$10),$FF        ; force a drift of -0.5 px/frame back inside
1BFA: DD 36 11 80     LD      (IX+$11),$80        
1BFE: DD CB 07 BE     RES     7,(IX+$07)          ; face him left, the way he is now pushed
1C02: C3 D8 1B        JP      reverseMarioVerticalArc; 

loc_1c05:
1C05: CD 1C 2B        CALL    loc_2b1c            ; probe for a landing under Mario's feet

loc_1c08:
1C08: 3D              DEC     A                   ; a verdict of 1 means he has touched down
1C09: CA 3A 1C        JP      Z,loc_1c3a          ; touched down: go tick the landing counter
1C0C: 3A 1F 62        LD      A,(workRam+21F)     ; 
1C0F: 3D              DEC     A                   ; is the fall-height test already armed?
1C10: CA 76 1C        JP      Z,markFatalFallByHeight; armed: re-run it this frame
1C13: 3A 14 62        LD      A,(workRam+214)     ; 
1C16: D6 14           SUB     $14                 ; how far the arc is from frame 20
1C18: C2 33 1C        JP      NZ,loc_1c33         ; not that frame, take the ordinary airborne tail
1C1B: 3E 01           LD      A,$01               
1C1D: 32 1F 62        LD      (workRam+21F),A     ; arm the fall-height test for this arc
1C20: CD 53 28        CALL    searchPlayerObjectOverlap; one overlap search per arc
1C23: A7              AND     A                   ; how many objects he cleared with that jump, graded 0, 1, 3 or 7
1C24: CA A6 1D        JP      Z,writeMarioSpriteRecord; cleared nothing -- just draw him
1C27: 32 42 63        LD      (workRam+342),A     ; the count picks the score award
1C2A: 3E 01           LD      A,$01               
1C2C: 32 40 63        LD      (workRam+340),A     ; raise the effect that pays it out
1C2F: 32 25 62        LD      (workRam+225),A     ; and flag it; the landing consumes it
1C32: 00              NOP                         

loc_1c33:
1C33: 3C              INC     A                   ; only the frame before the trigger wraps this to zero
1C34: CC 54 29        CALL    Z,latchHammerTouch  ; one hammer test per jump, not per frame
1C37: C3 A6 1D        JP      writeMarioSpriteRecord; 

loc_1c3a:
1C3A: 05              DEC     B                   ; tick the airborne counter
1C3B: CA 4F 1C        JP      Z,settleMarioOnLanding; it reached zero: he has landed
1C3E: 3C              INC     A                   
1C3F: 32 1F 62        LD      (workRam+21F),A     ; arm the land-check phase
1C42: AF              XOR     A                   
1C43: 21 10 62        LD      HL,$6210            ; point at the ballistic block
1C46: 06 05           LD      B,$05               ; five bytes: both velocities and the frame count

loc_1c48:
1C48: 77              LD      (HL),A              ; clear the arc's velocity and frame state
1C49: 2C              INC     L                   
1C4A: 10 FC           DJNZ    loc_1c48            ; 
1C4C: C3 A6 1D        JP      writeMarioSpriteRecord; 

; Settle Mario's state the instant he lands from a jump or fall. It stores the caller's
; landing flag (0 in play) into marioAirborne so the airborne handler stops driving him;
; sets marioActive to marioFatalFall flipped, so a clean landing leaves him alive and a
; lethal one leaves him inert for the death sequence; snaps marioSpriteCode to the
; standing/landing pose while preserving the facing bit; arms marioFreezeTimer to 4
; frames, holding him briefly still on touchdown, and clears marioAirLandcheck. If
; itemCollected is latched to 1 the pickup is committed, which stores the cleared flag
; back and, off 25m, queues the pickup tune. Finally it refreshes marioSpriteRecord from
; his just-settled fields.
settleMarioOnLanding:
1C4F: 32 16 62        LD      (workRam+216),A     ; stop the airborne handler driving him
1C52: 3A 20 62        LD      A,(workRam+220)     ; read back whether the fall was condemned
1C55: EE 01           XOR     $01                 ; invert it: a clean landing leaves him alive
1C57: 32 00 62        LD      (workRam+200),A     ; a lethal one leaves him inert for the death
1C5A: 21 07 62        LD      HL,$6207            
1C5D: 7E              LD      A,(HL)              
1C5E: E6 80           AND     $80                 ; keep the facing bit
1C60: F6 0F           OR      $0F                 ; snap to the standing pose
1C62: 77              LD      (HL),A              
1C63: 3E 04           LD      A,$04               
1C65: 32 1E 62        LD      (workRam+21E),A     ; hold him still four frames on touchdown
1C68: AF              XOR     A                   
1C69: 32 1F 62        LD      (workRam+21F),A     ; the fall-height test is finished with
1C6C: 3A 25 62        LD      A,(workRam+225)     ; was a pickup latched during the jump?
1C6F: 3D              DEC     A                   
1C70: CC 95 1D        CALL    Z,loc_1d95          ; commit it, and off 25m cue the pickup tune
1C73: C3 A6 1D        JP      writeMarioSpriteRecord; 

; Condemn the fall in progress as lethal once Mario has dropped far enough below the
; height he left the ground at. It runs on each airborne frame while the fall-height
; check is armed, comparing marioAirStartY against marioY with 15 pixels of survivable
; slack taken off the current reading. Height grows downward here, so once that
; slack-adjusted reading has reached the take-off height he is 15 or more pixels below
; where he started and the drop is deadly: marioFatalFall is latched -- the landing
; turns that into his death -- and the fall sound cued. A shallower drop leaves both
; alone. Either way it finishes through the movement machine's shared tail, which
; refreshes Mario's sprite record from his position and pose.
markFatalFallByHeight:
1C76: 3A 05 62        LD      A,(workRam+205)     ; 
1C79: 21 0E 62        LD      HL,$620E            ; point at the take-off height
1C7C: D6 0F           SUB     $0F                 ; allow 15 pixels of survivable drop
1C7E: BE              CP      (HL)                ; has he fallen further than that?
1C7F: DA A6 1D        JP      C,writeMarioSpriteRecord; a shallow drop, he survives it
1C82: 3E 01           LD      A,$01               
1C84: 32 20 62        LD      (workRam+220),A     ; latch the fall lethal; the landing kills him
1C87: 21 84 60        LD      HL,$6084            
1C8A: 36 03           LD      (HL),$03            ; cue the falling sound for three frames
1C8C: C3 A6 1D        JP      writeMarioSpriteRecord; 

; The rightward arm of Mario's per-frame walk, reached on a frame Right is held and he is
; on foot. The walk is paced in two tiers by marioMoveStepTimer. While the pacer is still
; running, the frame goes to the horizontal step with a +1 delta: Mario slides one pixel
; right and, on the girder board, his Y re-snaps to the sloped girder under the new X. On
; the frame the pacer expires, marioWalkAnim is pushed one step through a packed
; permutation table keyed by the byte 5 -- the leftward twin keys the same lookup with 1
; -- and the result's low two bits, the walk tile, are committed with bit 7 SET, the
; sprite-code flag that faces Mario right. That commit rings the footstep on the odd tile
; and re-arms the pacer. The cycle is 0 -> 2 -> 4 -> 1 -> 0, giving the repeating tile
; run 2, 0, 1, 0.
walkMarioRight:
1C8F: 06 01           LD      B,$01               ; one pixel per frame, rightward
1C91: 3A 0F 62        LD      A,(workRam+20F)     ; read the walk pacer
1C94: A7              AND     A                   
1C95: C2 D2 1C        JP      NZ,advanceMarioWalkX; step still in progress, just slide him
1C98: 3A 02 62        LD      A,(workRam+202)     ; 
1C9B: 47              LD      B,A                 
1C9C: 3E 05           LD      A,$05               ; key the walk-cycle table for the rightward order
1C9E: CD 09 30        CALL    nextAnimationStep   ; advance the walk cycle one place

loc_1ca1:
1CA1: 32 02 62        LD      (workRam+202),A     ; store the new walk-cycle index
1CA4: E6 03           AND     $03                 ; its low two bits are the walk tile
1CA6: F6 80           OR      $80                 ; set the facing-right bit
1CA8: C3 C2 1C        JP      beginWalkStep       ; 

; Drive one frame of Mario's leftward ground walk -- the mirror of the rightward stepper,
; differing in exactly three constants. It is paced by marioMoveStepTimer. While that is
; still running a step is already in progress, so this frame only shifts Mario one pixel
; further along it: a delta of -1 goes to the walk-X mover, which moves marioX, re-snaps
; marioY to the sloped girder on 25m, ticks the timer down and refreshes the sprite
; record. When the timer has expired the next step begins: marioWalkAnim advances one
; place round its four-value ring -- this selector walks 0, 1, 4, 2 and the rightward
; stepper walks it the other way, so the two directions give the same values reversed --
; and the new index's low two bits become the walk tile. marioSpriteCode's top bit, the
; facing flag, is left CLEAR here: that is what makes this the left arm.
walkMarioLeft:
1CAB: 06 FF           LD      B,$FF               ; one pixel per frame, leftward
1CAD: 3A 0F 62        LD      A,(workRam+20F)     ; read the walk pacer
1CB0: A7              AND     A                   
1CB1: C2 D2 1C        JP      NZ,advanceMarioWalkX; step still in progress, just slide him
1CB4: 3A 02 62        LD      A,(workRam+202)     ; 
1CB7: 47              LD      B,A                 
1CB8: 3E 01           LD      A,$01               ; key the same table for the leftward order
1CBA: CD 09 30        CALL    nextAnimationStep   ; advance the walk cycle one place

loc_1cbd:
1CBD: 32 02 62        LD      (workRam+202),A     ; store the new walk-cycle index
1CC0: E6 03           AND     $03                 ; the walk tile, with the facing bit left clear

; Start a new walk-animation step. The two horizontal walk steppers, one per direction,
; reach here on the frame their sub-step timer expires, handing over the sprite-code
; byte they just built -- the walk-cycle tile in the low bits plus the facing-right flag
; in bit 7. This publishes that byte as marioSpriteCode, rings the footstep sound only
; when the walk-cycle counter's low bit is set, so a step clicks every OTHER step rather
; than every one, re-arms marioMoveStepTimer for the next two 1px shift frames, and
; refreshes marioSpriteRecord.
beginWalkStep:
1CC2: 21 07 62        LD      HL,$6207            
1CC5: 77              LD      (HL),A              ; publish the walk tile and facing bit
1CC6: 1F              RRA                         ; test the walk counter's low bit
1CC7: DC 8F 1D        CALL    C,triggerWalkSound  ; a footstep every other step, not each one
1CCA: 3E 02           LD      A,$02               
1CCC: 32 0F 62        LD      (workRam+20F),A     ; re-arm the pacer for two 1px slide frames
1CCF: C3 A6 1D        JP      writeMarioSpriteRecord; 

; Advance Mario one pixel along a horizontal walk step. The signed delta handed in by
; the walk stepper -- 1 moving right, 255 (that is, -1) moving left, 0 for a held frame
; -- is added to marioX as a byte, wrapping the way the hardware does. On 25m ONLY,
; marioY is then re-snapped to the sloped girder now under his new X, stepping one unit
; as that X crosses a girder cell edge; on the conveyor, elevator and rivet boards Y is
; left as it was. Both paths tail into the walk-step continuation, which spends one
; frame of the move and refreshes his sprite record.
advanceMarioWalkX:
1CD2: 21 03 62        LD      HL,$6203            
1CD5: 7E              LD      A,(HL)              
1CD6: 80              ADD     A,B                 ; apply the signed one-pixel step
1CD7: 77              LD      (HL),A              ; commit his new column
1CD8: 3A 27 62        LD      A,(workRam+227)     ; 
1CDB: 3D              DEC     A                   
1CDC: C2 EB 1C        JP      NZ,continueWalkStep ; off 25m the girders are flat: leave Y be
1CDF: 66              LD      H,(HL)              
1CE0: 3A 05 62        LD      A,(workRam+205)     ; 
1CE3: 6F              LD      L,A                 
1CE4: CD 33 23        CALL    snapYToGirder       ; re-snap him to the slope under the new X

loc_1ce7:
1CE7: 7D              LD      A,L                 
1CE8: 32 05 62        LD      (workRam+205),A     ; commit the slope-corrected row

; Carry an in-progress walk step one frame further. Reached as the tail of the shared X
; advance while Mario's sub-step timer is still running: knock marioMoveStepTimer down by
; one, then refresh his four sprite-record bytes. When the timer reaches zero on a later
; frame the walk advances to its next animation frame and re-arms the timer instead of
; coming here.
continueWalkStep:
1CEB: 21 0F 62        LD      HL,$620F            
1CEE: 35              DEC     (HL)                ; spend one frame of the step in progress
1CEF: C3 A6 1D        JP      writeMarioSpriteRecord; 

; Per-frame driver for Mario's downward ladder climb, pacing the descent one animation
; sub-step at a time. While marioMoveStepTimer is still running it just knocks the pacer
; down by one and holds the current sub-step for this frame. On the frame the pacer
; reaches 0 it reloads it to the climb pace of 3 frames and advances one sub-step
; downward -- a +2 nudge to Mario's Y -- through the shared climb stepper. The upward
; twin feeds that same stepper a -2 step at the walk/climb reload; the fixed +2 and the
; reload of 3 are the whole of what makes this the DOWN direction.
climbMarioDown:
1CF2: 3A 0F 62        LD      A,(workRam+20F)     ; read the climb pacer
1CF5: A7              AND     A                   
1CF6: C2 8A 1D        JP      NZ,tickMoveStepTimer; mid sub-step: hold the pose this frame
1CF9: 3E 03           LD      A,$03               
1CFB: 32 0F 62        LD      (workRam+20F),A     ; down-climb pace: three frames a sub-step
1CFE: 3E 02           LD      A,$02               ; step of +2: two pixels DOWN the screen
1D00: C3 11 1D        JP      advanceClimbStep    ; 

; Drive Mario's upward climb one animation step per frame, paced by marioMoveStepTimer
; so the animation advances every few frames rather than every frame. While the timer is
; still running it hands off to the hold step, which decides whether to tick it down
; this frame, holding Mario between animation sub-steps without moving him. On expiry it
; reloads the timer to 4 and advances the climb one step UP: the shared climb stepper is
; handed a step of -2, so marioY decreases by two -- two pixels up the screen. The -2 is
; what makes this the climb-UP driver; the mirror-image climb-DOWN driver hands the same
; stepper +2.
climbMarioUp:
1D03: 3A 0F 62        LD      A,(workRam+20F)     ; read the climb pacer
1D06: A7              AND     A                   
1D07: C2 76 1D        JP      NZ,loc_1d76         ; mid sub-step: decide whether to tick the pacer
1D0A: 3E 04           LD      A,$04               
1D0C: 32 0F 62        LD      (workRam+20F),A     ; up-climb pace: four frames a sub-step
1D0F: 3E FE           LD      A,$FE               ; step of -2: two pixels UP the screen

; One climb-animation step, and the shared body both climb steppers fall into -- the up
; stepper enters with a -2 step, the down stepper with +2, so the per-frame vertical
; step is the caller's. It nudges marioY by that step, then flips a two-phase
; ladder-centring counter. On the phase that flips it non-zero it finalises through the
; centring path: snap Mario onto the ladder column, tick the footstep, commit his
; sprite. On the other phase it decides the outcome from where the new height sits
; against marioClimbLimitA and marioClimbLimitB, measured in (Y + 8) units: at either
; limit the climb has reached a ladder end and dismounts; otherwise it picks the climb
; sprite frame by how far above the near limit he sits -- 8 units up is frame 5, 12 up
; is frame 4, anything else frame 3.
advanceClimbStep:
1D11: 21 05 62        LD      HL,$6205            
1D14: 86              ADD     A,(HL)              
1D15: 77              LD      (HL),A              ; move him the caller's two pixels along the ladder
1D16: 47              LD      B,A                 
1D17: 3A 22 62        LD      A,(workRam+222)     ; 
1D1A: EE 01           XOR     $01                 ; flip the climb's two-phase counter
1D1C: 32 22 62        LD      (workRam+222),A     ; 
1D1F: C2 51 1D        JP      NZ,centerMarioAndCommitClimbStep; the step's centring phase
1D22: 78              LD      A,B                 
1D23: C6 08           ADD     A,$08               ; measure the new height in (Y+8) units
1D25: 21 1C 62        LD      HL,$621C            ; point at the far climb limit
1D28: BE              CP      (HL)                ; reached the end of the ladder?
1D29: CA 67 1D        JP      Z,endClimbAtLadderLimit; yes: dismount
1D2C: 2D              DEC     L                   
1D2D: 96              SUB     (HL)                ; how far he sits above the near limit
1D2E: CA 67 1D        JP      Z,endClimbAtLadderLimit; sitting on it: dismount
1D31: 06 05           LD      B,$05               ; climb frame 5
1D33: D6 08           SUB     $08                 ; exactly 8 units up?
1D35: CA 3F 1D        JP      Z,setClimbSpriteFrame; 
1D38: 05              DEC     B                   ; climb frame 4
1D39: D6 04           SUB     $04                 ; 12 units up?
1D3B: CA 3F 1D        JP      Z,setClimbSpriteFrame; 
1D3E: 05              DEC     B                   ; otherwise climb frame 3

; Stamp Mario's climb-animation sprite for one climb step. marioSpriteCode packs his
; horizontal-mirror flag in the top bit and an animation code in the low bits; this
; toggles the mirror flag -- the left/right leg wiggle that reads as climbing -- while
; dropping the previous step's code, and stamps in the new climb frame the caller chose.
; The climb codes are 3, 4 and 5, cycled by how far up the ladder he has got. It then
; falls into the shared climb tail, which re-asserts the on-ladder flag and copies his
; position and sprite code into his sprite record.
setClimbSpriteFrame:
1D3F: 3E 80           LD      A,$80               
1D41: 21 07 62        LD      HL,$6207            
1D44: A6              AND     (HL)                ; isolate the sprite mirror bit
1D45: EE 80           XOR     $80                 ; toggle it: the leg wiggle that reads as climbing
1D47: B0              OR      B                   ; stamp in the climb frame the caller chose
1D48: 77              LD      (HL),A              

; The tail of a climb step: set marioOnLadder := 1, re-asserted on every climb step and
; cleared back to 0 only by the ladder-end handler, then copy Mario's just-computed
; position and sprite code into his four-byte hardware sprite record. Reached from the
; arm that has just rewritten his facing/climb sprite code and from the climb-sound arm.
; The record write is a tail hand-off, so its return is this routine's return.
markOnLadderAndCommitSprite:
1D49: 3E 01           LD      A,$01               
1D4B: 32 15 62        LD      (workRam+215),A     ; on a ladder; only the dismount clears this
1D4E: C3 A6 1D        JP      writeMarioSpriteRecord; 

; The ladder-centering phase of a climb step. It snaps marioX onto the ladder column by
; forcing the low three bits to 3: ladders sit on an 8-pixel grid, so this re-glues
; Mario to the column each centering step and corrects horizontal drift left over from
; walking before he mounted. It then toggles marioClimbSoundToggle and, on the frame it
; flips to 0, requests the footstep sound, so a step blips every other frame. Finally it
; re-asserts marioOnLadder and copies his updated position and sprite code into
; marioSpriteRecord, which is what picks up the just-centred X.
centerMarioAndCommitClimbStep:
1D51: 2D              DEC     L                   ; back up to Mario's column
1D52: 2D              DEC     L                   
1D53: 7E              LD      A,(HL)              
1D54: F6 03           OR      $03                 ; force the low bits to 3: the ladder's 8px grid
1D56: CB 97           RES     2,A                 
1D58: 77              LD      (HL),A              
1D59: 3A 24 62        LD      A,(workRam+224)     ; 
1D5C: EE 01           XOR     $01                 ; flip the climb-step toggle
1D5E: 32 24 62        LD      (workRam+224),A     ; 
1D61: CC 8F 1D        CALL    Z,triggerWalkSound  ; a footstep every other centring step
1D64: C3 49 1D        JP      markOnLadderAndCommitSprite; 

; Finish a ladder climb that has reached either end -- he has climbed onto the girder at
; the top, or stepped off at the bottom. Three fixed stores and a sprite refresh,
; reading nothing at all: marioSpriteCode takes the ladder-end pose written FLAT, the
; facing bit deliberately dropped because at a ladder end Mario faces front; the climb
; half-step toggle is cleared; and marioOnLadder is cleared, which is the one bit
; distinguishing a dismount from a climb that continues. The shared refresh then copies
; his X, code, attribute and Y into his hardware sprite record, which is how the pose
; just stored reaches the screen.
endClimbAtLadderLimit:
1D67: 3E 06           LD      A,$06               ; the ladder-end pose, written flat: he faces front
1D69: 32 07 62        LD      (workRam+207),A     ; 
1D6C: AF              XOR     A                   
1D6D: 32 19 62        LD      (workRam+219),A     ; clear the climb half-step toggle
1D70: 32 15 62        LD      (workRam+215),A     ; he is off the ladder now
1D73: C3 A6 1D        JP      writeMarioSpriteRecord; 

loc_1d76:
1D76: 3A 1A 62        LD      A,(workRam+21A)     ; read the climb gate flag
1D79: A7              AND     A                   
1D7A: CA 8A 1D        JP      Z,tickMoveStepTimer ; flag clear: just tick the pacer down
1D7D: 32 19 62        LD      (workRam+219),A     ; mirror it into the neighbouring byte
1D80: 3A 1C 62        LD      A,(workRam+21C)     ; 
1D83: D6 13           SUB     $13                 ; take 19 off the far climb limit
1D85: 21 05 62        LD      HL,$6205            
1D88: BE              CP      (HL)                ; compare that against his row
1D89: D0              RET     NC                  ; still short of it: hold the pacer where it is

; Knock marioMoveStepTimer down by one, and that is all it does. It is the pacer that
; decides how long Mario holds each frame of his walking and climbing animation: while
; the timer is above zero the mover keeps showing its current sub-step, and on the frame
; it reaches zero the stepper reloads it and moves on to the next. Nothing here reloads
; it and nothing branches on the result -- the expiry decision is made later, by reading
; the timer back. It wraps if it was already zero.
tickMoveStepTimer:
1D8A: 21 0F 62        LD      HL,$620F            
1D8D: 35              DEC     (HL)                ; spend one frame of the current walk or climb sub-step
1D8E: C9              RET                         

; Ask for Mario's footstep sound. The walk sound is a discrete analog circuit behind a
; write-only latch, so game code asks for it through a work-RAM shadow instead: it stores
; a small frame count into the walk slot of sndTrigger and the per-vblank sound driver
; counts that value down, holding the latch asserted while it stays non-zero. This leaf
; stores 3 -- a three-frame hold, one short footstep blip. Whether a footstep happens at
; all was decided before the call.
triggerWalkSound:
1D8F: 3E 03           LD      A,$03               
1D91: 32 80 60        LD      (workRam+80),A      ; hold the footstep latch three frames
1D94: C9              RET                         

loc_1d95:
1D95: 32 25 62        LD      (workRam+225),A     ; commit the collection flag; callers pass 0
1D98: 3A 27 62        LD      A,(workRam+227)     ; 
1D9B: 3D              DEC     A                   ; which board is this?
1D9C: C8              RET     Z                   ; 25m plays no pickup sound
1D9D: 21 8A 60        LD      HL,$608A            
1DA0: 36 0D           LD      (HL),$0D            ; queue the pickup tune
1DA2: 2C              INC     L                   
1DA3: 36 03           LD      (HL),$03            ; for three frames, over the background music
1DA5: C9              RET                         

; The convergence tail of the movement machine -- grounded, airborne, climbing, hammer
; and landed paths all finish here, so this is the single place Mario's just-computed
; state reaches the sprite shadow buffer. It fills marioSpriteRecord in the RECORD's
; field order: +0 <- marioX, +1 <- marioSpriteCode (tile plus the facing bit 7), +2 <-
; marioSpriteAttr, +3 <- marioY. That is not the order the sources sit at in memory --
; marioY comes before the sprite code and attribute -- so the reads are deliberately out
; of source order. A leaf.
writeMarioSpriteRecord:
1DA6: 21 4C 69        LD      HL,$694C            ; point at Mario's hardware sprite record
1DA9: 3A 03 62        LD      A,(workRam+203)     ; 
1DAC: 77              LD      (HL),A              ; sprite X from his column
1DAD: 3A 07 62        LD      A,(workRam+207)     ; 
1DB0: 2C              INC     L                   
1DB1: 77              LD      (HL),A              ; sprite code: tile plus the facing bit
1DB2: 3A 08 62        LD      A,(workRam+208)     ; 
1DB5: 2C              INC     L                   
1DB6: 77              LD      (HL),A              ; his colour attribute
1DB7: 3A 05 62        LD      A,(workRam+205)     ; 
1DBA: 2C              INC     L                   
1DBB: 77              LD      (HL),A              ; sprite Y from his row
1DBC: C9              RET                         

; The router for the score-effect state machine held in effectState, writing nothing of
; its own -- pure control flow, a four-way branch on a small enum.
;   0 -- idle; the machine is dormant and nothing happens this frame.
;   1 -- the one-shot: arm effectTimer, spawn the effect sprite, advance the state to 2.
;   2 -- the countdown: work effectTimer down and, on expiry, tear the effect down and
;        return the machine to 0.
;   3 -- the machine's cold-start address, a defensive slot. No handler ever writes
;        it, so play never produces it; reaching it would restart the machine.
; The state byte only ever holds 0, 1 or 2 in play. What the effect DEPICTS is not
; established here -- the name is taken from the selector cell.
dispatchEffectState:
1DBD: 3A 40 63        LD      A,(workRam+340)     ; read the effect machine's state
1DC0: EF              RST     $28                 ; branch through the four-slot table below

; ---- $1DC1-$1DC8: jump table ----
1DC1: 49 1E C9 1D 4A 1E 00 00

; The arm step of the score-popup machine -- the small points figure that appears on
; screen when the player earns an award. Two writes happen first and on EVERY path,
; before any choice is made: effectTimer is loaded with the count the display state will
; work back down, and effectState is stepped on to that display state, so entering here
; always commits the popup. Then exactly one award value is staged, by handing control to
; one of five setters chosen by the FIRST set bit of effectSelect's low three bits. Bit 0
; derives the tier from the remaining select bits; bit 1 is a fixed award; bit 2 draws
; its tier from random, so that award varies from event to event. With no bit set an
; award sound is cued and the tier comes from level -- level 1 the low award, level 2 the
; middle, every other level the high one.
armScorePopupAndSelectAward:
1DC9: 3E 40           LD      A,$40               
1DCB: 32 41 63        LD      (workRam+341),A     ; arm the 64-frame hold of the display state
1DCE: 3E 02           LD      A,$02               
1DD0: 32 40 63        LD      (workRam+340),A     ; step the machine on to that display state
1DD3: 3A 42 63        LD      A,(workRam+342)     ; read the award selector
1DD6: 1F              RRA                         
1DD7: DA 70 3E        JP      C,pickAwardTierByObjectCount; bit 0: tier from the other bits
1DDA: 1F              RRA                         
1DDB: DA 00 1E        JP      C,stageAward300Popup; bit 1: the fixed 300 award
1DDE: 1F              RRA                         
1DDF: DA F5 1D        JP      C,pickRandomAwardTier; bit 2: draw the tier from random
1DE2: 21 85 60        LD      HL,$6085            
1DE5: 36 03           LD      (HL),$03            ; no bit set: cue the award sound
1DE7: 3A 29 62        LD      A,(workRam+229)     ; and take the tier from the level instead
1DEA: 3D              DEC     A                   
1DEB: CA 00 1E        JP      Z,stageAward300Popup; level 1 awards 300
1DEE: 3D              DEC     A                   
1DEF: CA 08 1E        JP      Z,stageAward500Popup; level 2 awards 500
1DF2: C3 10 1E        JP      stageAward800Popup  ; every later level awards 800

; Pick one of three score-popup tiers from two bits of random -- the random tail of the
; effect machine's arm-timer state. Bit 0 of random set selects the 500 tier; bit 0 clear
; with bit 1 set, the 800 tier; both clear, the 300 tier. The three sibling setters
; differ ONLY in the fixed pair each stages -- a sprite code and a deferred-task message
; -- before delegating to the shared popup feeder. This routine reads random and writes
; no memory of its own.
pickRandomAwardTier:
1DF5: 3A 18 60        LD      A,(workRam+18)      ; read the rolling random byte
1DF8: 1F              RRA                         
1DF9: DA 08 1E        JP      C,stageAward500Popup; bit 0 set: the 500 tier
1DFC: 1F              RRA                         
1DFD: DA 10 1E        JP      C,stageAward800Popup; bit 1 instead: the 800 tier

; One of three sibling setters the award draw picks between on two bits of the rolling
; random byte; this is the arm both bits clear select. Its whole contribution is two
; constants -- sprite code 0x7D, stamped into the effect record's code byte, and the
; deferred-task message opcode 0 with argument 3 -- after which the shared handler does
; every observable thing: post the task and stamp the effect sprite's record. What the
; sprite code draws, and what the posted task does with its argument, are not
; established here.
stageAward300Popup:
1E00: 06 7D           LD      B,$7D               ; the 300 glyph tile
1E02: 11 03 00        LD      DE,$0003            ; score-add task, payload 3
1E05: C3 15 1E        JP      stageAwardPopupAtHitObject; 

; The middle of three sibling award setters, each staging its own two constants before
; the shared effect handler runs. This one stages sprite code 0x7E and the task message
; (opcode 0, argument 5) -- opcode 0 is the score-add task and payload 5 is 500 points.
; The sprite code is stamped into the effect record by the handler's tail and the
; message posted onto the task ring; every memory write happens downstream. Staging the
; two constants is all this routine does.
stageAward500Popup:
1E08: 06 7E           LD      B,$7E               ; the 500 glyph tile
1E0A: 11 05 00        LD      DE,$0005            ; score-add task, payload 5
1E0D: C3 15 1E        JP      stageAwardPopupAtHitObject; 

; The 800-point effect-sprite setter: load this award's fixed sprite code and its
; deferred task message (opcode 0, argument 8), then hand off to the shared award-popup
; feeder, which posts the task, reads the sprite's X and Y out of the effect parameter
; block, and stamps the 4-byte sprite record. The third of three constant setters in the
; same family -- the 300, 500 and 800 popups -- each loading its own pair and jumping
; into the same feeder. It reads nothing: it overwrites both parameters with constants
; whatever they held on entry.
stageAward800Popup:
1E10: 06 7F           LD      B,$7F               ; the 800 glyph tile
1E12: 11 08 00        LD      DE,$0008            ; score-add task, payload 8

; Where the fixed-value award setters converge: post the caller's deferred message, take
; the score popup's screen position out of a parameter block, and hand both on to the
; sprite stamp. The message goes onto the task ring fire-and-forget, and both it and the
; sprite code survive the post untouched. A pointer is then loaded from effectParamPtr;
; the popup's X is read from the block's first byte and that byte is CLEARED in place --
; a consume-once read, so the same block cannot place a second popup -- and the Y from
; the block's fourth byte. Control then goes to the sprite stamp, which turns the
; position and the sprite code into the popup's hardware sprite record and cues the
; accompanying sound.
stageAwardPopupAtHitObject:
1E15: CD 9F 30        CALL    enqueueTask         ; post the score-add onto the task ring
1E18: 2A 43 63        LD      HL,(workRam+343)    ; point at the effect's parameter block
1E1B: 7E              LD      A,(HL)              ; the popup's X
1E1C: 36 00           LD      (HL),$00            ; consume it, so the block cannot place a second popup
1E1E: 2C              INC     L                   
1E1F: 2C              INC     L                   
1E20: 2C              INC     L                   
1E21: 4E              LD      C,(HL)              ; the popup's Y, from the block's fourth byte
1E22: C3 36 1E        JP      stampScorePopupSprite; 

; ---- $1E25-$1E27: data ----
1E25: 11 01 00

; Award points and stage the floating score glyph over Mario. It is entered with a
; MATCHED PAIR already chosen -- an award-table index and the glyph tile depicting the
; same amount -- so a bigger award always brings the bigger number sprite, which is what
; makes this a score POPUP and not a bare award. Three acts: enqueue a task carrying the
; opcode/index pair, so the score is credited later off the ring rather than here; write
; a 4-byte sprite record (X = marioX, code = the glyph tile, attribute 7, Y = marioY +
; 0x14) so the number appears just below Mario in his own column; and consult the board
; gate with a mask selecting 25m and 75m -- on those two, sndTrigger slot 5 is held for
; three frames, on the other two it returns silent.
awardScorePopup:
1E28: CD 9F 30        CALL    enqueueTask         ; post the score-add for the matched award
1E2B: 3A 05 62        LD      A,(workRam+205)     ; 
1E2E: C6 14           ADD     A,$14               ; place the glyph 20 pixels below Mario
1E30: 4F              LD      C,A                 
1E31: 3A 03 62        LD      A,(workRam+203)     ; and in his own column
1E34: 00              NOP                         
1E35: 00              NOP                         

; The shared tail of the effect machine: commit one 4-byte record into popupSprite from
; three caller-supplied bytes plus a hard-coded attribute -- +0 the X byte, +1 the
; sprite code, +2 a fixed 0x07, +3 the Y byte -- then assert a sound latch, but ONLY on
; the boards whose bit is set in the mask 0x05, that is 25m and 75m. On 50m and 100m the
; cue is simply not issued; the four record bytes are stored unconditionally on every
; board. popupSprite is a transient slot -- the companion state of the same machine
; blanks it again once the hold expires -- so what this stamps is on screen for a
; bounded stretch and then gone. Which sound the latch plays, and why only two of the
; four boards cue it, are not established here.
stampScorePopupSprite:
1E36: 21 30 6A        LD      HL,$6A30            ; point at the popup's sprite record
1E39: 77              LD      (HL),A              ; its X
1E3A: 2C              INC     L                   
1E3B: 70              LD      (HL),B              ; the number glyph's tile
1E3C: 2C              INC     L                   
1E3D: 36 07           LD      (HL),$07            ; fixed colour attribute
1E3F: 2C              INC     L                   
1E40: 71              LD      (HL),C              ; its Y
1E41: 3E 05           LD      A,$05               ; board mask: 25m and 75m
1E43: F7              RST     $30                 ; on 50m and 100m the cue below is skipped
1E44: 21 85 60        LD      HL,$6085            
1E47: 36 03           LD      (HL),$03            ; hold the award sound three frames

; The idle arm of the effect router, taken while effectState is 0: it does nothing at
; all. No inputs, no memory read or written, no branches -- the effect simply does not
; advance on such a frame. What the effect depicts is not established here.
effectStateIdle:
1E49: C9              RET                         ; the machine is dormant; nothing happens this frame

; The effect machine's state-2 arm: hold the effect on screen, then reset the machine.
; effectState is a four-way router -- 0 idle, 1 arm-timer, 2 this countdown, 3 reset. The
; arm-timer state loads effectTimer with 64 and advances the router to 2; this then
; decrements effectTimer once a frame and, while it is still non-zero, does nothing else.
; On the frame it reaches 0 the hold ends: clear popupSprite and drop effectState back to
; 0. A full hold is exactly 64 dispatches.
tickDispatcherCountdown:
1E4A: 21 41 63        LD      HL,$6341            
1E4D: 35              DEC     (HL)                ; work the 64-frame hold down
1E4E: C0              RET     NZ                  ; still holding, leave the effect on screen
1E4F: AF              XOR     A                   
1E50: 32 30 6A        LD      (workRam+A30),A     ; blank the popup sprite
1E53: 32 40 63        LD      (workRam+340),A     ; and return the machine to idle
1E56: C9              RET                         

; Mario's per-frame board-won check, routed by board, which defers to the arm that
; stamps the win on the frame the condition is met. On 100m the win is the rivet count,
; not his position, so that arm defers entirely and his coordinates are never even read.
; On the ODD boards -- 25m AND 75m together, since the selector is board bit 0, not 25m
; alone -- the win is positional, tested against marioY at the rescue row near Pauline.
; On the remaining board, 50m, it is won once he has climbed above a fixed line; screen
; Y DECREASES as he climbs, so "above" means a Y below 0x51, and at or below it nothing
; changes this frame. On that won arm Mario's X high bit picks the sprite facing he is
; left standing in. The return is a protocol: true means the board is not won and the
; movement cascade continues, false means it was won and the cascade has already
; unwound, so the caller must NOT continue.
checkBoardWonByType:
1E57: 3A 27 62        LD      A,(workRam+227)     ; read the board being played
1E5A: CB 57           BIT     2,A                 ; board 4, where the win is the rivets
1E5C: C2 80 1E        JP      NZ,completeRivetBoardWhenCleared; his position is never read
1E5F: 1F              RRA                         ; board bit 0 picks the odd boards, 25m and 75m
1E60: 3A 05 62        LD      A,(workRam+205)     ; 
1E63: DA 7A 1E        JP      C,completeBoardWhenMarioReachesRescueRow; 25m and 75m
1E66: FE 51           CP      $51                 ; 50m: has he climbed above the win line?
1E68: D0              RET     NC                  ; not yet; the movement cascade carries on
1E69: 3A 03 62        LD      A,(workRam+203)     ; his X's high bit picks the facing he is left in
1E6C: 17              RLA                         

loc_1e6d:
1E6D: 3E 00           LD      A,$00               
1E6F: DA 74 1E        JP      C,loc_1e74          ; 
1E72: 3E 80           LD      A,$80               

loc_1e74:
1E74: 32 4D 69        LD      (workRam+94D),A     ; stamp the facing he is left standing in
1E77: C3 85 1E        JP      enterBoardAdvanceAndUnwind; 

; The rescue-row test inside Mario's per-frame position check, and it serves 25m AND 75m
; -- not the girder board alone. The dispatcher above peels 100m off to the rivet arm
; first, then routes on the LOW BIT of board, which selects the two ODD boards; 50m
; falls through to a different threshold test entirely. It is reached once the earlier
; column and row checks have narrowed Mario to the situation near Pauline, and it is
; handed his screen Y. Y DECREASES as he climbs and 0x31 is the rescue-row line: at or
; below that line nothing changes this frame; above it the board is WON, and control
; falls into the board-won tail, which stamps Mario's sprite facing, commits the
; board-advance sub-state, and unwinds out of the movement cascade so nothing else runs
; that frame.
completeBoardWhenMarioReachesRescueRow:
1E7A: FE 31           CP      $31                 ; is he above the rescue row near Pauline?
1E7C: D0              RET     NC                  ; not yet; carry on with the frame
1E7D: C3 6D 1E        JP      loc_1e6d            ; 

; On the rivet board, win the board the frame its last rivet is gone. Reached only while
; 100m is the board being played, from Mario's per-frame position check. It reads
; rivetsLeft: while any remain the board is not won, so it reports "carry on" and the
; movement cascade continues normally; on the frame the count reaches zero it hands off
; to the board-advance step, which commits the board-cleared sub-state and unwinds out of
; the cascade. It writes nothing of its own -- the rivet count is only read.
completeRivetBoardWhenCleared:
1E80: 3A 90 62        LD      A,(workRam+290)     ; how many rivets are still standing?
1E83: A7              AND     A                   
1E84: C0              RET     NZ                  ; any left and the board is not won

; Commit "this board is complete". One store -- gameSubstate := the board-cleared/advance
; sub-state, which plays the board-advance interlude and steps on to the next board --
; and then an unwind that returns one extra level up, straight out of the movement
; cascade, so no further movement is processed on the frame the board is won. Reached
; from either board-clear condition: a rivet board whose last rivet has been collected,
; or any non-rivet board where Mario has climbed to the rescue row near Pauline.
enterBoardAdvanceAndUnwind:
1E85: 3E 16           LD      A,$16               
1E87: 32 0A 60        LD      (workRam+A),A       ; enter the board-cleared sub-state
1E8A: E1              POP     HL                  ; drop a level: abandon the rest of the movement cascade
1E8B: C9              RET                         

; The per-frame gate on the hit-effect latch, called near the head of the shared update
; cascade. With the latch clear the caller runs its ordinary frame -- the long run of
; updates starting with the movement machine. With it set the frame is spent on ONE beat
; of the effect animation instead, and the caller is told to abandon everything else, so
; the latch suspends gameplay for the effect's duration and play resumes on the frame
; the effect tears itself down. ANY nonzero value counts as set: this is a plain zero
; test, not a bit test and not a comparison against 1, and the distinction is real
; because the cell's other reader tests only bit 0. The latch is raised when the swung
; hammer strikes a hazard, by a routine that runs LATER in the same cascade -- so a hit
; recorded on one frame first suspends play on the NEXT -- and it is cleared on the
; effect sequence's teardown beat, the only way this gate reopens.
runHitEffectInsteadOfPlay:
1E8C: 3A 50 63        LD      A,(workRam+350)     ; read the hit-effect latch
1E8F: A7              AND     A                   
1E90: C8              RET     Z                   ; clear: let the caller run its ordinary frame
1E91: CD 96 1E        CALL    dispatchEffectSequenceStep; set: spend the frame on it

loc_1e94:
1E94: E1              POP     HL                  ; and make the caller abandon the rest of its update
1E95: C9              RET                         

; The router for the effect-sequence step machine in effectSeqState, run once a frame
; while the effect is armed. It hands the frame to that step's handler and writes
; nothing itself.
;   0 -- the one-shot that consumes the collision record, builds the effect sprite,
;        fires its priority sound, and advances to 1.
;   1 -- the two-stage rate divider that flashes the sprite's tile between two codes
;        and, after four beats, advances to 2.
;   2 -- the divider that marches the tile forward and, when it runs out, resets to 0
;        and re-arms the parent effect machine.
; The step index is doubled at 8 bits to reach a two-byte table slot, so 128, 129 and
; 130 alias steps 0, 1 and 2 -- unreachable in play, since the counters only produce
; 0-2.
dispatchEffectSequenceStep:
1E96: 3A 45 63        LD      A,(workRam+345)     ; read the effect sequence's step
1E99: EF              RST     $28                 ; branch through the three-slot table below

; ---- $1E9A-$1E9F: jump table ----
1E9A: A0 1E 09 1F 23 1F

; Step 0 of the effect sequence: spawn the hit-effect sprite from the record the board
; collision search left behind -- collidedObjectBase, collidedObjectStride and
; collidedObjectIndex. Four acts. Classify the array by the high byte of its base, i.e.
; by which page it lives on, and pick the matching group of source sprite records. Walk
; both records out to the hit index, the object by its own stride and the sprite by the
; fixed 4-byte one. Deactivate the object that was hit and read its +0x15 field to pick
; the effect variant into effectSelect -- zero selects 2, anything else 4. Then build
; effectSprite out of the source record, taking its +0 and +3 fields and blanking +0 in
; place so the source sprite stops being drawn, stamp the fixed effect tile and
; attribute, advance the sequence, reload its inner and outer counters, and fire the
; priority sound for three frames.
buildEffectSprite:
1EA0: 3A 52 63        LD      A,(workRam+352)     ; which page the hit object's array lives on
1EA3: FE 65           CP      $65                 
1EA5: 21 B8 69        LD      HL,$69B8            ; the page-0x65 array's sprite group
1EA8: CA B4 1E        JP      Z,loc_1eb4          ; 
1EAB: 21 D0 69        LD      HL,$69D0            ; a lower page's sprite group
1EAE: DA B4 1E        JP      C,loc_1eb4          ; 
1EB1: 21 80 69        LD      HL,$6980            ; a higher page: the barrel sprites

loc_1eb4:
1EB4: DD 2A 51 63     LD      IX,(workRam+351)    ; the hit object's array base
1EB8: 16 00           LD      D,$00               
1EBA: 3A 53 63        LD      A,(workRam+353)     ; that array's record stride
1EBD: 5F              LD      E,A                 
1EBE: 01 04 00        LD      BC,$0004            ; sprite records are a fixed four bytes
1EC1: 3A 54 63        LD      A,(workRam+354)     ; the hit record's index within the array
1EC4: A7              AND     A                   
1EC5: CA CF 1E        JP      Z,loc_1ecf          ; index 0: both cursors are already there

loc_1ec8:
1EC8: 09              ADD     HL,BC               ; walk the sprite cursor out to the hit index
1EC9: DD 19           ADD     IX,DE               ; and the object cursor by its own stride
1ECB: 3D              DEC     A                   
1ECC: C2 C8 1E        JP      NZ,loc_1ec8         ; 

loc_1ecf:
1ECF: DD 36 00 00     LD      (IX+$00),$00        ; deactivate the object that was hit
1ED3: DD 7E 15        LD      A,(IX+$15)          ; its kind field picks the effect variant
1ED6: A7              AND     A                   
1ED7: 3E 02           LD      A,$02               ; zero there selects variant 2
1ED9: CA DE 1E        JP      Z,loc_1ede          ; 
1EDC: 3E 04           LD      A,$04               ; anything else selects variant 4

loc_1ede:
1EDE: 32 42 63        LD      (workRam+342),A     ; stage the variant for the award setter
1EE1: 01 2C 6A        LD      BC,$6A2C            ; point at the effect sprite's record
1EE4: 7E              LD      A,(HL)              ; take the hit sprite's X
1EE5: 36 00           LD      (HL),$00            ; blank it, so the source stops being drawn
1EE7: 02              LD      (BC),A              ; the effect appears where the object was
1EE8: 0C              INC     C                   
1EE9: 2C              INC     L                   
1EEA: 3E 60           LD      A,$60               
1EEC: 02              LD      (BC),A              ; the effect's tile code
1EED: 0C              INC     C                   
1EEE: 2C              INC     L                   
1EEF: 3E 0C           LD      A,$0C               
1EF1: 02              LD      (BC),A              ; its colour attribute
1EF2: 0C              INC     C                   
1EF3: 2C              INC     L                   
1EF4: 7E              LD      A,(HL)              ; the hit sprite's Y
1EF5: 02              LD      (BC),A              ; copied across too
1EF6: 21 45 63        LD      HL,$6345            
1EF9: 34              INC     (HL)                ; advance the sequence to its flashing step
1EFA: 2C              INC     L                   
1EFB: 36 06           LD      (HL),$06            ; reload the inner beat divider
1EFD: 2C              INC     L                   
1EFE: 36 05           LD      (HL),$05            ; and the outer step counter
1F00: 21 8A 60        LD      HL,$608A            
1F03: 36 06           LD      (HL),$06            ; queue the hit tune over the music
1F05: 2C              INC     L                   
1F06: 36 03           LD      (HL),$03            ; for three frames
1F08: C9              RET                         

; Effect-sequence step 1: a two-stage rate divider that flashes a sprite on most beats
; and hands the sequence on every fourth. effectSeqInner counts down on every dispatch
; and the routine returns until it drains, once every six calls. On that beat it reloads
; the inner counter to 6 and ticks effectSeqOuter: while the outer counter is still
; running it XORs the low bit of the effect sprite's tile-code byte, so the tile
; oscillates between two consecutive codes; once every four beats, when the outer counter
; drains instead, it reloads that counter to 4 and advances effectSeqState, moving the
; sequence on to its next step. So the sprite blinks at one-sixth of the dispatch rate
; and the sequence steps forward at one twenty-fourth. What the effect depicts on screen
; is not established here.
flashEffectSpriteThenAdvanceSequence:
1F09: 21 46 63        LD      HL,$6346            
1F0C: 35              DEC     (HL)                ; one beat in six
1F0D: C0              RET     NZ                  
1F0E: 36 06           LD      (HL),$06            ; reload the beat divider
1F10: 2C              INC     L                   
1F11: 35              DEC     (HL)                ; tick the outer step counter
1F12: CA 1D 1F        JP      Z,loc_1f1d          ; four beats done: hand the sequence on
1F15: 21 2D 6A        LD      HL,$6A2D            ; point at the effect sprite's tile code
1F18: 7E              LD      A,(HL)              
1F19: EE 01           XOR     $01                 ; flip between the two tiles of the pair
1F1B: 77              LD      (HL),A              
1F1C: C9              RET                         

loc_1f1d:
1F1D: 36 04           LD      (HL),$04            ; reload the outer counter
1F1F: 2D              DEC     L                   
1F20: 2D              DEC     L                   
1F21: 34              INC     (HL)                ; advance the sequence to its next step
1F22: C9              RET                         

; Step 2 of the effect sequence: a two-stage rate divider. effectSeqInner ticks down on
; every dispatch and the routine returns until it drains -- one beat in twelve. On a
; beat it reloads effectSeqInner to 12 and ticks effectSeqOuter down: while that is
; still running it increments the effect sprite's code byte, so the tile MARCHES forward
; one step per beat, which is what distinguishes this step from its sibling, which
; flashes between two tiles. When effectSeqOuter drains it is NOT reloaded; instead the
; sequence is torn down -- effectSeqState back to 0, effectState re-armed to 1,
; effectParamPtr aimed back at effectSprite, and a shared scratch cell cleared, which is
; what hands the per-frame cascade back to ordinary play. What the effect DEPICTS is not
; claimed -- only the byte-level animation and the teardown.
animateEffectSpriteThenRearmEffect:
1F23: 21 46 63        LD      HL,$6346            
1F26: 35              DEC     (HL)                ; one beat in twelve
1F27: C0              RET     NZ                  
1F28: 36 0C           LD      (HL),$0C            ; reload the beat divider
1F2A: 2C              INC     L                   
1F2B: 35              DEC     (HL)                ; tick the outer step counter
1F2C: CA 34 1F        JP      Z,loc_1f34          ; the counter drains: tear the effect down
1F2F: 21 2D 6A        LD      HL,$6A2D            ; point at the effect sprite's tile code
1F32: 34              INC     (HL)                ; march the tile forward one step
1F33: C9              RET                         

loc_1f34:
1F34: 2D              DEC     L                   
1F35: 2D              DEC     L                   
1F36: AF              XOR     A                   
1F37: 77              LD      (HL),A              ; reset the sequence to its first step
1F38: 32 50 63        LD      (workRam+350),A     ; drop the latch, handing the frame back to play
1F3B: 3C              INC     A                   
1F3C: 32 40 63        LD      (workRam+340),A     ; re-arm the parent effect machine
1F3F: 21 2C 6A        LD      HL,$6A2C            
1F42: 22 43 63        LD      (workRam+343),HL    ; point it back at the effect sprite
1F45: C9              RET                         

; When marioStartFall is set -- the slope and ledge contact check raises it the moment
; there is no girder under Mario's foot -- this consumes the trigger and rebuilds his
; motion state for a fall that starts from rest: marioXFrac and marioYFrac and the whole
; airborne velocity and elapsed-frame block are cleared, so the fall begins at zero
; speed; marioAirborne is set, which is what the movement machine reads next frame;
; marioAirLandcheck is armed at once, so the drop is evaluated from the very next
; airborne frame rather than waiting for a jump to near its apex; and marioY is
; snapshotted into marioAirStartY, the value the landing code measures the depth
; against. The trigger is a one-shot cleared here, and any non-zero value launches the
; same fall. With it clear this does nothing at all.
beginMarioFall:
1F46: 3A 21 62        LD      A,(workRam+221)     ; read the fall trigger
1F49: A7              AND     A                   
1F4A: C8              RET     Z                   ; no ground went away this frame
1F4B: AF              XOR     A                   
1F4C: 32 04 62        LD      (workRam+204),A     ; the fall starts from a whole pixel
1F4F: 32 06 62        LD      (workRam+206),A     ; 
1F52: 32 21 62        LD      (workRam+221),A     ; consume the one-shot trigger
1F55: 32 10 62        LD      (workRam+210),A     ; no horizontal speed: he drops straight down
1F58: 32 11 62        LD      (workRam+211),A     ; 
1F5B: 32 12 62        LD      (workRam+212),A     ; and no vertical speed: gravity alone
1F5E: 32 13 62        LD      (workRam+213),A     ; 
1F61: 32 14 62        LD      (workRam+214),A     ; arc frame count back to zero
1F64: 3C              INC     A                   
1F65: 32 16 62        LD      (workRam+216),A     ; the mover drives him airborne from next frame
1F68: 32 1F 62        LD      (workRam+21F),A     ; evaluate the drop from the first frame
1F6B: 3A 05 62        LD      A,(workRam+205)     ; 
1F6E: 32 0E 62        LD      (workRam+20E),A     ; remember the height he fell from
1F71: C9              RET                         

; The head of the 25m barrel engine. One test, then a hand-off: if board is not the
; girder board it returns at once, which is why three quarters of the game never runs the
; walk and why objArray67 stays identically zero elsewhere. On 25m it seeds the walk's
; four working values -- the record it starts on, the sprite cursor, the record stride,
; and the count of ten -- and falls into the walk. The walk advances the record pointer
; by one stride and the sprite cursor by four bytes each pass, so over the ten iterations
; the cursor sweeps exactly the ten stride-4 records at actorSprites; only its low byte
; is incremented, so the sweep cannot leave that page.
update25mBarrels:
1F72: 3A 27 62        LD      A,(workRam+227)     ; read the board being played
1F75: 3D              DEC     A                   
1F76: C0              RET     NZ                  ; barrels only roll on 25m
1F77: DD 21 00 67     LD      IX,$6700            ; the first of the ten barrel records
1F7B: 21 80 69        LD      HL,$6980            ; the sprite staging cursor
1F7E: 11 20 00        LD      DE,$0020            ; records are 32 bytes apart
1F81: 06 0A           LD      B,$0A               ; ten slots to service

; The barrel walk's per-slot gate, at the head of every pass round the loop. It reads one
; byte, the record's active flag, and either hands the whole record to the motion
; dispatch -- which takes the sprite staging cursor over with it -- or skips the slot,
; the skip's whole job being to step that cursor over the four bytes this slot did not
; write. It moves three of them; the fourth belongs to the between-slots step both halves
; converge on, so the cursor lands on a record boundary whichever half ran. THE TEST IS
; EQUALITY WITH 1, not a bit test, and the difference is a mechanic: the byte takes three
; values live -- 0 empty, 1 running, 2 claimed -- so a record the release path has
; stamped with 2 is skipped exactly as an empty one is, and does not reach the motion
; dispatch until it is actually running.
serviceBarrelSlotIfLive:
1F83: DD 7E 00        LD      A,(IX+$00)          ; the slot's active flag
1F86: 3D              DEC     A                   
1F87: CA 93 1F        JP      Z,advanceBarrelMotion; only 1 runs; 2 is claimed and skipped
1F8A: 2C              INC     L                   ; step the cursor over the bytes this slot did not write
1F8B: 2C              INC     L                   
1F8C: 2C              INC     L                   

loc_1f8d:
1F8D: 2C              INC     L                   ; the fourth step, landing on the next record
1F8E: DD 19           ADD     IX,DE               ; on to the next barrel record
1F90: 10 F1           DJNZ    serviceBarrelSlotIfLive; round again while slots remain
1F92: C9              RET                         

; Send a live barrel to its motion arm for this frame. It reads two record bytes, writes
; nothing, and jumps to one of five arms, each of which does the record's actual
; movement and rejoins the walk; every arm is a tail jump, so this routine's result IS
; the moved barrel. TWO FIELDS, AND THE FIRST OUTRANKS THE SECOND: the select byte
; (record +1) is tested first and for EQUALITY WITH 1, and only if that fails are the
; low three bits of the mode byte (record +2) tested, lowest first, first set bit wins.
; A record can arrive with the select byte at 1 over an already-set mode byte, and that
; priority is the only thing deciding which arm it gets. The mode byte's bits above bit
; 2 are never examined, so a record with mode 8 and one with mode 0 are
; indistinguishable here.
advanceBarrelMotion:
1F93: DD 7E 01        LD      A,(IX+$01)          ; the select byte, which outranks the mode bits
1F96: 3D              DEC     A                   
1F97: CA EC 20        JP      Z,advanceFallingBarrel; 1: it is falling between girders
1F9A: DD 7E 02        LD      A,(IX+$02)          ; otherwise the mode bits, lowest first
1F9D: 1F              RRA                         
1F9E: DA AC 1F        JP      C,loc_1fac          ; bit 0: it is walking down a ladder
1FA1: 1F              RRA                         
1FA2: DA E5 1F        JP      C,stepBarrelRight   ; bit 1: roll one pixel right
1FA5: 1F              RRA                         
1FA6: DA EF 1F        JP      C,stepBarrelLeft    ; bit 2: roll one pixel left
1FA9: C3 53 20        JP      loc_2053            ; no bit set: carry it along its arc

loc_1fac:
1FAC: D9              EXX                         ; the arms work in the alternate bank; the tail swaps back
1FAD: DD 34 05        INC     (IX+$05)            ; one row further down the ladder
1FB0: DD 7E 17        LD      A,(IX+$17)          ; the row this descent stops on
1FB3: DD BE 05        CP      (IX+$05)            
1FB6: C2 CE 1F        JP      NZ,advanceBarrelTileAnimation; not there yet, so just animate
1FB9: DD 7E 15        LD      A,(IX+$15)          
1FBC: 07              RLCA                        
1FBD: 07              RLCA                        
1FBE: C6 15           ADD     A,$15               
1FC0: DD 77 07        LD      (IX+$07),A          ; stamp the arrival sprite code on the frame it lands
1FC3: DD 7E 02        LD      A,(IX+$02)          
1FC6: EE 07           XOR     $07                 ; flip the arm-select bits: hand it to a roll arm
1FC8: DD 77 02        LD      (IX+$02),A          
1FCB: C3 BA 21        JP      publishBarrelSprite ; 

; Step one barrel record's animation prescaler (+0x0f) and, on the visit it runs out,
; flip the LOWEST bit of the barrel's sprite code -- swapping it to the other tile of an
; adjacent pair -- and reload the prescaler to 4. The walk above visits an active record
; once a frame, so each tile is held four frames. The prescaler is stepped as a byte, so
; a value of 0 goes to 255 rather than expiring; only exactly zero is expiry. That
; lowest bit is a CHOICE OF TILE, not a way of drawing one: the raster flip lives in bit
; 7 of the same byte and is never touched here. What the two tiles of a pair depict is
; not established.
advanceBarrelTileAnimation:
1FCE: DD 7E 0F        LD      A,(IX+$0F)          ; the tile-hold prescaler
1FD1: 3D              DEC     A                   
1FD2: C2 DF 1F        JP      NZ,loc_1fdf         ; still holding this tile
1FD5: DD 7E 07        LD      A,(IX+$07)          
1FD8: EE 01           XOR     $01                 ; swap to the other tile of the pair
1FDA: DD 77 07        LD      (IX+$07),A          
1FDD: 3E 04           LD      A,$04               ; hold the new tile four frames

loc_1fdf:
1FDF: DD 77 0F        LD      (IX+$0F),A          ; store the prescaler back
1FE2: C3 BA 21        JP      publishBarrelSprite ; 

; The +X motion arm of the barrel walk: bank the register file, load the two values the
; shared roll tail consumes, and increment the record's X by one -- the sign of that
; increment is the direction in the name. The staged step selector reaches the girder
; snap, and 1 is the value that snaps on the offset-0 edge of a 16-pixel girder cell,
; which is the edge a barrel walking X upwards crosses first; the mirror arm passes the
; offset-15 value instead. The second value folds into a packed sprite-orientation
; lookup; all that is derived here is that this arm passes 0. Every motion arm works in
; the alternate bank and none swaps back -- the swap happens where the arms converge, and
; it is what restores the walk's own loop state.
stepBarrelRight:
1FE5: D9              EXX                         ; into the arms' own register bank
1FE6: 01 00 01        LD      BC,$0100            ; snap on the offset-0 cell edge; direction code 0
1FE9: DD 34 03        INC     (IX+$03)            ; one pixel right
1FEC: C3 F6 1F        JP      advanceRollingBarrel; 

; The -X motion arm of the barrel walk: bank the register file, stage the two direction
; constants the shared roll tail consumes, and decrement this barrel's X. The walk keeps
; its loop state in the main bank, so every motion arm works in the alternate one. The
; first constant, 255, is the girder snap's step selector: the snap moves Y one pixel
; along the slope only on the frame X lands on a 16-pixel cell boundary, and the selector
; picks which edge counts -- 1 fires at cell offset 0, any other value at offset 15 -- so
; a barrel walking X downwards, which enters a new cell at offset 15, wants the not-1
; selector. The mirror arm is this routine with the increment and the constants 1 and 0;
; the second constant is a direction code the sprite-orientation refresh folds into its
; lookup selector as 3 OR code.
stepBarrelLeft:
1FEF: D9              EXX                         
1FF0: 01 04 FF        LD      BC,$FF04            ; snap on the offset-15 cell edge; direction code 4
1FF3: DD 35 03        DEC     (IX+$03)            ; one pixel left

; The shared tail of the two roll arms: carry the barrel one step along its current run,
; then route it. Both entry arms have already moved the record's X by one. One X value
; in eight (X = 3 mod 8) goes straight out to the LADDER DETOUR before any of the work
; below, which is why this tail must not be described as "along the girder". Otherwise
; the barrel is re-glued to the girder slope, its sprite orientation is refreshed, and
; the bottom-of-playfield gate decides: below the low edge (28) the low-end arm stamps
; the leftward step; inside the window it goes straight to the shared sprite publish,
; which is the overwhelming majority of passes; at or past the high edge (228) the
; mirrored rightward step of +96, in the record's 1/256-pixel units, is stamped into its
; own step bytes. The slope snap is handed a coordinate three pixels off the record's Y
; and the three go back on afterwards; what those three pixels are is not determined
; here.
advanceRollingBarrel:
1FF6: DD 66 03        LD      H,(IX+$03)          ; the barrel's column
1FF9: DD 6E 05        LD      L,(IX+$05)          ; and its row
1FFC: 7C              LD      A,H                 
1FFD: E6 07           AND     $07                 
1FFF: FE 03           CP      $03                 ; one column in eight is a ladder position
2001: CA 5F 21        JP      Z,loc_215f          ; take the ladder detour before the work below
2004: 2D              DEC     L                   ; the snap works three pixels off the record's row
2005: 2D              DEC     L                   
2006: 2D              DEC     L                   
2007: CD 33 23        CALL    snapYToGirder       ; re-glue the barrel to the girder slope

loc_200a:
200A: 2C              INC     L                   ; put the three pixels back on
200B: 2C              INC     L                   
200C: 2C              INC     L                   
200D: 7D              LD      A,L                 
200E: DD 77 05        LD      (IX+$05),A          ; commit the slope-corrected row
2011: CD DE 23        CALL    advanceBarrelSpriteOrientation; the two sprite mirror bits
2014: CD B4 24        CALL    retireBarrelIntoOilDrum; it can take the record over
2017: DD 7E 03        LD      A,(IX+$03)          
201A: FE 1C           CP      $1C                 ; past the low edge of the playfield?
201C: DA 2F 20        JP      C,loc_202f          ; yes: stamp the leftward step
201F: FE E4           CP      $E4                 ; still short of the high edge?
2021: DA BA 21        JP      C,publishBarrelSprite; mid-playfield: just publish the sprite
2024: AF              XOR     A                   
2025: DD 77 10        LD      (IX+$10),A          ; at the high edge: stamp the rightward step, +96
2028: DD 36 11 60     LD      (IX+$11),$60        
202C: C3 38 20        JP      loc_2038            ; 

loc_202f:
202F: AF              XOR     A                   ; at the low edge instead
2030: DD 36 10 FF     LD      (IX+$10),$FF        ; stamp the leftward step, -96
2034: DD 36 11 A0     LD      (IX+$11),$A0        

loc_2038:
2038: DD 36 12 FF     LD      (IX+$12),$FF        ; arm the fall: initial vertical velocity -16
203C: DD 36 13 F0     LD      (IX+$13),$F0        
2040: DD 77 14        LD      (IX+$14),A          ; arc frame count back to zero
2043: DD 77 0E        LD      (IX+$0E),A          ; and the arm's sub-state counter
2046: DD 77 04        LD      (IX+$04),A          ; clear both coordinate fractions
2049: DD 77 06        LD      (IX+$06),A          
204C: DD 36 02 08     LD      (IX+$02),$08        ; no arm-select bit set: the arc arm takes it next
2050: C3 BA 21        JP      publishBarrelSprite ; 

loc_2053:
2053: D9              EXX                         
2054: CD 9C 23        CALL    stepBallisticMotion ; carry the barrel a frame along its arc
2057: CD 2F 2A        CALL    loc_2a2f            ; probe the girder under it

loc_205a:
205A: A7              AND     A                   
205B: C2 83 20        JP      NZ,loc_2083         ; contact: it has landed on a girder
205E: DD 7E 03        LD      A,(IX+$03)          
2061: C6 08           ADD     A,$08               ; within eight pixels of X = 0, either side of the wrap?
2063: FE 10           CP      $10                 
2065: DA 79 20        JP      C,loc_2079          ; yes: retire the record
2068: CD B4 24        CALL    retireBarrelIntoOilDrum; it can take the record over
206B: DD 7E 10        LD      A,(IX+$10)          
206E: E6 01           AND     $01                 ; the step's direction bit
2070: 07              RLCA                        
2071: 07              RLCA                        ; becomes the orientation selector, 0 or 4
2072: 4F              LD      C,A                 
2073: CD DE 23        CALL    advanceBarrelSpriteOrientation; the two sprite mirror bits
2076: C3 BA 21        JP      publishBarrelSprite ; 

loc_2079:
2079: AF              XOR     A                   
207A: DD 77 00        LD      (IX+$00),A          ; free the slot
207D: DD 77 03        LD      (IX+$03),A          ; park it at X = 0, so it is not drawn where it died
2080: C3 BA 21        JP      publishBarrelSprite ; 

loc_2083:
2083: DD 34 0E        INC     (IX+$0E)            ; count one step further into the landing sub-state
2086: DD 7E 0E        LD      A,(IX+$0E)          
2089: 3D              DEC     A                   
208A: CA A2 20        JP      Z,loc_20a2          ; first step: decide whether it turns round
208D: 3D              DEC     A                   
208E: CA C3 20        JP      Z,loc_20c3          ; second step: bounce it
2091: DD 7E 10        LD      A,(IX+$10)          ; the whole-pixel half of its horizontal step
2094: 3D              DEC     A                   
2095: 3E 04           LD      A,$04               ; select the left-walk arm
2097: C2 9C 20        JP      NZ,loc_209c         ; 
209A: 3E 02           LD      A,$02               ; a rightward whole pixel: select the right-walk arm

loc_209c:
209C: DD 77 02        LD      (IX+$02),A          ; put it on a one-pixel-a-frame walk next frame
209F: C3 BA 21        JP      publishBarrelSprite ; 

loc_20a2:
20A2: DD 7E 15        LD      A,(IX+$15)          ; the barrel's kind
20A5: A7              AND     A                   
20A6: C2 B5 20        JP      NZ,loc_20b5         ; the alternate kind turns round unasked
20A9: 21 05 62        LD      HL,$6205            ; point at Mario's row
20AC: DD 7E 05        LD      A,(IX+$05)          
20AF: D6 16           SUB     $16                 ; 22 rows of clearance below him
20B1: BE              CP      (HL)                
20B2: D2 C3 20        JP      NC,loc_20c3         ; come to rest well below him: let it carry on

loc_20b5:
20B5: DD 7E 10        LD      A,(IX+$10)          ; the whole-pixel half of its horizontal step
20B8: A7              AND     A                   
20B9: C2 E1 20        JP      NZ,loc_20e1         ; anything but a sub-pixel rightward step goes right
20BC: DD 77 11        LD      (IX+$11),A          
20BF: DD 36 10 FF     LD      (IX+$10),$FF        ; send it off leftward at one pixel a frame

loc_20c3:
20C3: CD 07 24        CALL    loc_2407            ; reflect the arc: 16*frames minus the launch speed
20C6: CB 3C           SRL     H                   
20C8: CB 1D           RR      L                   
20CA: CB 3C           SRL     H                   ; a quarter of it: it comes back slower than it arrived
20CC: CB 1D           RR      L                   
20CE: DD 74 12        LD      (IX+$12),H          ; the damped speed becomes the new launch speed
20D1: DD 75 13        LD      (IX+$13),L          
20D4: AF              XOR     A                   
20D5: DD 77 14        LD      (IX+$14),A          ; restart the arc's frame count
20D8: DD 77 04        LD      (IX+$04),A          ; and clear both coordinate fractions
20DB: DD 77 06        LD      (IX+$06),A          
20DE: C3 BA 21        JP      publishBarrelSprite ; 

loc_20e1:
20E1: DD 36 10 01     LD      (IX+$10),$01        ; send it off rightward at one pixel a frame
20E5: DD 36 11 00     LD      (IX+$11),$00        
20E9: C3 C3 20        JP      loc_20c3            ; 

; Carry a barrel one frame further down its fall between girders, and decide whether
; this is a frame on which it may test the girder underneath it. The record keeps a
; snapshot of where the barrel was when it last registered a girder contact, and the
; probe is held off until its Y has moved 26 past that snapshot -- a re-arm distance, so
; a barrel cannot immediately re-detect the girder it just landed on. Under that
; distance control goes to the end-of-range retirement check, which only asks whether
; the barrel has run off the edge; past it, a slope tile underneath takes the contact
; arm and no contact the other. The gravity ramp restarts at each landing, which is what
; makes this a fall and not a bounce. The re-arm subtraction is 8-bit, so a barrel whose
; Y is under 26 wraps to a large value and PASSES the gate instead of failing it.
advanceFallingBarrel:
20EC: D9              EXX                         
20ED: CD 9C 23        CALL    stepBallisticMotion ; carry the barrel one frame further down
20F0: 7C              LD      A,H                 ; the freshly stepped row
20F1: D6 1A           SUB     $1A                 ; the 26-pixel contact re-arm distance
20F3: DD 46 19        LD      B,(IX+$19)          ; the row of its last girder contact
20F6: B8              CP      B                   
20F7: DA 04 21        JP      C,retireBarrelAtEndOfRange; too close to re-probe the girder
20FA: CD 2F 2A        CALL    loc_2a2f            ; probe the girder under it

loc_20fd:
20FD: A7              AND     A                   
20FE: C2 18 21        JP      NZ,loc_2118         ; contact: set up the next arc
2101: CD B4 24        CALL    retireBarrelIntoOilDrum; no contact this frame

; Retire a barrel once its X has run down to the bottom of its travel range; otherwise
; hand the record on unchanged. The test is on X + 8 taken at eight-bit width, not on X,
; so an X that has run PAST zero and wrapped to the top of the byte counts as retired
; just as a small positive X does -- without that wrap a barrel whose X had gone negative
; would be treated as if it were at the far end of the range. On the retire arm the
; record's active flag and its X are both zeroed: the flag frees the slot, and the zeroed
; X stops the freed slot from being drawn where it died. Either way the record's four
; sprite fields are still gathered this pass.
retireBarrelAtEndOfRange:
2104: DD 7E 03        LD      A,(IX+$03)          
2107: C6 08           ADD     A,$08               ; within eight pixels of X = 0, either side of the wrap?
2109: FE 10           CP      $10                 
210B: D2 CE 1F        JP      NC,advanceBarrelTileAnimation; still in range, so just animate
210E: AF              XOR     A                   
210F: DD 77 00        LD      (IX+$00),A          ; free the slot
2112: DD 77 03        LD      (IX+$03),A          ; and stop it being drawn where it died
2115: C3 BA 21        JP      publishBarrelSprite ; 

loc_2118:
2118: DD 7E 05        LD      A,(IX+$05)          
211B: FE E0           CP      $E0                 ; has it reached the bottom line of the screen?
211D: DA 46 21        JP      C,loc_2146          ; no: seed it a fresh trajectory
2120: DD 7E 07        LD      A,(IX+$07)          
2123: E6 FC           AND     $FC                 
2125: F6 01           OR      $01                 ; force the sprite code's low two bits to 01
2127: DD 77 07        LD      (IX+$07),A          
212A: AF              XOR     A                   
212B: DD 77 01        LD      (IX+$01),A          ; clear the select byte
212E: DD 77 02        LD      (IX+$02),A          ; and the mode byte
2131: DD 36 10 FF     LD      (IX+$10),$FF        ; a fixed leftward step of one pixel a frame
2135: DD 77 11        LD      (IX+$11),A          
2138: DD 77 12        LD      (IX+$12),A          
213B: DD 36 13 B0     LD      (IX+$13),$B0        ; launch vertical speed 176
213F: DD 36 0E 01     LD      (IX+$0E),$01        ; start its sub-state counter at one
2143: C3 53 21        JP      loc_2153            ; 

loc_2146:
2146: CD 07 24        CALL    loc_2407            ; 
2149: CD CB 22        CALL    loc_22cb            ; seed the two step fields by mode and difficulty

loc_214c:
214C: DD 7E 05        LD      A,(IX+$05)          
214F: DD 77 19        LD      (IX+$19),A          ; remember this row as the contact reference
2152: AF              XOR     A                   

loc_2153:
2153: DD 77 14        LD      (IX+$14),A          ; restart the arc's frame count
2156: DD 77 04        LD      (IX+$04),A          ; and clear both coordinate fractions
2159: DD 77 06        LD      (IX+$06),A          
215C: C3 BA 21        JP      publishBarrelSprite ; 

loc_215f:
215F: 7D              LD      A,L                 
2160: C6 05           ADD     A,$05               ; the discriminator sits five rows below the barrel
2162: 57              LD      D,A                 
2163: 7C              LD      A,H                 ; the search key is the barrel's column
2164: 01 15 00        LD      BC,$0015            ; one column of the ladder table, 21 entries
2167: CD 6D 21        CALL    startBarrelDescentAtLadder; does it take the ladder down?
216A: C3 BA 21        JP      publishBarrelSprite ; 

; Decide whether a rolling barrel takes a ladder down -- the "wild" barrel. The caller
; hands over a search key, a discriminator and a scan count. A miss in the
; ladder-endpoint table unwinds on the caller's behalf, and a hit tagged 0 rather than 1
; is rejected. A tag-1 hit ALWAYS stamps the record's descent-target field with (paired
; slot - 5), then runs the gates that decide whether the barrel also starts moving. A
; clear spawn-mode gate byte starts it immediately. Otherwise: reject unless Mario has
; descended far enough that (his Y - 4) reaches the discriminator, then unless a
; difficulty-weighted random throttle passes; then by Mario's column against the key --
; exactly on it always goes, past it if Left is held, before it if Right is held, else a
; final random gate decides. Which ladder a barrel takes is a function of Mario.
startBarrelDescentAtLadder:
216D: CD 6E 23        CALL    findOppositeLadderEnd; look the column up in the table
2170: 3D              DEC     A                   
2171: C0              RET     NZ                  ; only a near-end hit is processed
2172: 78              LD      A,B                 ; the paired slot at the other end of the ladder
2173: D6 05           SUB     $05                 
2175: DD 77 17        LD      (IX+$17),A          ; stamp the row the descent stops on
2178: 3A 48 63        LD      A,(workRam+348)     ; read the spawn-mode gate
217B: A7              AND     A                   
217C: CA B2 21        JP      Z,loc_21b2          ; clear: start the descent at once
217F: 3A 05 62        LD      A,(workRam+205)     ; 
2182: D6 04           SUB     $04                 ; Mario must have descended past the discriminator
2184: BA              CP      D                   
2185: D8              RET     C                   ; he has not come far enough yet
2186: 3A 80 63        LD      A,(workRam+380)     ; weight the throttle by difficulty
2189: 1F              RRA                         
218A: 3C              INC     A                   
218B: 47              LD      B,A                 ; half the difficulty, plus one
218C: 3A 18 60        LD      A,(workRam+18)      ; read the rolling random byte
218F: 4F              LD      C,A                 
2190: E6 03           AND     $03                 
2192: B8              CP      B                   ; two random bits against that throttle
2193: D0              RET     NC                  ; the draw rejects this attempt
2194: 21 10 60        LD      HL,$6010            ; point at the cooked control word
2197: 3A 03 62        LD      A,(workRam+203)     ; 
219A: BB              CP      E                   ; compare Mario's column with the ladder's
219B: CA B2 21        JP      Z,loc_21b2          ; standing right on it: the barrel takes it
219E: D2 A9 21        JP      NC,loc_21a9         ; he is past the ladder
21A1: CB 46           BIT     0,(HL)              ; before it: is Right held, toward it?
21A3: CA AE 21        JP      Z,loc_21ae          ; 
21A6: C3 B2 21        JP      loc_21b2            ; coming toward it: take the ladder

loc_21a9:
21A9: CB 4E           BIT     1,(HL)              ; past it: is Left held, back toward it?
21AB: C2 B2 21        JP      NZ,loc_21b2         ; coming toward it: take the ladder

loc_21ae:
21AE: 79              LD      A,C                 
21AF: E6 18           AND     $18                 ; otherwise a last random gate decides
21B1: C0              RET     NZ                  ; the draw says roll on past

loc_21b2:
21B2: DD 34 07        INC     (IX+$07)            ; bump the barrel's sprite code
21B5: DD CB 02 C6     SET     0,(IX+$02)          ; put the barrel on the ladder-travel arm
21B9: C9              RET                         

; The join every motion arm of the barrel walk arrives at, and where the record's four
; sprite bytes are actually written. Its first act is an unconditional register
; exchange: the arms that jump here swapped the walk's registers out for a working set
; of their own and none swaps back, so from this point the staging cursor, record
; pointer, stride and remaining-slot count are the walk's again. It then gathers four
; record fields into the sprite buffer in the sprite record's own field order -- X,
; code, attribute, Y. It is a PERMUTING gather and not a block copy: the record's Y is
; read after the two sprite bytes, and the two fields lying between them are never read.
; That permutation is the whole point, because it turns a barrel record into the byte
; layout the sprite hardware reads. Three of the four cursor steps happen here and the
; fourth in the between-slots step it hands on to; only the cursor's low byte moves, so
; it can never leave its page.
publishBarrelSprite:
21BA: D9              EXX                         ; the walk's cursors and slot count are the live set again
21BB: DD 7E 03        LD      A,(IX+$03)          
21BE: 77              LD      (HL),A              ; sprite X from the barrel's column
21BF: 2C              INC     L                   
21C0: DD 7E 07        LD      A,(IX+$07)          
21C3: 77              LD      (HL),A              ; its tile code
21C4: 2C              INC     L                   
21C5: DD 7E 08        LD      A,(IX+$08)          
21C8: 77              LD      (HL),A              ; its colour attribute
21C9: 2C              INC     L                   
21CA: DD 7E 05        LD      A,(IX+$05)          
21CD: 77              LD      (HL),A              ; sprite Y from the barrel's row
21CE: C3 8D 1F        JP      loc_1f8d            ; 

; ---- $21D1-$21ED: data ----
21D1: 80 FE 01 C0 04 50 02 10 82 60 02 10 82 CA 01 10
21E1: 81 FF 02 38 01 80 02 FF 04 80 04 60 80

; Advance the canned input script that drives the attract demo, called once per demo
; frame immediately before the shared per-frame update. The script is a table of
; two-byte (input, duration) pairs: demoScriptIndex picks the pair -- the entry offset
; being the index doubled, low byte only, so the table page never carries -- and that
; step's input byte is written straight over p1Input, the same cooked control word the
; joystick fills, so the demo plays itself. demoScriptCountdown is then read and
; decremented, and its value BEFORE the decrement decides: still non-zero means keep
; holding this step; zero means reload the countdown from the duration byte and step the
; index to the next pair. A duration of N holds its input for N+1 frames, and the input
; byte is re-issued on every frame it is held.
advanceAttractDemoInput:
21EE: 11 D1 21        LD      DE,$21D1            ; the demo script: pairs of input byte and duration
21F1: 21 CC 63        LD      HL,$63CC            ; the script cursor
21F4: 7E              LD      A,(HL)              
21F5: 07              RLCA                        ; two bytes per entry
21F6: 83              ADD     A,E                 
21F7: 5F              LD      E,A                 ; index the current pair; the page never carries
21F8: 1A              LD      A,(DE)              ; this step's input byte
21F9: 32 10 60        LD      (workRam+10),A      ; overwrite the control word: the demo plays itself
21FC: 2C              INC     L                   
21FD: 7E              LD      A,(HL)              ; read the hold countdown
21FE: 35              DEC     (HL)                ; and spend one frame of it
21FF: A7              AND     A                   
2200: C0              RET     NZ                  ; the step is still being held
2201: 1C              INC     E                   
2202: 1A              LD      A,(DE)              ; the duration byte of the pair just finished
2203: 77              LD      (HL),A              ; reload the countdown from it
2204: 2D              DEC     L                   
2205: 34              INC     (HL)                ; step the script on to the next pair
2206: C9              RET                         

; The 50m board-object state machine. A board gate opens the body ONLY on 50m; elsewhere
; it is dispatched every pass and does nothing. Frame parity picks which of the two
; 8-byte boardObjScratch records is serviced -- odd frame the first, even frame the
; second -- and the record's state byte, held in 0..3, selects one of four arms. The
; record's travel counter IS a screen Y, and larger is lower, so its minimum of 0x68 is
; the object's HIGHEST point and its maximum of 0x78 the lowest. State 0 parks at the top
; for a 256-frame dwell, stamping a shared flag on a Mario hit; state 1 steps the counter
; UP, moving the object DOWN the screen; state 2 is a randomised dwell at the lowest
; point; state 3 steps back up and re-parks at state 0. What these two objects ARE is
; open: their sprite reads as a ladder graphic and the four arms would fit a moving
; ladder, but that is reading a picture, not the code.
dispatch50mObjectState:
2207: 3E 02           LD      A,$02               ; board mask: 50m only
2209: F7              RST     $30                 ; on the other three boards the body below is skipped
220A: 3A 1A 60        LD      A,(workRam+1A)      ; frame parity picks which record is serviced
220D: 1F              RRA                         
220E: 21 80 62        LD      HL,$6280            ; the first object record
2211: 7E              LD      A,(HL)              ; its state byte
2212: DA 19 22        JP      C,loc_2219          ; 
2215: 21 88 62        LD      HL,$6288            ; the second object record
2218: 7E              LD      A,(HL)              ; its state byte

loc_2219:
2219: E5              PUSH    HL                  ; hand the record base to the arm
221A: EF              RST     $28                 ; branch through the four-slot table below

; ---- $221B-$2226: jump table ----
221B: 27 22 59 22 99 22 A2 22 00 00 00 00

; The PARKED arm of the 50m board-object state machine: hold the object still while its
; dwell timer runs down. The record is 8 bytes -- +0 its state, +1 the dwell timer, +2
; the object's column -- and this is the arm taken while the state byte is 0. Nothing
; moves; the object's position counter is not touched anywhere here, which is what
; "parked" means. Every pass counts the dwell timer down by one. If the timer just
; reached zero the state is advanced -- which is what sets the object moving -- and Mario
; is hit-tested against the object's column, stamping a shared flag with 1 on a hit. If
; the timer is still running the same hit test stamps the flag with 0. A MISS unwinds two
; levels instead of returning, so only the stamp is skipped. Which 50m object the record
; drives is not established.
hold50mObjectParked:
2227: E1              POP     HL                  ; the record base the dispatcher pushed
2228: 2C              INC     L                   
2229: 35              DEC     (HL)                ; count the dwell timer down
222A: C2 3A 22        JP      NZ,loc_223a         ; still parked
222D: 2D              DEC     L                   
222E: 34              INC     (HL)                ; dwell over: advance the state, setting it moving
222F: 2C              INC     L                   
2230: 2C              INC     L                   ; point at the object's column
2231: CD 43 22        CALL    marioReachedTargetColumn; is Mario standing on it?
2234: 3E 01           LD      A,$01               
2236: 32 1A 62        LD      (workRam+21A),A     ; on a hit, stamp the shared flag set
2239: C9              RET                         

loc_223a:
223A: 2C              INC     L                   ; point at the object's column
223B: CD 43 22        CALL    marioReachedTargetColumn; is Mario standing on it?
223E: AF              XOR     A                   
223F: 32 1A 62        LD      (workRam+21A),A     ; still parked: stamp the shared flag clear
2242: C9              RET                         

; Has Mario reached the target position? Three conditions must ALL hold for a hit:
; marioY is numerically under 122 -- larger Y is LOWER on screen, so the reach band is
; the rows ABOVE that one and at or below it the hit never registers; marioAirborne is
; clear, so the hit only counts with Mario grounded and not mid jump or fall; and marioX
; exactly equals the object's target X, the byte whose address the caller supplies. On a
; hit the caller runs its own tail; on any miss it hands off to the shared no-hit tail,
; whose result unwinds two levels up. A pure predicate over Mario's position and
; airborne state: it writes no memory and claims no game purpose beyond the test itself.
marioReachedTargetColumn:
2243: 3A 05 62        LD      A,(workRam+205)     ; read Mario's row
2246: FE 7A           CP      $7A                 ; the reach band is the rows above this one
2248: D2 57 22        JP      NC,reportNoHitAndSkipCaller; too low on screen: no hit
224B: 3A 16 62        LD      A,(workRam+216)     ; is he mid jump or fall?
224E: A7              AND     A                   
224F: C2 57 22        JP      NZ,reportNoHitAndSkipCaller; airborne does not count
2252: 3A 03 62        LD      A,(workRam+203)     ; 
2255: BE              CP      (HL)                ; is he exactly on the target column?
2256: C8              RET     Z                   ; a hit; the caller runs its own tail

; The "no hit" tail of the target-column hit test. That test asks whether Mario is
; standing on the target column -- marioY below 0x7A, marioAirborne clear, and marioX
; equal to the byte at a caller-supplied pointer -- and on a HIT returns normally, so
; its caller, one of the 50m object-state arms, runs its own tail. On NO HIT control
; reaches here, which discards the parent's return address and returns past it: the
; caller's tail never runs and control resumes two levels up. Pure control flow -- it
; reads nothing and writes nothing.
reportNoHitAndSkipCaller:
2257: E1              POP     HL                  ; discard the caller's return: unwind two levels
2258: C9              RET                         

; The descend arm of that machine, and the exact mirror of its raise arm. It counts the
; record's per-tick timer down by one, and until that underflows the object just idles.
; On the tick it does: reload the timer, step the position counter UP by one -- one pixel
; DOWN the screen -- and mirror the new counter into the object's on-screen sprite cell;
; when the counter reaches the bottom of its travel, advance the object's state. It then
; hit-tests Mario against the object's column, and on a miss the shared no-hit path
; unwinds two levels up. On a hit it settles Mario's climb downward: while he is still
; above the settle line, or on an odd pixel row, he is stepped one pixel down held in the
; climb pose; once his Y reaches the line on an even row, the climb-centring toggle is
; published from bit 1 of his Y so it alternates as he climbs.
slide50mObjectDown:
2259: E1              POP     HL                  ; the record base the dispatcher pushed
225A: 2C              INC     L                   ; walk out to the object's per-tick timer
225B: 2C              INC     L                   
225C: 2C              INC     L                   
225D: 2C              INC     L                   
225E: 35              DEC     (HL)                ; tick it down
225F: C0              RET     NZ                  ; the object idles until it underflows
2260: 3E 04           LD      A,$04               
2262: 77              LD      (HL),A              ; reload the timer
2263: 2D              DEC     L                   
2264: 34              INC     (HL)                ; step the counter up: one pixel DOWN the screen
2265: CD BD 22        CALL    publish50mObjectYToSprite; mirror the new position on screen
2268: 3E 78           LD      A,$78               
226A: BE              CP      (HL)                ; reached the bottom of its travel?
226B: C2 75 22        JP      NZ,loc_2275         ; 
226E: 2D              DEC     L                   
226F: 2D              DEC     L                   
2270: 2D              DEC     L                   
2271: 34              INC     (HL)                ; at the bottom: advance the object's state
2272: 2C              INC     L                   
2273: 2C              INC     L                   
2274: 2C              INC     L                   

loc_2275:
2275: 2D              DEC     L                   ; point at the object's column byte
2276: CD 43 22        CALL    marioReachedTargetColumn; hit-test Mario against the column
2279: 3A 05 62        LD      A,(workRam+205)     ; read Mario's screen row
227C: FE 68           CP      $68                 ; compare against the climb settle line
227E: D2 8A 22        JP      NC,loc_228a         ; at or past the line, check the row parity

; Step Mario down one pixel, held in the climb-down pose. The Y-descend tail of the
; conveyor board's descend arm, reached when Mario's screen Y is still numerically above
; the centring line -- that is, still higher on screen, since larger Y is lower -- or on
; an odd row. Each call increments marioY, so the logical position drops a pixel; pins
; his hardware sprite to a fixed climb frame and takes back a pointer to that record's Y
; field; then increments that Y, so the drawn sprite drops a pixel to match. Pose codes 3
; to 5 are the climb frames, and the pin forces the first of them with the mirror flag
; clear.
stepMarioDownInClimbPose:
2281: 21 05 62        LD      HL,$6205            ; point at Mario's screen row
2284: 34              INC     (HL)                ; drop him one pixel down the screen
2285: CD C0 3F        CALL    pinMarioClimbPose   ; pin the climb pose and take the sprite Y
2288: 34              INC     (HL)                ; drop the drawn sprite a pixel to match
2289: C9              RET                         

loc_228a:
228A: 1F              RRA                         ; row parity: odd rows keep him stepping
228B: DA 81 22        JP      C,stepMarioDownInClimbPose; odd row -- step him down again
228E: 1F              RRA                         ; now bit 1 of the row decides the toggle
228F: 3E 01           LD      A,$01               
2291: DA 95 22        JP      C,loc_2295          ; 
2294: AF              XOR     A                   

loc_2295:
2295: 32 22 62        LD      (workRam+222),A     ; publish the climb-centring toggle
2298: C9              RET                         

; Let a 50m board object move on to its next state, but only on a randomly chosen frame,
; so it dwells an unpredictable length of time in the one it is in. The caller picks
; which object's record to work on and hands over its base address; the state byte sits
; at the front of the record. random is sampled and the state is stepped ONLY when four
; selected bits of it happen to be clear -- about one frame in sixteen -- and on every
; other frame the object simply stays where it is. The effect is that it holds a state
; for a random spell instead of advancing on a fixed cadence, so its behaviour does not
; look metronomic.
advance50mObjectStateOnRandomGate:
2299: E1              POP     HL                  ; take the object record's base off the stack
229A: 3A 18 60        LD      A,(workRam+18)      ; sample the random accumulator
229D: E6 3C           AND     $3C                 ; four selected bits -- about one frame in sixteen
229F: C0              RET     NZ                  ; not the chosen frame, hold this state
22A0: 34              INC     (HL)                ; step the object on to its next state
22A1: C9              RET                         

; One idle-then-retract tick for one of the two 50m travelling objects in
; boardObjScratch, the dispatcher choosing between the two eight-byte records by frame
; parity. Field +4 is a per-tick countdown: it is stepped down every time this arm runs,
; and until it underflows the object simply idles. On the tick it does, the countdown
; reloads and the position counter at +3 steps DOWN by one, and that new position is
; mirrored into the object's on-screen sprite cell. That counter is a SCREEN Y published
; straight into a sprite record's Y byte, and larger Y is lower, so stepping it down
; moves the object UP the screen. Reaching the top of travel parks it: field +1 takes a
; fixed value and the state byte at +0 is cleared, back to state 0. Across the four
; states the cycle is park at the top, extend down, dwell at the bottom, retract, park
; -- the motion of a retracting ladder, though the object is unidentified.
raise50mObjectAndPark:
22A2: E1              POP     HL                  ; take the object record's base off the stack
22A3: 2C              INC     L                   ; walk out to the per-tick countdown at +4
22A4: 2C              INC     L                   
22A5: 2C              INC     L                   
22A6: 2C              INC     L                   
22A7: 35              DEC     (HL)                ; tick the countdown
22A8: C0              RET     NZ                  ; not yet time to move, idle this frame
22A9: 36 02           LD      (HL),$02            ; reload the countdown
22AB: 2D              DEC     L                   ; back to the position counter at +3
22AC: 35              DEC     (HL)                ; step it down -- one pixel UP the screen
22AD: CD BD 22        CALL    publish50mObjectYToSprite; mirror the position to its sprite
22B0: 3E 68           LD      A,$68               ; the top of the object's travel
22B2: BE              CP      (HL)                
22B3: C0              RET     NZ                  ; not at the top yet, keep going
22B4: AF              XOR     A                   
22B5: 06 80           LD      B,$80               ; the parked value for the dwell field
22B7: 2D              DEC     L                   ; back to the dwell field at +1
22B8: 2D              DEC     L                   
22B9: 70              LD      (HL),B              ; park the object
22BA: 2D              DEC     L                   ; back to the state byte at +0
22BB: 77              LD      (HL),A              ; return it to state 0
22BC: C9              RET                         

; Mirror the byte at a source pointer into one of two sprite slots, chosen by bit 3 of
; the pointer's low byte -- clear picks the lower-addressed cell, set the higher. Both
; targets are the Y field of a 4-byte record inside spriteBuffer, records 17 and 18, the
; two slots just below Mario's, so the store refreshes an on-screen sprite's Y straight
; from the source byte; larger Y is lower on screen, so raising the copied byte drops the
; sprite down the display. Its two callers are the moving arms of the 50m object machine,
; each handing it a pointer to an object record's position counter, and the selector
; works because the two records' bases differ by 8. The sprite therefore holds the
; record's value as of the PREVIOUS frame, not this one.
publish50mObjectYToSprite:
22BD: 7E              LD      A,(HL)              ; read the object's position counter
22BE: CB 5D           BIT     3,L                 ; which of the two object records is this?
22C0: 11 4B 69        LD      DE,$694B            ; the Y field of the higher sprite slot
22C3: C2 C9 22        JP      NZ,loc_22c9         ; 
22C6: 11 47 69        LD      DE,$6947            ; the Y field of the lower slot

loc_22c9:
22C9: 12              LD      (DE),A              ; refresh the sprite's Y from the counter
22CA: C9              RET                         

loc_22cb:
22CB: 3A 48 63        LD      A,(workRam+348)     ; read the velocity-source mode latch
22CE: A7              AND     A                   
22CF: CA E1 22        JP      Z,loc_22e1          ; latch clear -- pick the magnitude by level
22D2: 3A 80 63        LD      A,(workRam+380)     ; otherwise the difficulty picks the source
22D5: 3D              DEC     A                   ; difficulty 1 indexes entry 0
22D6: EF              RST     $28                 ; vector into the four-arm table below

; ---- $22D7-$22E0: jump table ----
22D7: F6 22 F6 22 03 23 03 23 1A 23

loc_22e1:
22E1: 3A 29 62        LD      A,(workRam+229)     ; read the level
22E4: 47              LD      B,A                 
22E5: 05              DEC     B                   
22E6: 3E 01           LD      A,$01               ; level 1's magnitude
22E8: CA F9 22        JP      Z,loc_22f9          ; 
22EB: 05              DEC     B                   
22EC: 3E B1           LD      A,$B1               ; level 2's magnitude
22EE: CA F9 22        JP      Z,loc_22f9          ; 
22F1: 3E E9           LD      A,$E9               ; every later level shares this one
22F3: C3 F9 22        JP      loc_22f9            ; 

loc_22f6:
22F6: 3A 18 60        LD      A,(workRam+18)      ; difficulty 1-2: magnitude straight from chance

loc_22f9:
22F9: DD 77 11        LD      (IX+$11),A          ; store it as the object's step magnitude
22FC: E6 01           AND     $01                 ; its low bit picks the direction
22FE: 3D              DEC     A                   ; odd gives 00, even gives ff
22FF: DD 77 10        LD      (IX+$10),A          ; store the signed direction byte
2302: C9              RET                         

loc_2303:
2303: 3A 18 60        LD      A,(workRam+18)      ; difficulty 3-4: a random step magnitude
2306: DD 77 11        LD      (IX+$11),A          
2309: 3A 03 62        LD      A,(workRam+203)     ; steer the step toward Mario
230C: DD BE 03        CP      (IX+$03)            ; compare him against the object's column
230F: 3E 01           LD      A,$01               ; step right by default
2311: D2 16 23        JP      NC,loc_2316         ; Mario at or right of it -- keep +1
2314: 3D              DEC     A                   ; Mario to the left -- step -1 instead
2315: 3D              DEC     A                   

loc_2316:
2316: DD 77 10        LD      (IX+$10),A          ; store the toward-Mario direction
2319: C9              RET                         

loc_231a:
231A: 3A 03 62        LD      A,(workRam+203)     ; difficulty 5: derive both fields from the offset
231D: DD 96 03        SUB     (IX+$03)            ; signed horizontal offset out to Mario
2320: 0E FF           LD      C,$FF               ; sign fill for Mario on the left
2322: DA 26 23        JP      C,loc_2326          ; 
2325: 0C              INC     C                   ; Mario at or right of it -- no sign fill

loc_2326:
2326: 07              RLCA                        ; rotate the offset's top two bits down
2327: CB 11           RL      C                   
2329: 07              RLCA                        
232A: CB 11           RL      C                   
232C: DD 71 10        LD      (IX+$10),C          ; store the toward-Mario step code
232F: DD 77 11        LD      (IX+$11),A          ; store the offset-derived step delta
2332: C9              RET                         

; Nudge a coordinate one pixel along the 25m girder slope. The 25m girders are not flat
; -- each runs a shallow diagonal -- so a body walking or rolling across one has to shift
; its Y by one pixel every time its X crosses into a new 16-pixel girder cell to stay
; glued to the slope. Given the mover's X, the Y being corrected and a step selector, it
; moves Y ONLY at the instant X lands on a cell boundary: pixel offset 0 within the cell
; when the selector is 1, offset 15 otherwise. Anywhere mid-cell Y comes back unchanged.
; Direction is read from Y itself: a single bit of Y picks +1 against -1 in the general
; band, with two hard-coded band-seam rails, Y == 240 and Y == 76, that flip on X
; instead. Which physical girder runs those two rails join is not established.
snapYToGirder:
2333: 3E 0F           LD      A,$0F               
2335: A4              AND     H                   ; the mover's offset within its 16-pixel cell
2336: 05              DEC     B                   ; selector 1 picks the cell-entry test
2337: CA 42 23        JP      Z,loc_2342          ; 
233A: FE 0F           CP      $0F                 ; otherwise Y only moves at offset 15
233C: D8              RET     C                   ; mid-cell -- leave Y alone
233D: 06 FF           LD      B,$FF               ; nudge of one pixel, the other way
233F: C3 47 23        JP      loc_2347            ; 

loc_2342:
2342: FE 01           CP      $01                 ; selector 1: Y only moves at offset 0
2344: D0              RET     NC                  ; mid-cell -- leave Y alone
2345: 06 01           LD      B,$01               ; nudge of one pixel

loc_2347:
2347: 3E F0           LD      A,$F0               
2349: BD              CP      L                   ; Y == 240 is a band-seam rail
234A: CA 60 23        JP      Z,loc_2360          ; 
234D: 3E 4C           LD      A,$4C               
234F: BD              CP      L                   ; Y == 76 is the other rail
2350: CA 66 23        JP      Z,loc_2366          ; 
2353: 7D              LD      A,L                 
2354: CB 6F           BIT     5,A                 ; a bit of Y picks which way the girder slopes
2356: CA 5C 23        JP      Z,loc_235c          ; 

loc_2359:
2359: 90              SUB     B                   ; step Y one pixel against the nudge

loc_235a:
235A: 6F              LD      L,A                 ; hand the corrected Y back
235B: C9              RET                         

loc_235c:
235C: 80              ADD     A,B                 ; step Y one pixel with the nudge
235D: C3 5A 23        JP      loc_235a            ; 

loc_2360:
2360: CB 7C           BIT     7,H                 ; at the 240 rail, X decides instead
2362: C2 59 23        JP      NZ,loc_2359         ; 
2365: C9              RET                         ; left half of the screen -- Y unchanged

loc_2366:
2366: 7C              LD      A,H                 
2367: FE 98           CP      $98                 ; at the 76 rail, only X past 152 shifts
2369: D8              RET     C                   ; short of the seam -- Y unchanged
236A: 7D              LD      A,L                 
236B: C3 5C 23        JP      loc_235c            ; 

; Find a key in the ladder (object-parameter) table and hand back the paired slot at the
; OTHER end of that ladder, tagged with which end the caller started from. It scans
; objParamTable0 for the first byte equal to the search key; a matched entry carries two
; slots at a fixed +0x15 and +0x2A past it, and the routine returns whichever slot the
; discriminator did NOT match -- discriminator equal to the +0x15 byte gives tag 1 and
; the +0x2A byte, equal to the +0x2A byte gives tag 0 and the +0x15 byte, and if neither
; matches the scan resumes past that entry. A key never found before the count runs out
; is a MISS, and a miss is a DOUBLE unwind: the caller must return too. It writes no
; work RAM. That the records are LADDERS is not derivable from this body; what is, is
; the shape -- a keyed lookup into fixed-stride records, returning the far member of a
; pair and saying which end it came from.
findOppositeLadderEnd:
236E: 21 00 63        LD      HL,$6300            ; scan from the head of the parameter table

loc_2371:
2371: ED B1           CPIR                        ; search for the first entry matching the key
2373: C2 9A 23        JP      NZ,loc_239a         ; not found before the count ran out -- a miss
2376: E5              PUSH    HL                  ; remember where the scan had got to
2377: C5              PUSH    BC                  
2378: 01 14 00        LD      BC,$0014            
237B: 09              ADD     HL,BC               ; step out to this entry's paired slot at +0x15
237C: 0C              INC     C                   
237D: 5F              LD      E,A                 
237E: 7A              LD      A,D                 ; the discriminator names the caller's own end
237F: BE              CP      (HL)                
2380: CA 8F 23        JP      Z,loc_238f          ; that end matched -- take the far one
2383: 09              ADD     HL,BC               ; try the slot at +0x2A instead
2384: BE              CP      (HL)                
2385: CA 95 23        JP      Z,loc_2395          ; 
2388: 57              LD      D,A                 
2389: 7B              LD      A,E                 
238A: C1              POP     BC                  
238B: E1              POP     HL                  
238C: C3 71 23        JP      loc_2371            ; neither end matched -- resume the scan

loc_238f:
238F: 09              ADD     HL,BC               ; take the +0x2A slot
2390: 3E 01           LD      A,$01               ; tag 1: the caller started at the near end
2392: C3 98 23        JP      loc_2398            ; 

loc_2395:
2395: AF              XOR     A                   ; tag 0: the caller started at the far end
2396: ED 42           SBC     HL,BC               ; take the +0x15 slot instead

loc_2398:
2398: C1              POP     BC                  
2399: 46              LD      B,(HL)              ; hand back the slot at the other end

loc_239a:
239A: E1              POP     HL                  ; reclaim the scan pointer -- or, on a miss, the return
239B: C9              RET                         

; Advance an airborne actor one frame along its ballistic arc, on whatever record the
; caller points at. Two of its fields are 16-bit fixed-point coordinates, big-endian
; with the high byte at the lower offset, each with its own signed 16-bit per-frame
; velocity, plus a one-byte airborne-frame counter t that drives a ramping gravity term:
;   +03:+04  coordinate A (horizontal)  += velocity A at +10:+11
;   +05:+06  coordinate B (vertical)    -= velocity B at +12:+13, then += gravity(t)
;   +14      t, incremented after use
; gravity(t) = (2t + 1) * 8 = 16t + 8, the discrete integral of a constant downward
; acceleration -- so coordinate B follows a parabola, velocity B first carrying it one
; way and the accumulating gravity term overtaking it, while A drifts at constant
; velocity. The same shape serves Mario and the airborne objects alike; no fixed address
; is referenced.
stepBallisticMotion:
239C: DD 7E 04        LD      A,(IX+$04)          ; low byte of the horizontal coordinate
239F: DD 86 11        ADD     A,(IX+$11)          ; add the low byte of its per-frame velocity
23A2: DD 77 04        LD      (IX+$04),A          
23A5: DD 7E 03        LD      A,(IX+$03)          
23A8: DD 8E 10        ADC     A,(IX+$10)          ; carry into the high byte
23AB: DD 77 03        LD      (IX+$03),A          
23AE: DD 7E 06        LD      A,(IX+$06)          ; low byte of the vertical coordinate
23B1: DD 96 13        SUB     (IX+$13)            ; subtract its own per-frame velocity
23B4: 6F              LD      L,A                 
23B5: DD 7E 05        LD      A,(IX+$05)          
23B8: DD 9E 12        SBC     A,(IX+$12)          
23BB: 67              LD      H,A                 
23BC: DD 7E 14        LD      A,(IX+$14)          ; frames elapsed since the actor left the ground
23BF: A7              AND     A                   
23C0: 17              RLA                         
23C1: 3C              INC     A                   ; form 2t + 1, the gravity ramp's multiplier
23C2: 06 00           LD      B,$00               
23C4: CB 10           RL      B                   
23C6: CB 27           SLA     A                   ; scale it by eight into a 16-bit gravity term
23C8: CB 10           RL      B                   
23CA: CB 27           SLA     A                   
23CC: CB 10           RL      B                   
23CE: CB 27           SLA     A                   
23D0: CB 10           RL      B                   
23D2: 4F              LD      C,A                 
23D3: 09              ADD     HL,BC               ; add the ramping gravity term
23D4: DD 74 05        LD      (IX+$05),H          ; store the new vertical coordinate
23D7: DD 75 06        LD      (IX+$06),L          
23DA: DD 34 14        INC     (IX+$14)            ; one more airborne frame
23DD: C9              RET                         

; Refresh a barrel's two sprite MIRROR bits from a packed direction lookup, one call in
; four. Almost every call just steps the record's own down-counter at +0x0F and returns;
; only the call that finds it at 1 does the work and reloads the counter to 4. On that
; beat it rewrites the top bit of two record bytes and leaves their low seven bits alone:
; bit 7 of the sprite code is the VERTICAL mirror, bit 7 of the attribute the HORIZONTAL
; one. The new pair comes from a packed 4x2-bit lookup keyed by the barrel's current
; orientation -- its two existing top bits as a 2-bit selector -- together with the
; direction code, so the facing advances one step through a table while the tile and the
; colour in the low bits are preserved. Two mirror bits do not by themselves make a
; rotation, and the barrel artwork is not decoded here.
advanceBarrelSpriteOrientation:
23DE: DD 7E 0F        LD      A,(IX+$0F)          ; read this barrel's orientation countdown
23E1: 3D              DEC     A                   
23E2: C2 03 24        JP      NZ,loc_2403         ; not the beat -- just store the ticked counter
23E5: AF              XOR     A                   
23E6: DD CB 07 26     SLA     (IX+$07)            ; shift the vertical-mirror bit out of the code
23EA: 17              RLA                         ; collect it as the top bit of the selector
23EB: DD CB 08 26     SLA     (IX+$08)            ; and the horizontal mirror out of the attribute
23EF: 17              RLA                         
23F0: 47              LD      B,A                 ; the barrel's current orientation, two bits
23F1: 3E 03           LD      A,$03               
23F3: B1              OR      C                   ; key the lookup with the direction code
23F4: CD 09 30        CALL    nextAnimationStep   ; advance the orientation one step
23F7: 1F              RRA                         ; shift the new mirror bits back in
23F8: DD CB 08 1E     RR      (IX+$08)            
23FC: 1F              RRA                         
23FD: DD CB 07 1E     RR      (IX+$07)            
2401: 3E 04           LD      A,$04               ; reload the countdown -- one call in four

loc_2403:
2403: DD 77 0F        LD      (IX+$0F),A          ; store the countdown back
2406: C9              RET                         

loc_2407:
2407: DD 7E 14        LD      A,(IX+$14)          ; the record's packed pair of 4-bit digits
240A: 07              RLCA                        ; swap the two digits
240B: 07              RLCA                        
240C: 07              RLCA                        
240D: 07              RLCA                        
240E: 4F              LD      C,A                 
240F: E6 0F           AND     $0F                 ; the high digit is the whole-number part
2411: 67              LD      H,A                 
2412: 79              LD      A,C                 
2413: E6 F0           AND     $F0                 ; the low digit becomes the fraction
2415: 6F              LD      L,A                 
2416: DD 4E 13        LD      C,(IX+$13)          ; the 16-bit operand carried in the record
2419: DD 46 12        LD      B,(IX+$12)          
241C: ED 42           SBC     HL,BC               ; the fixed-point difference the callers use
241E: C9              RET                         

; The horizontal position gate: classify Mario's X into a two-flag verdict the movement
; code turns into a restraint on his X. It reads marioX, marioY and board and writes no
; memory. The tests run in order, all unsigned, first to fire deciding: X < 0x16 gives
; (1,0), far left; X >= 0xEA gives (0,1), the right edge; an even board gives (0,0), so
; the gate never opens on 50m or 100m; Y >= 0x58 gives (0,0); X >= 0x6C gives (0,0), past
; the mid column; anything else gives (1,0). So the left verdict also covers an INTERIOR
; wall at the left end of the top platform on the odd boards, not just the screen edge.
; The walk step reads the right flag as "a rightward step is refused"; the airborne step
; reads the left flag as a push back inward, a rightward drift of half a pixel a frame
; rather than a stop; and the X clamp uses both.
limitMarioHorizontalTravel:
241F: 11 00 01        LD      DE,$0100            ; start from the far-left verdict
2422: 3A 03 62        LD      A,(workRam+203)     ; read Mario's column
2425: FE 16           CP      $16                 ; is he left of the far-left limit?
2427: D8              RET     C                   ; yes -- the left flag stands
2428: 15              DEC     D                   ; switch to the right-edge verdict
2429: 1C              INC     E                   
242A: FE EA           CP      $EA                 ; at or past the right edge?
242C: D0              RET     NC                  ; yes -- refuse a rightward step
242D: 1D              DEC     E                   ; otherwise no restraint at all
242E: 3A 27 62        LD      A,(workRam+227)     ; the gate never opens on 50m or 100m
2431: 0F              RRCA                        
2432: D0              RET     NC                  
2433: 3A 05 62        LD      A,(workRam+205)     ; read his screen row
2436: FE 58           CP      $58                 ; below the top platform -- no restraint
2438: D0              RET     NC                  
2439: 3A 03 62        LD      A,(workRam+203)     ; read his column again
243C: FE 6C           CP      $6C                 ; past the mid column -- no restraint
243E: D0              RET     NC                  
243F: 14              INC     D                   ; left of the top platform's interior wall
2440: C9              RET                         

; Scatter this board's object-init records into two parallel work-RAM attribute arrays,
; once per board. Two heads run first. One forms an 8-bit modular checksum, seeded with
; 0x5E, over six bytes of program data and uses it to pick the second group's base --
; zero selects the base, anything else one byte past it. The shipped image sums to
; exactly zero, so the alternate base never happens in practice: it is a data-integrity
; guard. The other head picks the record table from board, 25m / 50m / 75m each having
; their own and everything else taking the default. The walk then reads fixed 5-byte
; records [type, fieldA, fieldB, unused, fieldC]: type 0 de-interleaves into
; objParamTable0 and type 1 into objParamTable1, the three fields landing in three
; parallel arrays a fixed stride apart. Type 0xAA ends the walk and is the ONLY exit;
; any other type is skipped whole and the walk continues.
loadBoardObjectRecords:
2441: 21 0C 3F        LD      HL,$3F0C            ; the six program bytes the integrity sum covers
2444: 3E 5E           LD      A,$5E               ; checksum seed
2446: 06 06           LD      B,$06               

loc_2448:
2448: 86              ADD     A,(HL)              ; sum the six bytes
2449: 23              INC     HL                  
244A: 10 FC           DJNZ    loc_2448            ; 
244C: FD 21 10 63     LD      IY,$6310            ; base of the type-1 attribute array
2450: A7              AND     A                   
2451: CA 56 24        JP      Z,loc_2456          ; sum zero -- use that base
2454: FD 23           INC     IY                  ; a non-zero sum shifts it one byte on

loc_2456:
2456: 3A 27 62        LD      A,(workRam+227)     ; pick the record table for this board
2459: 3D              DEC     A                   
245A: 21 E4 3A        LD      HL,$3AE4            ; 25m's records
245D: CA 71 24        JP      Z,loc_2471          ; 
2460: 3D              DEC     A                   
2461: 21 5D 3B        LD      HL,$3B5D            ; 50m's records
2464: CA 71 24        JP      Z,loc_2471          ; 
2467: 3D              DEC     A                   
2468: 21 E5 3B        LD      HL,$3BE5            ; 75m's records
246B: CA 71 24        JP      Z,loc_2471          ; 
246E: 21 8B 3C        LD      HL,$3C8B            ; the default table for every other board

loc_2471:
2471: DD 21 00 63     LD      IX,$6300            ; base of the type-0 attribute array
2475: 11 05 00        LD      DE,$0005            ; records are five bytes long

loc_2478:
2478: 7E              LD      A,(HL)              ; read the record's type byte
2479: A7              AND     A                   
247A: CA 88 24        JP      Z,loc_2488          ; type 0 -- scatter into the first array
247D: 3D              DEC     A                   
247E: CA 9E 24        JP      Z,loc_249e          ; type 1 -- into the parallel array
2481: FE A9           CP      $A9                 ; aa ends the walk, the only exit
2483: C8              RET     Z                   
2484: 19              ADD     HL,DE               ; any other type is skipped whole
2485: C3 78 24        JP      loc_2478            ; 

loc_2488:
2488: 23              INC     HL                  
2489: 7E              LD      A,(HL)              ; first field
248A: DD 77 00        LD      (IX+$00),A          ; into the base of the type-0 array
248D: 23              INC     HL                  
248E: 7E              LD      A,(HL)              ; second field
248F: DD 77 15        LD      (IX+$15),A          ; one stride on
2492: 23              INC     HL                  ; step over the record's unused byte
2493: 23              INC     HL                  
2494: 7E              LD      A,(HL)              ; third field
2495: DD 77 2A        LD      (IX+$2A),A          ; two strides on
2498: DD 23           INC     IX                  ; on to the next slot
249A: 23              INC     HL                  
249B: C3 78 24        JP      loc_2478            ; 

loc_249e:
249E: 23              INC     HL                  
249F: 7E              LD      A,(HL)              ; first field
24A0: FD 77 00        LD      (IY+$00),A          ; into the base of the type-1 array
24A3: 23              INC     HL                  
24A4: 7E              LD      A,(HL)              ; second field
24A5: FD 77 15        LD      (IY+$15),A          ; one stride on
24A8: 23              INC     HL                  ; step over the record's unused byte
24A9: 23              INC     HL                  
24AA: 7E              LD      A,(HL)              ; third field
24AB: FD 77 2A        LD      (IY+$2A),A          ; two strides on
24AE: FD 23           INC     IY                  ; on to the next slot
24B0: 23              INC     HL                  
24B1: C3 78 24        JP      loc_2478            ; 

; Retire a barrel that has reached the bottom of the playfield inside the oil drum's
; column band. Three gates in a row, each an ordinary return that leaves everything
; alone: the record's Y must have reached the bottom row (larger Y is lower on this
; screen), and its X must lie inside the band, both ends tested against the SAME loaded
; byte, so it is one range and not two independent limits. Past them the record's active
; flag and X are both zeroed -- the slot freed and the column blanked -- the impact
; sound latch is asserted for three frames, and control is handed to the shared sprite
; publish INSTEAD of returning, so the caller's remaining code must not run. Two latches
; are armed here, and they are why this is more than a bounds check: for a barrel whose
; kind byte is non-zero the fixed hazard's phase byte is set to bits 0 and 1, the pair
; that lets the hazard machine run AND take the arm that counts down and eventually
; raises an object-insert request -- so retiring one kind of barrel is what ARMS a fire
; release, the fire itself being spawned later and capped by a live count. And a
; one-shot difficulty latch is set the first time this ever fires and never touched
; again: both its readers take CLEAR as the simple early-game behaviour and SET as the
; difficulty-graded one, so this is a one-way switch into the graded behaviour.
retireBarrelIntoOilDrum:
24B4: DD 7E 05        LD      A,(IX+$05)          ; the barrel's screen row
24B7: FE E8           CP      $E8                 ; has it reached the bottom row?
24B9: D8              RET     C                   ; not down yet -- leave it running
24BA: DD 7E 03        LD      A,(IX+$03)          ; its column
24BD: FE 2A           CP      $2A                 ; right of the drum's band?
24BF: D0              RET     NC                  
24C0: FE 20           CP      $20                 ; left of the band?
24C2: D8              RET     C                   
24C3: DD 7E 15        LD      A,(IX+$15)          ; the barrel's kind byte
24C6: A7              AND     A                   
24C7: CA D0 24        JP      Z,loc_24d0          ; ordinary barrel -- just free the slot
24CA: 3E 03           LD      A,$03               ; bits 0 and 1: run the hazard AND arm a fire
24CC: 32 B9 62        LD      (workRam+2B9),A     ; 
24CF: AF              XOR     A                   ; the value that frees the record below

loc_24d0:
24D0: DD 77 00        LD      (IX+$00),A          ; clear the record's active flag
24D3: DD 77 03        LD      (IX+$03),A          ; and blank the column it died in
24D6: 21 82 60        LD      HL,$6082            ; the impact sound line
24D9: 36 03           LD      (HL),$03            ; assert it for three frames
24DB: E1              POP     HL                  ; drop the return: the publish returns in its place
24DC: 3A 48 63        LD      A,(workRam+348)     ; the one-shot mode latch
24DF: A7              AND     A                   
24E0: C2 BA 21        JP      NZ,publishBarrelSprite; already set -- just publish the sprite
24E3: 3C              INC     A                   
24E4: 32 48 63        LD      (workRam+348),A     ; the first retirement switches the mode on
24E7: C3 BA 21        JP      publishBarrelSprite ; publish the blanked sprite, and return

update50mMovingObjects:
24EA: 3E 02           LD      A,$02               ; board mask: 50m only
24EC: F7              RST     $30                 ; closed on any other board -- skip the whole body
24ED: CD 23 25        CALL    service50mObjectSpawnRequest; spawn one if one is due
24F0: CD 91 25        CALL    advance50mObjectRow ; move them, cull the ones that ran out
24F3: DD 21 A0 65     LD      IX,$65A0            ; the six 50m object records
24F7: 06 06           LD      B,$06               
24F9: 21 B8 69        LD      HL,$69B8            ; and their six four-byte sprite records

loc_24fc:
24FC: DD 7E 00        LD      A,(IX+$00)          ; is this record live?
24FF: A7              AND     A                   
2500: CA 1C 25        JP      Z,loc_251c          ; dormant -- step past its sprite slot untouched
2503: DD 7E 03        LD      A,(IX+$03)          ; the object's column
2506: 77              LD      (HL),A              
2507: 2C              INC     L                   
2508: DD 7E 07        LD      A,(IX+$07)          ; its sprite
250B: 77              LD      (HL),A              
250C: 2C              INC     L                   
250D: DD 7E 08        LD      A,(IX+$08)          ; its colour and flip attribute
2510: 77              LD      (HL),A              
2511: 2C              INC     L                   
2512: DD 7E 05        LD      A,(IX+$05)          ; and its row -- it is on screen this frame
2515: 77              LD      (HL),A              
2516: 2C              INC     L                   

loc_2517:
2517: DD 19           ADD     IX,DE               ; on to the next record; the stride came back from the row advance
2519: 10 E1           DJNZ    loc_24fc            ; 
251B: C9              RET                         

loc_251c:
251C: 7D              LD      A,L                 ; leave the slot showing whatever the cull left there
251D: C6 04           ADD     A,$04               
251F: 6F              LD      L,A                 
2520: C3 17 25        JP      loc_2517            ; 

service50mObjectSpawnRequest:
2523: 21 9B 63        LD      HL,$639B            ; the spawn cooldown
2526: 7E              LD      A,(HL)              
2527: A7              AND     A                   
2528: C2 8F 25        JP      NZ,loc_258f         ; still cooling down -- just tick it and stop
252B: 3A 9A 63        LD      A,(workRam+39A)     ; is a spawn actually being asked for?
252E: A7              AND     A                   
252F: C8              RET     Z                   ; nothing asked -- nothing to do
2530: 06 06           LD      B,$06               
2532: 11 10 00        LD      DE,$0010            
2535: DD 21 A0 65     LD      IX,$65A0            ; hunt the six records for a free slot

loc_2539:
2539: DD CB 00 46     BIT     0,(IX+$00)          ; a free slot has this bit clear
253D: CA 45 25        JP      Z,loc_2545          ; 
2540: DD 19           ADD     IX,DE               
2542: 10 F5           DJNZ    loc_2539            ; 
2544: C9              RET                         ; every slot busy -- no spawn this pass

loc_2545:
2545: CD 57 00        CALL    stirRandomSeed      ; roll the seed to place the new one
2548: FE 60           CP      $60                 
254A: DD 36 05 7C     LD      (IX+$05),$7C        ; put it on the upper row by default
254E: DA 58 25        JP      C,loc_2558          ; a low roll sends it to the lower row
2551: 3A A3 62        LD      A,(workRam+2A3)     ; otherwise object 2's heading decides
2554: 3D              DEC     A                   
2555: C2 6E 25        JP      NZ,loc_256e         ; not that way -- re-roll, just for the side

loc_2558:
2558: DD 36 05 CC     LD      (IX+$05),$CC        ; the lower row
255C: 3A A6 62        LD      A,(workRam+2A6)     ; object 3's heading picks the side
255F: 07              RLCA                        

loc_2560:
2560: DD 36 03 07     LD      (IX+$03),$07        ; start it at the low end of its travel
2564: D2 76 25        JP      NC,loc_2576         ; 
2567: DD 36 03 F8     LD      (IX+$03),$F8        ; or the high end
256B: C3 76 25        JP      loc_2576            ; 

loc_256e:
256E: CD 57 00        CALL    stirRandomSeed      ; a second roll, this time only for the side
2571: FE 68           CP      $68                 
2573: C3 60 25        JP      loc_2560            ; 

loc_2576:
2576: DD 36 00 01     LD      (IX+$00),$01        ; bring the record to life
257A: DD 36 07 4B     LD      (IX+$07),$4B        ; its sprite
257E: DD 36 09 08     LD      (IX+$09),$08        ; its hit box: eight across
2582: DD 36 0A 03     LD      (IX+$0A),$03        ; and three down
2586: 3E 7C           LD      A,$7C               
2588: 32 9B 63        LD      (workRam+39B),A     ; no other may spawn for 124 frames
258B: AF              XOR     A                   
258C: 32 9A 63        LD      (workRam+39A),A     ; the request is consumed

loc_258f:
258F: 35              DEC     (HL)                ; tick the cooldown -- or, from the spawn path, the spin counter
2590: C9              RET                         

advance50mObjectRow:
2591: DD 21 A0 65     LD      IX,$65A0            ; the same six records
2595: 11 10 00        LD      DE,$0010            ; the record stride, which the caller reuses after this returns
2598: 06 06           LD      B,$06               

loc_259a:
259A: DD CB 00 46     BIT     0,(IX+$00)          ; only live records move
259E: CA BB 25        JP      Z,loc_25bb          ; 
25A1: DD 7E 03        LD      A,(IX+$03)          ; the object's column
25A4: 67              LD      H,A                 
25A5: C6 07           ADD     A,$07               
25A7: FE 0E           CP      $0E                 ; within seven of column zero, wrapping through it?
25A9: DA D6 25        JP      C,loc_25d6          ; it has run off the end -- retire it
25AC: DD 7E 05        LD      A,(IX+$05)          ; which row it was spawned on
25AF: FE 7C           CP      $7C                 
25B1: CA C0 25        JP      Z,loc_25c0          ; the upper row's object splits at mid-screen
25B4: 3A A6 63        LD      A,(workRam+3A6)     ; everything else rides object 3's step
25B7: 84              ADD     A,H                 
25B8: DD 77 03        LD      (IX+$03),A          ; move it

loc_25bb:
25BB: DD 19           ADD     IX,DE               
25BD: 10 DB           DJNZ    loc_259a            ; 
25BF: C9              RET                         

loc_25c0:
25C0: 7C              LD      A,H                 
25C1: FE 80           CP      $80                 ; the exact middle of its travel
25C3: CA D6 25        JP      Z,loc_25d6          ; it reached the middle -- retire it
25C6: 3A A5 63        LD      A,(workRam+3A5)     ; past the middle, object 2's step
25C9: D2 CF 25        JP      NC,loc_25cf         ; 
25CC: 3A A4 63        LD      A,(workRam+3A4)     ; short of it, the mirror of that step

loc_25cf:
25CF: 84              ADD     A,H                 
25D0: DD 77 03        LD      (IX+$03),A          ; move it
25D3: C3 BB 25        JP      loc_25bb            ; 

loc_25d6:
25D6: 21 B8 69        LD      HL,$69B8            ; retiring: walk to this record's sprite slot
25D9: 3E 06           LD      A,$06               
25DB: 90              SUB     B                   ; how many records in, four sprite bytes each

loc_25dc:
25DC: CA E7 25        JP      Z,loc_25e7          ; 
25DF: 2C              INC     L                   
25E0: 2C              INC     L                   
25E1: 2C              INC     L                   
25E2: 2C              INC     L                   
25E3: 3D              DEC     A                   
25E4: C3 DC 25        JP      loc_25dc            ; 

loc_25e7:
25E7: AF              XOR     A                   
25E8: DD 77 00        LD      (IX+$00),A          ; free the record
25EB: DD 77 03        LD      (IX+$03),A          ; blank the column it died in
25EE: 77              LD      (HL),A              ; and blank its sprite, so it leaves the screen
25EF: C3 BB 25        JP      loc_25bb            ; 

; The 50m board's per-frame object update. A board gate with mask 0x02 opens it ONLY on
; 50m; on any other board it is dispatched constantly and the body never runs. When it
; does run it drives the board's object state in the fixed order the mover depends on:
; conveyor object 1's reverse-timer / step-direction driver, object 2's (gated by Mario's
; vertical position), object 3's, and then the carry -- read which conveyor row Mario is
; standing on and move his X by that row's freshly published step, so he rides the
; platform. The three drivers MUST run before the carry, because each publishes the
; signed step the carry consumes.
update50mConveyorObjects:
25F2: 3E 02           LD      A,$02               ; board mask: 50m only
25F4: F7              RST     $30                 ; closed on any other board -- skip the body
25F5: CD 02 26        CALL    loc_2602            ; conveyor object 1's step driver
25F8: CD 2F 26        CALL    loc_262f            ; object 2's
25FB: CD 79 26        CALL    loc_2679            ; object 3's
25FE: CD D3 2A        CALL    carryMarioOnConveyorRow; carry Mario along his conveyor row
2601: C9              RET                         

loc_2602:
2602: 3A 1A 60        LD      A,(workRam+1A)      ; read the frame counter
2605: 0F              RRCA                        
2606: DA 16 26        JP      C,loc_2616          ; odd frame -- skip the turnaround timer
2609: 21 A0 62        LD      HL,$62A0            ; object 1's reversal timer
260C: 35              DEC     (HL)                
260D: C2 16 26        JP      NZ,loc_2616         ; 
2610: 36 80           LD      (HL),$80            ; reload the full period
2612: 2C              INC     L                   ; point at the step-direction latch
2613: CD DE 26        CALL    reverseStepDirection; flip which way the object travels

loc_2616:
2616: 21 A1 62        LD      HL,$62A1            ; object 1's step-direction latch
2619: CD E9 26        CALL    signStepHalfRate    ; reduce it to a 0 / +-1 step
261C: 32 A3 63        LD      (workRam+3A3),A     ; publish the step the mover reads
261F: 3A 1A 60        LD      A,(workRam+1A)      ; 
2622: E6 1F           AND     $1F                 ; only every 32nd frame animates
2624: FE 01           CP      $01                 
2626: C0              RET     NZ                  
2627: 11 E4 69        LD      DE,$69E4            ; object 1's mirrored sprite-code pair
262A: EB              EX      DE,HL               
262B: CD A6 26        CALL    loc_26a6            ; advance the pair one walk frame
262E: C9              RET                         

loc_262f:
262F: 21 A3 62        LD      HL,$62A3            ; object 2's step-direction latch
2632: 3A 05 62        LD      A,(workRam+205)     ; read Mario's screen row
2635: FE C0           CP      $C0                 ; is he high on the screen?
2637: DA 6F 26        JP      C,loc_266f          ; yes -- force this object's step negative
263A: 3A 1A 60        LD      A,(workRam+1A)      ; read the frame counter
263D: 0F              RRCA                        
263E: DA 4C 26        JP      C,loc_264c          ; odd frame -- skip the timer
2641: 2D              DEC     L                   ; back to object 2's reversal timer
2642: 35              DEC     (HL)                
2643: C2 4C 26        JP      NZ,loc_264c         ; 
2646: 36 C0           LD      (HL),$C0            ; reload object 2's period
2648: 2C              INC     L                   ; on to the step-direction latch
2649: CD DE 26        CALL    reverseStepDirection; reverse which way it travels

loc_264c:
264C: 21 A3 62        LD      HL,$62A3            ; object 2's step-direction latch
264F: CD E9 26        CALL    signStepHalfRate    ; reduce it to a 0 / +-1 step
2652: 32 A5 63        LD      (workRam+3A5),A     ; publish the positive-going step
2655: ED 44           NEG                         ; negate it
2657: 32 A4 63        LD      (workRam+3A4),A     ; publish the other polarity too
265A: 3A 1A 60        LD      A,(workRam+1A)      ; read the frame counter
265D: E6 1F           AND     $1F                 
265F: C0              RET     NZ                  ; animate only on every 32nd frame
2660: 2D              DEC     L                   ; the reversal timer doubles as arm select
2661: 11 EC 69        LD      DE,$69EC            ; object 2's mirrored sprite-code pair
2664: EB              EX      DE,HL               
2665: CD A6 26        CALL    loc_26a6            ; advance the pair one walk frame
2668: E6 7F           AND     $7F                 ; drop the horizontal-flip bit
266A: 21 ED 69        LD      HL,$69ED            
266D: 77              LD      (HL),A              ; re-stamp the pair's low cell
266E: C9              RET                         

loc_266f:
266F: CB 7E           BIT     7,(HL)              ; is it already stepping negative?
2671: C2 4C 26        JP      NZ,loc_264c         ; 
2674: 36 FF           LD      (HL),$FF            ; force the latch negative
2676: C3 4C 26        JP      loc_264c            ; 

loc_2679:
2679: 3A 1A 60        LD      A,(workRam+1A)      ; read the frame counter
267C: 0F              RRCA                        
267D: DA 8D 26        JP      C,loc_268d          ; odd frame -- skip the timer
2680: 21 A5 62        LD      HL,$62A5            ; object 3's reversal timer
2683: 35              DEC     (HL)                
2684: C2 8D 26        JP      NZ,loc_268d         ; 
2687: 36 FF           LD      (HL),$FF            ; reload object 3's period
2689: 2C              INC     L                   ; on to the step-direction latch
268A: CD DE 26        CALL    reverseStepDirection; reverse which way it travels

loc_268d:
268D: 21 A6 62        LD      HL,$62A6            ; object 3's step-direction latch
2690: CD E9 26        CALL    signStepHalfRate    ; reduce it to a 0 / +-1 step
2693: 32 A6 63        LD      (workRam+3A6),A     ; publish object 3's step
2696: 3A 1A 60        LD      A,(workRam+1A)      ; read the frame counter
2699: E6 1F           AND     $1F                 
269B: FE 02           CP      $02                 ; animate only on every 32nd frame
269D: C0              RET     NZ                  
269E: 11 F4 69        LD      DE,$69F4            ; object 3's mirrored sprite-code pair
26A1: EB              EX      DE,HL               
26A2: CD A6 26        CALL    loc_26a6            ; advance the pair one walk frame
26A5: C9              RET                         

loc_26a6:
26A6: 2C              INC     L                   ; step to the pair's low counter
26A7: 1A              LD      A,(DE)              ; the caller's direction-select byte
26A8: 17              RLA                         
26A9: DA C5 26        JP      C,loc_26c5          ; select bit set -- run the mirrored arm
26AC: 7E              LD      A,(HL)              
26AD: 3C              INC     A                   ; count the low counter up
26AE: FE 53           CP      $53                 ; past the last of its three frames?
26B0: C2 B5 26        JP      NZ,loc_26b5         ; 
26B3: 3E 50           LD      A,$50               ; wrap back to the first

loc_26b5:
26B5: 77              LD      (HL),A              
26B6: 7D              LD      A,L                 ; the mirrored counter sits four bytes on
26B7: C6 04           ADD     A,$04               
26B9: 6F              LD      L,A                 
26BA: 7E              LD      A,(HL)              
26BB: 3D              DEC     A                   ; count that one the other way
26BC: FE CF           CP      $CF                 ; past the end of its three frames?
26BE: C2 C3 26        JP      NZ,loc_26c3         ; 
26C1: 3E D2           LD      A,$D2               ; wrap it, mirror bit set

loc_26c3:
26C3: 77              LD      (HL),A              
26C4: C9              RET                         

loc_26c5:
26C5: 7E              LD      A,(HL)              
26C6: 3D              DEC     A                   ; count the low counter down
26C7: FE 4F           CP      $4F                 ; below the first of its three frames?
26C9: C2 CE 26        JP      NZ,loc_26ce         ; 
26CC: 3E 52           LD      A,$52               ; wrap round to the last

loc_26ce:
26CE: 77              LD      (HL),A              
26CF: 7D              LD      A,L                 ; the mirrored counter, four bytes on
26D0: C6 04           ADD     A,$04               
26D2: 6F              LD      L,A                 
26D3: 7E              LD      A,(HL)              
26D4: 3C              INC     A                   ; count that one the other way
26D5: FE D3           CP      $D3                 ; past the end of its three frames?
26D7: C2 DC 26        JP      NZ,loc_26dc         ; 
26DA: 3E D0           LD      A,$D0               ; wrap it, mirror bit set

loc_26dc:
26DC: 77              LD      (HL),A              
26DD: C9              RET                         

; Reverse a timed sprite object's travel by flipping the sign of the one-byte signed step
; that steers it. The caller hands over a pointer to that byte; bit 7 is the direction.
; The byte is rewritten to +2 when it was negative and to -2 when it was not, so the sign
; always comes out opposite to what went in. The MAGNITUDE is reset to 2 whatever it was,
; and no reader looks at it -- the readers take the sign alone, to choose which of two
; movement offsets to apply and which way to publish the object's step -- so the
; observable effect is a direction reversal and nothing else.
reverseStepDirection:
26DE: CB 7E           BIT     7,(HL)              ; which way is it travelling now?
26E0: CA E6 26        JP      Z,loc_26e6          ; 
26E3: 36 02           LD      (HL),$02            ; it was negative -- make it positive
26E5: C9              RET                         

loc_26e6:
26E6: 36 FE           LD      (HL),$FE            ; it was positive -- make it negative
26E8: C9              RET                         

; Collapse a direction byte to a unit step, every other frame. The caller keeps a signed
; direction byte per moving channel and hands over its address. On an EVEN frame the
; routine returns a step of zero and writes nothing, so the channel stands still. On an
; ODD frame the byte's sign bit decides -- negative gives -1, anything else +1 -- and
; the step is written back over the byte as well as returned. So the byte is a
; persistent direction LATCH whose magnitude is thrown away, and what the caller
; publishes pulses 0 / +-1 across frames: a fixed direction delivered at HALF the frame
; rate. Reversing the direction is done elsewhere.
signStepHalfRate:
26E9: 3A 1A 60        LD      A,(workRam+1A)      ; read the frame counter
26EC: E6 01           AND     $01                 
26EE: C8              RET     Z                   ; even frame -- a zero step, the channel holds
26EF: CB 7E           BIT     7,(HL)              ; the latch's sign picks the direction
26F1: 3E FF           LD      A,$FF               
26F3: C2 F8 26        JP      NZ,loc_26f8         ; 
26F6: 3E 01           LD      A,$01               

loc_26f8:
26F8: 77              LD      (HL),A              ; write the unit step back and hand it out
26F9: C9              RET                         

; The 75m per-frame service router, and the clearest difficulty ramp in the game. A
; board gate opens the whole routine on board 3 only. First, marioY at or above 240 --
; larger Y is lower, so this is Mario at the very bottom of the display -- kills him.
; The test is on marioY alone, with NO X-band test, so he need not be on a lift at all.
; Otherwise it services on a cadence keyed to the frame counter, and the cadence DOUBLES
; after level 1: at level 1 it advances and spawns the board objects on one frame in
; four, runs the vertical-reposition machine on another of the four, and idles the
; remaining two -- and since the frame counter counts DOWN, the reposition frame comes
; round before the object frame, not after it; at
; every other level it alternates every frame with no idle at all. So both run twice as
; often from level 2 on. The test is level != 1, not level >= 2, so level 0 takes the
; fast cadence too.
service75mBoard:
26FA: 3E 04           LD      A,$04               ; board mask: 75m only
26FC: F7              RST     $30                 ; closed on any other board -- skip the body
26FD: 3A 05 62        LD      A,(workRam+205)     ; read Mario's screen row
2700: FE F0           CP      $F0                 ; off the bottom of the display?
2702: D2 7F 27        JP      NC,killMarioAtEndOfLiftTravel; yes -- that kills him
2705: 3A 29 62        LD      A,(workRam+229)     ; read the level
2708: 3D              DEC     A                   
2709: 3A 1A 60        LD      A,(workRam+1A)      ; read the frame counter
270C: C2 1A 27        JP      NZ,loc_271a         ; past level 1 -- take the faster cadence
270F: E6 03           AND     $03                 ; level 1 services one frame in four
2711: FE 01           CP      $01                 
2713: CA 1E 27        JP      Z,loc_271e          ; this frame runs the lift-ride machine
2716: DA 22 27        JP      C,serviceBoardObjects; and this one the board objects
2719: C9              RET                         ; the other two frames idle

loc_271a:
271A: 0F              RRCA                        
271B: DA 22 27        JP      C,serviceBoardObjects; alternate every frame, no idle at all

loc_271e:
271E: CD 45 27        CALL    dispatchElevatorRideByColumn; run the lift-ride machine
2721: C9              RET                         

; Service the six 16-byte records of objArray66 for one pass, then publish them. Three
; things, in order. ADVANCE: step every active object one pixel toward its limit, landing
; or deactivating it on arrival. SPAWN: on the spawn cadence, claim a free slot and seed
; a new object; otherwise just tick the cadence timer. PUBLISH: walk all six records and
; copy each object's X and Y into its own 4-byte sprite record in the shadow buffer, X at
; the record's first byte and Y at its fourth, the six sitting consecutively from
; spriteBuffer + 88. The order matters -- advance and spawn are what MOVE the objects,
; and the publish walk reads the positions they just produced.
serviceBoardObjects:
2722: CD 97 27        CALL    advanceBoardObjectTravel; step every active object one pixel
2725: CD DA 27        CALL    spawnBoardObject    ; spawn a new one on the cadence
2728: 06 06           LD      B,$06               ; six records to publish
272A: 11 10 00        LD      DE,$0010            ; 16 bytes per object record
272D: 21 58 69        LD      HL,$6958            ; the six sprite records that draw them
2730: DD 21 00 66     LD      IX,$6600            ; the object array

loc_2734:
2734: DD 7E 03        LD      A,(IX+$03)          ; the object's X
2737: 77              LD      (HL),A              ; into the sprite record's first byte
2738: 2C              INC     L                   
2739: 2C              INC     L                   
273A: 2C              INC     L                   
273B: DD 7E 05        LD      A,(IX+$05)          ; the object's Y
273E: 77              LD      (HL),A              ; into the sprite record's fourth byte
273F: 2C              INC     L                   
2740: DD 19           ADD     IX,DE               ; on to the next object record
2742: 10 F0           DJNZ    loc_2734            ; 
2744: C9              RET                         

; While Mario is riding a lift, route him to the up-column or the down-column carry by
; his X. Two guards drop the whole routine: the standing-on-a-lift flag clear, or Mario
; airborne, leaving the carry for a grounded frame. Past both it reads marioX and routes
; by band, one band per lift column: the lower band is the RISING column and carries him
; up, the higher band the DESCENDING one and carries him down. Each carry steps his Y one
; pixel with the lift, mirrors that to his sprite record, and hands off to a check that
; KILLS Mario -- clearing marioActive -- once his Y crosses that column's own fixed row
; limit, read without reference to where the lift has got to. The bands are the columns
; because a lift is spawned at X 55 and teleported to X 119 the instant it finishes
; rising, dead centre of one band each. A third arm covers every other X.
dispatchElevatorRideByColumn:
2745: 3A 98 63        LD      A,(workRam+398)     ; the lift flag: is he riding one?
2748: A7              AND     A                   
2749: C8              RET     Z                   ; no -- nothing to carry
274A: 3A 16 62        LD      A,(workRam+216)     ; is he airborne?
274D: A7              AND     A                   
274E: C0              RET     NZ                  ; yes -- carry him only on a grounded frame
274F: 3A 03 62        LD      A,(workRam+203)     ; read his column
2752: FE 2C           CP      $2C                 ; left of both lift columns
2754: DA 66 27        JP      C,loc_2766          ; 
2757: FE 43           CP      $43                 ; in the rising column -- carry him up
2759: DA 6F 27        JP      C,carryMarioUpWithLift; 
275C: FE 6C           CP      $6C                 ; between the two columns
275E: DA 66 27        JP      C,loc_2766          ; 
2761: FE 83           CP      $83                 ; in the descending column -- carry him down
2763: DA 87 27        JP      C,carryMarioDownWithLift; 

loc_2766:
2766: AF              XOR     A                   
2767: 32 98 63        LD      (workRam+398),A     ; clear the lift flag
276A: 3C              INC     A                   
276B: 32 21 62        LD      (workRam+221),A     ; and start him falling
276E: C9              RET                         

; The rising arm of the lift ride. Once marioY has come up past the limit row 0x71 --
; smaller Y is higher on this screen -- it hands off to the kill, which clears
; marioActive; the death animation runs and a life is lost. Otherwise it takes one off
; marioY, moving him a single pixel up, and mirrors the new value into the Y field of
; marioSpriteRecord so the sprite follows the same frame. The limit is an ABSOLUTE row
; of Mario's, not the lift's end of travel: this arm never reads an object record, and
; since he rides eleven or twelve pixels above the platform he is standing on, he
; crosses the limit row while the platform still has climb left and it carries on past
; him.
carryMarioUpWithLift:
276F: 3A 05 62        LD      A,(workRam+205)     ; read Mario's screen row
2772: FE 71           CP      $71                 ; has he risen past the top limit row?
2774: DA 7F 27        JP      C,killMarioAtEndOfLiftTravel; yes -- the ride kills him
2777: 3D              DEC     A                   ; one pixel up the screen
2778: 32 05 62        LD      (workRam+205),A     ; 
277B: 32 4F 69        LD      (workRam+94F),A     ; mirror it into his sprite record's Y
277E: C9              RET                         

; Kill Mario at the end of his run on a 75m lift. Both carries tail in here the moment
; his Y leaves the band their arm allows: the up-carry steps Y down one a frame, which
; is UP the screen, and arrives once his row is below 113; the down-carry steps it up
; and arrives at row 232 or beyond. This then unconditionally clears marioActive and
; edgeRepositionFlag and returns -- no inputs, no branches, always the same two zeros.
; Zeroing marioActive IS the game's kill primitive, not merely switching the mover off:
; it freezes Mario and runs the death animation, then the life decrement, then the
; respawn. Both carries compare an ABSOLUTE Mario row against a constant and read no
; lift record, so the two limits do not coincide with the platform's -- on the up arm
; the rising column runs on another 27-28 pixels after he is dead. A third caller
; reaches the same write: Mario dropping off the bottom of a 75m board.
killMarioAtEndOfLiftTravel:
277F: AF              XOR     A                   
2780: 32 00 62        LD      (workRam+200),A     ; clear the alive flag -- he dies here
2783: 32 98 63        LD      (workRam+398),A     ; and the lift flag with it
2786: C9              RET                         

; Carry Mario one pixel DOWN the screen while he rides a descending lift, or kill him
; once he reaches the bottom of his own run. On the lift board the lifts run in two
; columns, one rising and one descending, and this is the arm the ride handler picks
; while Mario stands in the descending column. marioY is the sole input: still above the
; bottom-of-run row and his Y is advanced by one -- a single pixel down, since larger Y
; is lower -- and mirrored into the Y field of his sprite record so the drawn sprite
; tracks the move; at or past that row his run is over and control passes to the kill,
; which clears the flag keeping him active. The limit is an absolute screen row for
; MARIO, not the lift's end of travel: no object record is read here at all, and the lift
; itself keeps going a few pixels past the row that kills him.
carryMarioDownWithLift:
2787: 3A 05 62        LD      A,(workRam+205)     ; read Mario's screen row
278A: FE E8           CP      $E8                 ; reached the bottom of his run?
278C: D2 7F 27        JP      NC,killMarioAtEndOfLiftTravel; yes -- the ride kills him
278F: 3C              INC     A                   ; one pixel down the screen
2790: 32 05 62        LD      (workRam+205),A     ; 
2793: 32 4F 69        LD      (workRam+94F),A     ; mirror it into his sprite record's Y
2796: C9              RET                         

; Advance the six board objects in the 0x6600 array: each active object drifts one pixel
; vertically toward its limit, then lands or deactivates on arrival. The array is 75m-only
; and the drift matches the moving platforms, but nothing in the code names these objects,
; so this describes what they DO rather than what they are. The array is six 16-byte records, all
; six walked once per board-object service pass, and a record is processed only while bit
; 0 of its active flag is set. Bit 3 of the state byte picks the direction, and larger Y
; is lower on screen. Bit 3 set means rising, so Y decreases, and on reaching row 96 the
; object LANDS -- X snaps to column 119 and the state byte becomes 4, which clears bit 3
; so the next pass falls. Bit 3 clear means falling, so Y increases, and on reaching row
; 248 the object DEACTIVATES. The 16-byte stride is RETURNED, and load-bearing: the spawn
; walk that runs next in the same pass reuses it without reloading.
advanceBoardObjectTravel:
2797: 06 06           LD      B,$06               ; six records to walk
2799: 11 10 00        LD      DE,$0010            ; 16-byte stride -- the spawn walk reuses it
279C: DD 21 00 66     LD      IX,$6600            ; the object array

loc_27a0:
27A0: DD CB 00 46     BIT     0,(IX+$00)          ; skip an inactive slot
27A4: CA C2 27        JP      Z,loc_27c2          ; 
27A7: DD CB 0D 5E     BIT     3,(IX+$0D)          ; bit 3 of the state picks rising or falling
27AB: CA C7 27        JP      Z,loc_27c7          ; clear -- this one is falling
27AE: DD 7E 05        LD      A,(IX+$05)          
27B1: 3D              DEC     A                   ; rising: one pixel up the screen
27B2: DD 77 05        LD      (IX+$05),A          
27B5: FE 60           CP      $60                 ; reached the landing row?
27B7: C2 C2 27        JP      NZ,loc_27c2         ; 
27BA: DD 36 03 77     LD      (IX+$03),$77        ; snap it to the landing column
27BE: DD 36 0D 04     LD      (IX+$0D),$04        ; clear bit 3, so the next pass falls

loc_27c2:
27C2: DD 19           ADD     IX,DE               ; on to the next record
27C4: 10 DA           DJNZ    loc_27a0            ; 
27C6: C9              RET                         

loc_27c7:
27C7: DD 7E 05        LD      A,(IX+$05)          
27CA: 3C              INC     A                   ; falling: one pixel down the screen
27CB: DD 77 05        LD      (IX+$05),A          
27CE: FE F8           CP      $F8                 ; off the bottom of its travel?
27D0: C2 C2 27        JP      NZ,loc_27c2         ; 
27D3: DD 36 00 00     LD      (IX+$00),$00        ; deactivate the slot
27D7: C3 C2 27        JP      loc_27c2            ; 

; The periodic allocator behind the objArray66 board objects, called once per pass from
; the object service after the live ones have been animated. While spawnTimer is nonzero
; nothing spawns -- the timer is simply ticked down one and it returns. At zero it scans
; the six records for the first whose active flag's low bit is clear, claims it, marks
; it active and stamps a fixed spawn position (X 0x37, Y 0xF8, near the bottom of the
; screen) with initial state 0x08, then reloads the cooldown to 0x34 and ticks it once,
; so it lands at reload minus one. If every slot is busy it gives up WITHOUT reloading
; or ticking, leaving the timer at zero so it retries every following pass. What kind of
; object this seeds is not claimed here.
spawnBoardObject:
27DA: 21 A7 62        LD      HL,$62A7            ; the spawn-cadence timer
27DD: 7E              LD      A,(HL)              
27DE: A7              AND     A                   
27DF: C2 06 28        JP      NZ,decrementByteAt  ; still cooling down -- just tick it
27E2: 06 06           LD      B,$06               ; six slots to try
27E4: DD 21 00 66     LD      IX,$6600            

loc_27e8:
27E8: DD CB 00 46     BIT     0,(IX+$00)          ; look for a free slot
27EC: CA F4 27        JP      Z,loc_27f4          ; 
27EF: DD 19           ADD     IX,DE               
27F1: 10 F5           DJNZ    loc_27e8            ; on to the next slot
27F3: C9              RET                         ; every slot busy -- retry next pass

loc_27f4:
27F4: DD 36 00 01     LD      (IX+$00),$01        ; claim the slot
27F8: DD 36 03 37     LD      (IX+$03),$37        ; seed its column
27FC: DD 36 05 F8     LD      (IX+$05),$F8        ; and its row, near the bottom of the screen
2800: DD 36 0D 08     LD      (IX+$0D),$08        ; state 8: bit 3 set, so it starts rising
2804: 36 34           LD      (HL),$34            ; reload the cooldown

; Take one off the byte at the address handed in -- load, subtract one (0 wrapping back
; to 255), store it back, and nothing else. The target is a parameter, not a fixed cell.
decrementByteAt:
2806: 35              DEC     (HL)                ; tick the caller's byte down by one
2807: C9              RET                         

; Kill Mario when a board object overlaps his hitbox. Once a frame it asks the current
; board's collision handler whether any active object -- barrel, fireball, oil flame --
; overlaps him. The test is a bounding box centred on Mario: his Y goes in as the compare
; coordinate, his X is read by the handler out of his context block, and the half-extents
; are 4 wide and 7 tall. The handler sweeps its arrays and reports 1 or 0. On 0 nothing
; happens; on 1 that byte minus one, i.e. 0, goes into marioActive, which the movement
; machine reads as death and turns into the death, life-loss and respawn cycle.
killMarioOnObjectCollision:
2808: FD 21 00 62     LD      IY,$6200            ; Mario's record is the reference point
280C: 3A 05 62        LD      A,(workRam+205)     ; read his screen row
280F: 4F              LD      C,A                 
2810: 21 07 04        LD      HL,$0407            ; half-extents: 4 across and 7 down
2813: CD 6F 28        CALL    dispatchBoardCollision; ask this board's collision handler

loc_2816:
2816: A7              AND     A                   
2817: C8              RET     Z                   ; nothing overlapped him
2818: 3D              DEC     A                   ; a hit reports 1; one less is 0
2819: 32 00 62        LD      (workRam+200),A     ; clear the alive flag -- he dies
281C: C9              RET                         

recordHammerHitOnObject:
281D: 06 02           LD      B,$02               ; the hammer pair: two records
281F: 11 10 00        LD      DE,$0010            
2822: FD 21 80 66     LD      IY,$6680            

loc_2826:
2826: FD CB 01 46     BIT     0,(IY+$01)          ; which one is in play?
282A: C2 32 28        JP      NZ,loc_2832         ; 
282D: FD 19           ADD     IY,DE               
282F: 10 F5           DJNZ    loc_2826            ; 
2831: C9              RET                         ; no hammer in play -- nothing to test

loc_2832:
2832: FD 4E 05        LD      C,(IY+$05)          ; the hammer's own row
2835: FD 66 09        LD      H,(IY+$09)          ; and its hit box, re-stamped each frame from the swing pose
2838: FD 6E 0A        LD      L,(IY+$0A)          
283B: CD 6F 28        CALL    dispatchBoardCollision; what is the hammer touching?
283E: A7              AND     A                   
283F: C8              RET     Z                   ; it struck nothing this frame
2840: 32 50 63        LD      (workRam+350),A     ; raise the hit-effect latch: play is suspended
2843: 3A B9 63        LD      A,(workRam+3B9)     ; the count the sweep started with
2846: 90              SUB     B                   ; less the ones still to go: which record was struck
2847: 32 54 63        LD      (workRam+354),A     ; 
284A: 7B              LD      A,E                 ; the stride the board's sweep was walking
284B: 32 53 63        LD      (workRam+353),A     ; 
284E: DD 22 51 63     LD      (workRam+351),IX    ; and the array it came from
2852: C9              RET                         

; Run the current board's object-overlap search for Mario and hand its severity code
; back. A thin setup-and-dispatch step: it stages the object-record base the board's arm
; walks, a search bound taken from Mario's Y plus a fixed offset of 12 -- so the probe
; point is twelve pixels BELOW him, not on him -- and an overlap-threshold word chosen by
; whether a left/right direction is held, whose low and high bytes become the two
; per-axis thresholds. The dispatch is a genuine tail call. The board's arm counts how
; many active objects overlap and grades the total into 0, 1, 3 or 7 -- not a scale but a
; unary thermometer, zero to three bits set, because the consumer walks those low bits
; one at a time. This is the "how many did he jump over?" search, not the "did something
; hit him?" one.
searchPlayerObjectOverlap:
2853: FD 21 00 62     LD      IY,$6200            ; Mario's record is the reference point
2857: 3A 05 62        LD      A,(workRam+205)     ; read his screen row
285A: C6 0C           ADD     A,$0C               ; probe twelve pixels below him
285C: 4F              LD      C,A                 
285D: 3A 10 60        LD      A,(workRam+10)      ; read the control word
2860: E6 03           AND     $03                 ; is left or right being held?
2862: 21 08 05        LD      HL,$0508            ; the standing search window
2865: CA 6B 28        JP      Z,loc_286b          ; 
2868: 21 08 13        LD      HL,$1308            ; a wider window across while he moves

loc_286b:
286B: CD 88 3E        CALL    dispatchBoardOverlapSearch; run this board's overlap sweep
286E: C9              RET                         

; Vector a collision test to the current board's handler. It reads board and vectors
; through a six-entry jump table -- 1 = 25m girders, 2 = 50m, 3 = 75m, 4 = 100m rivets,
; with entries 0 and 5 null guard slots -- and board is never range-checked, so a stray
; value vectors off the table. The caller's position is genuine DATA, not call plumbing:
; it is PUSHED first, beneath the dispatch's own frame, and every handler recovers it
; with a pop as its opening act; drop the push and each handler pops the wrong stack
; word. The table index is an 8-BIT double of the board number, so 128 or more wraps the
; offset back into the start of the table rather than reading past its end. Each handler
; sweeps its board's object records for one overlapping the caller's position and leaves
; the hit/miss result its callers read back.
dispatchBoardCollision:
286F: 3A 27 62        LD      A,(workRam+227)     ; read the current board
2872: E5              PUSH    HL                  ; the tolerance pair, for the handler to pop
2873: EF              RST     $28                 ; vector through the per-board table below

; ---- $2874-$287F: jump table ----
2874: 00 00 80 28 B0 28 E0 28 01 29 00 00

; The 25m arm of the per-board collision search, reached through the dispatcher's board
; table with the per-axis search tolerances already pushed. It recovers that pair and
; runs the shared bounding-box search over three object arrays in turn: ten objArray67
; barrel records at stride 0x20, then five objArray64 records at the same stride, then a
; single record with stride zero. Before each sweep it stamps that sweep's record count
; into objSearchCount, where the hit handler reads it back to recover the matched
; record's index. The FIRST sweep to find an overlapping record wins and stops the
; remaining sweeps; all three coming up empty reaches the same place the normal way.
; This is the only collision arm that sweeps the barrel array, which is why the board
; table vectors here for exactly this board.
search25mObjectOverlap:
2880: E1              POP     HL                  ; recover the per-axis tolerances
2881: 06 0A           LD      B,$0A               ; ten barrel records
2883: 78              LD      A,B                 
2884: 32 B9 63        LD      (workRam+3B9),A     ; stamp the count, for index recovery
2887: 11 20 00        LD      DE,$0020            ; stride between barrel records
288A: DD 21 00 67     LD      IX,$6700            ; the barrel array
288E: CD 13 29        CALL    findCollidingObject ; first sweep -- a hit unwinds past here
2891: 06 05           LD      B,$05               ; five fire records
2893: 78              LD      A,B                 
2894: 32 B9 63        LD      (workRam+3B9),A     ; stamp this sweep's count
2897: 1E 20           LD      E,$20               
2899: DD 21 00 64     LD      IX,$6400            ; the fire array
289D: CD 13 29        CALL    findCollidingObject ; second sweep
28A0: 06 01           LD      B,$01               ; one record: the board's fixed hazard
28A2: 78              LD      A,B                 
28A3: 32 B9 63        LD      (workRam+3B9),A     ; stamp this sweep's count
28A6: 1E 00           LD      E,$00               
28A8: DD 21 A0 66     LD      IX,$66A0            ; the oil drum
28AC: CD 13 29        CALL    findCollidingObject ; third sweep
28AF: C9              RET                         

; The 50m arm of the board collision dispatch: three bounding-box sweeps in order,
; stopping at the first overlap. The dispatcher pushes the per-axis tolerances before
; jumping here; this recovers that pair and then runs the shared collision search three
; times, each time pointing it at a different object array and stamping that sweep's
; record count into objSearchCount, from which the found-handler recovers the matched
; record's index. Sweep 1 is the five-record array at stride 0x20, sweep 2 the six-record
; array of this board's own movers at stride 0x10, sweep 3 a single record at stride
; zero. A hit splices control straight back to the dispatch site and abandons the
; remaining sweeps, so the stored count is whichever sweep terminated the routine.
search50mObjectOverlap:
28B0: E1              POP     HL                  ; recover the per-axis tolerances
28B1: 06 05           LD      B,$05               ; five fire records
28B3: 78              LD      A,B                 
28B4: 32 B9 63        LD      (workRam+3B9),A     ; stamp the count, for index recovery
28B7: 11 20 00        LD      DE,$0020            
28BA: DD 21 00 64     LD      IX,$6400            ; the fire array
28BE: CD 13 29        CALL    findCollidingObject ; first sweep -- a hit unwinds past here
28C1: 06 06           LD      B,$06               ; six of this board's travelling objects
28C3: 78              LD      A,B                 
28C4: 32 B9 63        LD      (workRam+3B9),A     ; stamp this sweep's count
28C7: 1E 10           LD      E,$10               
28C9: DD 21 A0 65     LD      IX,$65A0            ; the 50m moving-object array
28CD: CD 13 29        CALL    findCollidingObject ; second sweep
28D0: 06 01           LD      B,$01               ; one record: the board's fixed hazard
28D2: 78              LD      A,B                 
28D3: 32 B9 63        LD      (workRam+3B9),A     ; stamp this sweep's count
28D6: 1E 00           LD      E,$00               
28D8: DD 21 A0 66     LD      IX,$66A0            
28DC: CD 13 29        CALL    findCollidingObject ; third sweep
28DF: C9              RET                         

; The 75m arm of the board-overlap-search dispatch: two bounding-box sweeps, stopping at
; the first hit. It recovers the caller's per-axis tolerances from the stack and points
; the shared bounding-box search at two object arrays in turn -- first objArray64, 0x20
; stride, 5 records, then objArray65, 0x10 stride, 10 records. Before each sweep it
; records that sweep's object count in objSearchCount, where the found-handler reads it
; back to recover the matched record's index as the count minus the search's residual B.
; The short-circuit is what distinguishes this from the single-sweep arms: if sweep 1
; hits, the search takes its caller-skip return and unwinds PAST this routine, so sweep 2
; never runs and objSearchCount is left holding 5, not 10.
search75mObjectOverlap:
28E0: E1              POP     HL                  ; recover the per-axis tolerances
28E1: 06 05           LD      B,$05               ; five fire records
28E3: 78              LD      A,B                 
28E4: 32 B9 63        LD      (workRam+3B9),A     ; stamp the count, for index recovery
28E7: 11 20 00        LD      DE,$0020            
28EA: DD 21 00 64     LD      IX,$6400            ; the fire array
28EE: CD 13 29        CALL    findCollidingObject ; first sweep -- a hit unwinds past here
28F1: 06 0A           LD      B,$0A               ; ten spring records
28F3: 78              LD      A,B                 
28F4: 32 B9 63        LD      (workRam+3B9),A     ; stamp this sweep's count
28F7: 1E 10           LD      E,$10               
28F9: DD 21 00 65     LD      IX,$6500            ; the spring array
28FD: CD 13 29        CALL    findCollidingObject ; second sweep, only reached on a miss
2900: C9              RET                         

; The 100m arm of the board-overlap dispatch: one bounding-box collision sweep over
; objArray64, the hazard array 100m uses. It recovers the per-axis search tolerances the
; dispatcher pushed, records the sweep's object count in objSearchCount where the
; found-handler reads it back, and points the shared bounding-box search at objArray64
; with seven records at a 0x20 stride for a single scan. A hit sets the result byte to 1
; and leaves the count-minus-index residue behind so the matched record's index can be
; recovered; an exhausted scan leaves the result byte 0. It is the single-sweep member
; of a family -- the other boards run two or three sweeps over different arrays.
search100mObjectOverlap:
2901: E1              POP     HL                  ; recover the per-axis tolerances
2902: 06 07           LD      B,$07               ; seven fire records on this board
2904: 78              LD      A,B                 
2905: 32 B9 63        LD      (workRam+3B9),A     ; stamp the count, for index recovery
2908: 11 20 00        LD      DE,$0020            
290B: DD 21 00 64     LD      IX,$6400            ; the fire array
290F: CD 13 29        CALL    findCollidingObject ; the single sweep
2912: C9              RET                         

; The collision primitive: scan an object list for the first record whose bounding box
; overlaps a reference point on both axes. A board collision handler points it at one of
; its arrays and hands it a reference coordinate pair -- in practice Mario's position --
; plus a per-axis base tolerance. For each ACTIVE record:
;   axis 1: |reference - record+5| + 1 must fall inside the base window, or past that
;           inside the record's own extra span at +0x0a;
;   axis 2: |reference - record+3|     must fall inside the base window, or past that
;           inside the record's own extra span at +9.
; A record with bit 0 of its flag byte (+0) clear is inactive and skipped, as is one
; that fails either axis. The FIRST record passing both is a hit and the scan stops
; there, the caller's post-call code being skipped so its own caller resumes; the result
; byte is 1 on a hit and 0 on exhaustion, with the count less the matched index left for
; the caller to recover that index. A count of zero on entry is NOT guarded: it scans
; 256 records.
findCollidingObject:
2913: DD E5           PUSH    IX                  ; save the array base for the exit

loc_2915:
2915: DD CB 00 46     BIT     0,(IX+$00)          ; skip an inactive record
2919: CA 4C 29        JP      Z,loc_294c          ; 
291C: 79              LD      A,C                 ; the reference point on the first axis
291D: DD 96 05        SUB     (IX+$05)            ; distance out to this record
2920: D2 25 29        JP      NC,loc_2925         ; 
2923: ED 44           NEG                         ; take it unsigned

loc_2925:
2925: 3C              INC     A                   
2926: 95              SUB     L                   ; inside the base window?
2927: DA 30 29        JP      C,loc_2930          ; yes -- try the other axis
292A: DD 96 0A        SUB     (IX+$0A)            ; otherwise allow the record's own extra span
292D: D2 4C 29        JP      NC,loc_294c         ; still outside -- this record misses

loc_2930:
2930: FD 7E 03        LD      A,(IY+$03)          ; the reference point on the second axis
2933: DD 96 03        SUB     (IX+$03)            ; distance out to this record
2936: D2 3B 29        JP      NC,loc_293b         ; 
2939: ED 44           NEG                         ; take it unsigned

loc_293b:
293B: 94              SUB     H                   ; inside the base window?
293C: DA 45 29        JP      C,loc_2945          ; 
293F: DD 96 09        SUB     (IX+$09)            ; otherwise the record's own extra span
2942: D2 4C 29        JP      NC,loc_294c         ; still outside -- this record misses

loc_2945:
2945: 3E 01           LD      A,$01               ; both axes overlap -- report a hit
2947: DD E1           POP     IX                  
2949: 33              INC     SP                  ; step over the caller's return, skipping it
294A: 33              INC     SP                  
294B: C9              RET                         

loc_294c:
294C: DD 19           ADD     IX,DE               ; on to the next record
294E: 10 C5           DJNZ    loc_2915            ; 
2950: AF              XOR     A                   ; the list is exhausted -- no overlap
2951: DD E1           POP     IX                  
2953: C9              RET                         

; Latch whether Mario is touching one of the two hammer records, and mark which one. A
; board gate comes first, with a mask naming exactly the boards a hammer appears on --
; 25m, 50m and 100m; 75m is the one hammer-free board. Then a search tests his position
; against the two-record pair and leaves an overlap flag plus an index naming the matched
; record. The flag is stored UNCONDITIONALLY into marioHammerPending and drives the item
; sound trigger -- asserted for 64 frames on a touch, silenced on a miss -- so a run with
; no overlap clears a latch and a sound a previous run set. On an overlap only, the
; touched record's in-play field is set to 1, and both the hammer-hit collision scan and
; the hammer sprite driver read that mark to find the right record. Nothing here puts the
; hammer in Mario's hands: the movement machine transfers the pending flag into the
; held-hammer flag once the post-landing freeze expires.
latchHammerTouch:
2954: 3E 0B           LD      A,$0B               ; board mask: 25m, 50m and 100m carry hammers
2956: F7              RST     $30                 ; closed on 75m -- skip the body
2957: CD 74 29        CALL    findHammerOverlappingMario; is Mario touching either hammer?
295A: 32 18 62        LD      (workRam+218),A     ; publish the touch; a miss clears it
295D: 0F              RRCA                        ; turn a touch into 0x40
295E: 0F              RRCA                        
295F: 32 85 60        LD      (workRam+85),A      ; hold the pickup sound line for 64 frames
2962: 78              LD      A,B                 ; which record did the search match?
2963: A7              AND     A                   
2964: C8              RET     Z                   ; nothing overlapped -- nothing to mark
2965: FE 01           CP      $01                 ; a residue of 1 means the second record
2967: CA 6F 29        JP      Z,loc_296f          ; 
296A: DD 36 01 01     LD      (IX+$01),$01        ; mark the first hammer as the one in play
296E: C9              RET                         

loc_296f:
296F: DD 36 11 01     LD      (IX+$11),$01        ; mark the second hammer instead
2973: C9              RET                         

; Test whether Mario overlaps either of the two hammer objects, and report which one. A
; thin front end over the shared object-list bounding-box search, fixing its parameters
; for this one query: the records are the two-object hammer pair at the object-record
; stride; the reference point is Mario, his Y as the first axis and his X as the second,
; reached by aiming the reference pointer at the base of his live block; and the per-axis
; base tolerances are 8 and 4. The search stops at the first active record whose box
; overlaps him on both axes. This routine writes no memory at all and only reports the
; overlap -- the caller records a "touched but not yet held" hammer from the result and
; triggers the hammer-grab sound. Which of the two records is which hammer is not
; established.
findHammerOverlappingMario:
2974: FD 21 00 62     LD      IY,$6200            ; Mario's record supplies the reference X
2978: 3A 05 62        LD      A,(workRam+205)     ; read his screen row
297B: 4F              LD      C,A                 
297C: 21 08 04        LD      HL,$0408            ; allow 8 on the row and 4 across
297F: 06 02           LD      B,$02               ; the two hammer records
2981: 11 10 00        LD      DE,$0010            ; 16 bytes apart
2984: DD 21 80 66     LD      IX,$6680            ; the hammer pair
2988: CD 13 29        CALL    findCollidingObject ; 
298B: C9              RET                         

; Report that a fire has reached the edge of the ground it is walking on. The answer is
; the SOLE thing its caller uses to decide to turn the fire around. It takes the record
; objIterPtr points at and probes the tile 12 pixels BELOW the fire -- the same axis and
; offset Mario's own ground test uses. It does NOT look ahead of the fire, and the axis
; the fire travels along is left exactly as the record holds it. The verdict is a plain
; predicate on the tile found there: below 0xB0 is OUT of band; at or above 0xB0 with
; the low nibble 8 or more is OUT; at or above 0xB0 with the low nibble under 8 is IN.
; So the accepted band is the high half of the tile set with the low nibble kept under
; 8, and ground ends where that band ends, which is why an out-of-band answer is an EDGE
; and not just a different tile. It writes no memory.
turnFireAtGroundEdge:
298C: 2A C8 63        LD      HL,(workRam+3C8)    ; the record the fire driver is walking
298F: 7D              LD      A,L                 ; step out to the record's position pair
2990: C6 0E           ADD     A,$0E               
2992: 6F              LD      L,A                 
2993: 56              LD      D,(HL)              ; the fire's column
2994: 2C              INC     L                   
2995: 7E              LD      A,(HL)              
2996: C6 0C           ADD     A,$0C               ; twelve pixels below it -- the ground beneath
2998: 5F              LD      E,A                 
2999: EB              EX      DE,HL               
299A: CD F0 2F        CALL    tileAddrForPixel    ; find the tile that contains that point
299D: 7E              LD      A,(HL)              ; read the tile there
299E: FE B0           CP      $B0                 ; below the ground band -- not ground
29A0: DA AC 29        JP      C,loc_29ac          ; 
29A3: E6 0F           AND     $0F                 ; within the band, the low nibble decides
29A5: FE 08           CP      $08                 ; 8 or more is outside it too
29A7: D2 AC 29        JP      NC,loc_29ac         ; 
29AA: AF              XOR     A                   ; still on ground -- keep the fire walking
29AB: C9              RET                         

loc_29ac:
29AC: 3E 01           LD      A,$01               ; the ground has ended -- turn the fire round
29AE: C9              RET                         

loc_29af:
29AF: 3E 04           LD      A,$04               ; board mask: 75m only
29B1: F7              RST     $30                 ; closed on any other board -- skip the body
29B2: FD 21 00 62     LD      IY,$6200            ; Mario's record is the reference point
29B6: 3A 05 62        LD      A,(workRam+205)     ; read his screen row
29B9: 4F              LD      C,A                 
29BA: 21 08 04        LD      HL,$0408            ; allow 8 on the row and 4 across
29BD: CD 22 2A        CALL    loc_2a22            ; does he touch any of the six objects?
29C0: A7              AND     A                   
29C1: CA 20 2A        JP      Z,loc_2a20          ; no contact -- nothing to resolve
29C4: 3E 06           LD      A,$06               ; the record count the search started with
29C6: 90              SUB     B                   ; less its residue: the matched record's index

loc_29c7:
29C7: CA D0 29        JP      Z,loc_29d0          ; 
29CA: DD 19           ADD     IX,DE               ; walk out to that record
29CC: 3D              DEC     A                   
29CD: C3 C7 29        JP      loc_29c7            ; 

loc_29d0:
29D0: DD 7E 05        LD      A,(IX+$05)          ; the object's own row
29D3: D6 04           SUB     $04                 ; its contact line sits four above it
29D5: 57              LD      D,A                 
29D6: 3A 0C 62        LD      A,(workRam+20C)     ; where he was before this frame's motion
29D9: C6 05           ADD     A,$05               
29DB: BA              CP      D                   
29DC: D2 EE 29        JP      NC,loc_29ee         ; not clearly above the line -- try below
29DF: 7A              LD      A,D                 ; he lands: a standing height above the line
29E0: D6 08           SUB     $08                 
29E2: 32 05 62        LD      (workRam+205),A     ; place him there
29E5: 3E 01           LD      A,$01               
29E7: 47              LD      B,A                 
29E8: 32 98 63        LD      (workRam+398),A     ; raise the just-repositioned one-shot
29EB: 33              INC     SP                  ; skip the caller -- the handler above resumes
29EC: 33              INC     SP                  
29ED: C9              RET                         

loc_29ee:
29EE: 3A 0C 62        LD      A,(workRam+20C)     ; where Mario was before this frame's motion
29F1: D6 0E           SUB     $0E                 ; clearance for the below-the-line test
29F3: BA              CP      D                   ; against the object's contact line
29F4: D2 1B 2A        JP      NC,loc_2a1b         ; clearly below it -- Mario is killed
29F7: 3A 10 62        LD      A,(workRam+210)     ; route selector: airborne horizontal speed
29FA: A7              AND     A                   
29FB: 3A 03 62        LD      A,(workRam+203)     ; his X, the value both routes snap
29FE: CA 08 2A        JP      Z,loc_2a08          ; no speed: take the sibling snap arm
2A01: F6 07           OR      $07                 ; reach the right edge of his 8-pixel cell
2A03: D6 04           SUB     $04                 ; and step back to its middle
2A05: C3 0E 2A        JP      loc_2a0e            ; 

loc_2a08:
2A08: D6 08           SUB     $08                 ; the other route to the same snapped X
2A0A: F6 07           OR      $07                 
2A0C: C6 04           ADD     A,$04               

loc_2a0e:
2A0E: 32 03 62        LD      (workRam+203),A     ; side-on contact: put him in the cell middle
2A11: 32 4C 69        LD      (workRam+94C),A     ; so the sprite follows him
2A14: 3E 01           LD      A,$01               ; report the contact
2A16: 06 00           LD      B,$00               ; 0 tells the airborne handler to keep him falling
2A18: 33              INC     SP                  ; discard the caller's return so it is skipped
2A19: 33              INC     SP                  
2A1A: C9              RET                         

loc_2a1b:
2A1B: AF              XOR     A                   
2A1C: 32 00 62        LD      (workRam+200),A     ; struck from below -- Mario dies
2A1F: C9              RET                         

loc_2a20:
2A20: 47              LD      B,A                 
2A21: C9              RET                         

loc_2a22:
2A22: 06 06           LD      B,$06               ; six records in this board's object array
2A24: 11 10 00        LD      DE,$0010            ; 16 bytes apart
2A27: DD 21 00 66     LD      IX,$6600            ; the array to search
2A2B: CD 13 29        CALL    findCollidingObject ; does Mario overlap any of the six?
2A2E: C9              RET                         

loc_2a2f:
2A2F: DD 7E 03        LD      A,(IX+$03)          ; the object's X
2A32: 67              LD      H,A                 
2A33: DD 7E 05        LD      A,(IX+$05)          ; the object's Y
2A36: C6 04           ADD     A,$04               ; probe 4 pixels below it
2A38: 6F              LD      L,A                 
2A39: E5              PUSH    HL                  ; keep the probe pixel; the lookup destroys it
2A3A: CD F0 2F        CALL    tileAddrForPixel    ; find the tilemap cell under the probe
2A3D: D1              POP     DE                  ; recover the probe pixel
2A3E: 7E              LD      A,(HL)              ; the tile the object is over
2A3F: FE B0           CP      $B0                 ; below the girder band: nothing to stand on
2A41: DA 7B 2A        JP      C,loc_2a7b          ; report no contact
2A44: E6 0F           AND     $0F                 
2A46: FE 08           CP      $08                 ; a low nibble of 8+ is the tile pair's far half
2A48: D2 7B 2A        JP      NC,loc_2a7b         ; 
2A4B: 7E              LD      A,(HL)              ; re-read the tile unmasked
2A4C: FE C0           CP      $C0                 ; the plain 0xC0 tile is not a girder surface
2A4E: CA 7B 2A        JP      Z,loc_2a7b          ; 
2A51: DA 69 2A        JP      C,loc_2a69          ; 0xB0-0xBF: surface one pixel above the cell
2A54: FE D0           CP      $D0                 
2A56: DA 6E 2A        JP      C,loc_2a6e          ; 0xC1-0xCF: offset is the nibble less 9
2A59: FE E0           CP      $E0                 
2A5B: DA 63 2A        JP      C,loc_2a63          ; 0xD0-0xDF: offset is the nibble less 1
2A5E: FE F0           CP      $F0                 
2A60: DA 6E 2A        JP      C,loc_2a6e          ; 0xE0-0xEF: the nibble less 9 again

loc_2a63:
2A63: E6 0F           AND     $0F                 
2A65: 3D              DEC     A                   ; the nibble less 1 -- this cell's surface row
2A66: C3 72 2A        JP      loc_2a72            ; 

loc_2a69:
2A69: 3E FF           LD      A,$FF               ; surface sits one pixel above the cell edge
2A6B: C3 72 2A        JP      loc_2a72            ; 

loc_2a6e:
2A6E: E6 0F           AND     $0F                 
2A70: D6 09           SUB     $09                 ; the nibble less 9 -- this cell's surface row

loc_2a72:
2A72: 4F              LD      C,A                 
2A73: 7B              LD      A,E                 ; the probe's own row
2A74: E6 F8           AND     $F8                 ; rounded back to its 8-pixel cell boundary
2A76: 81              ADD     A,C                 ; plus the slope offset: the girder surface
2A77: BB              CP      E                   ; has the object reached it?
2A78: DA 7D 2A        JP      C,loc_2a7d          ; surface above the probe -- it has landed

loc_2a7b:
2A7B: AF              XOR     A                   ; report no contact
2A7C: C9              RET                         

loc_2a7d:
2A7D: D6 04           SUB     $04                 ; undo the 4-pixel probe nudge
2A7F: DD 77 05        LD      (IX+$05),A          ; snap the object up onto the girder
2A82: 3E 01           LD      A,$01               ; report contact
2A84: C9              RET                         

; While Mario is in plain standing or walking contact, look at the tile under his foot
; and, if the girder there is not level, defer to the slope-footing fall check. It
; early-outs if marioOnLadder, if marioAirborne, or while an edge-reposition runs. Past
; those three gates it samples the tilemap cell just under his foot -- 3 pixels back
; along his X, 12 along his Y. A solid flat girder there, a tile code at or above 0xB0
; whose low nibble is under 8, means level footing and there is nothing to do. Anything
; else -- a slope tile, or a girder tile whose low nibble is 8 or more -- means the
; ground may be angled or gone, and it hands off to the slope decision, which chooses
; between keeping his footing and starting a fall. The whole cascade below here has
; exactly ONE memory effect, raising marioStartFall, so starting a fall is all it can
; cause; nothing in it writes marioY.
startMarioFallWhenGroundGivesWay:
2A85: 3A 15 62        LD      A,(workRam+215)     ; footing is not in question while on a ladder
2A88: A7              AND     A                   
2A89: C0              RET     NZ                  
2A8A: 3A 16 62        LD      A,(workRam+216)     ; nor while he is already in the air
2A8D: A7              AND     A                   
2A8E: C0              RET     NZ                  
2A8F: 3A 98 63        LD      A,(workRam+398)     ; nor while an edge reposition is running
2A92: FE 01           CP      $01                 
2A94: C8              RET     Z                   
2A95: 3A 03 62        LD      A,(workRam+203)     ; foot probe: 3 pixels back along his X
2A98: D6 03           SUB     $03                 
2A9A: 67              LD      H,A                 
2A9B: 3A 05 62        LD      A,(workRam+205)     ; and 12 along his Y
2A9E: C6 0C           ADD     A,$0C               
2AA0: 6F              LD      L,A                 
2AA1: E5              PUSH    HL                  ; keep the probe pixel for the slope decision
2AA2: CD F0 2F        CALL    tileAddrForPixel    ; the tilemap cell just under his foot
2AA5: D1              POP     DE                  
2AA6: 7E              LD      A,(HL)              ; the tile he is standing on
2AA7: FE B0           CP      $B0                 ; not a girder tile at all
2AA9: DA B4 2A        JP      C,decideSlopeGirderFooting; the ground may be angled or gone
2AAC: E6 0F           AND     $0F                 
2AAE: FE 08           CP      $08                 ; a low nibble of 8+ means it is not flat
2AB0: D2 B4 2A        JP      NC,decideSlopeGirderFooting; 
2AB3: C9              RET                         ; flat girder underfoot: nothing to do

; Decide whether Mario keeps his footing on an angled girder or the ground has run out
; and he starts to fall. Reached from the foot-contact cascade once his foot-probe tile
; is a slope tile. Two ways the ground is judged to have gone, each starting a fall: he
; is exactly column-aligned (his sub-tile X offset is zero), so there is no girder edge
; to catch him; or the NEIGHBOURING tile -- one tilemap row back from the foot cell,
; which is a step of 8 pixels along his X rather than a vertical one, since the row term
; is built from X and mirrored -- is not a solid girder tile, code at or above 0xB0 with
; a low nibble under 8. A girder tile there means he is standing on the angled girder
; and keeps his footing. Starting the fall is a single
; one-shot request; the keeps-footing case writes nothing.
decideSlopeGirderFooting:
2AB4: 7A              LD      A,D                 ; the probe's offset within its tile
2AB5: E6 07           AND     $07                 
2AB7: CA CD 2A        JP      Z,triggerMarioFall  ; column-aligned: no edge to catch him
2ABA: 01 20 00        LD      BC,$0020            ; one tilemap row
2ABD: ED 42           SBC     HL,BC               ; step to the cell one row back
2ABF: 7E              LD      A,(HL)              ; the tile there
2AC0: FE B0           CP      $B0                 ; no girder there -- the ground has run out
2AC2: DA CD 2A        JP      C,triggerMarioFall  ; 
2AC5: E6 0F           AND     $0F                 
2AC7: FE 08           CP      $08                 ; a far-half tile is not solid ground either
2AC9: D2 CD 2A        JP      NC,triggerMarioFall ; 
2ACC: C9              RET                         ; girder found: he keeps his footing on the slope

; Raise the one-shot "start falling" trigger, because the ground under Mario went away.
; Reached from the slope/ledge contact check on the branches that find no girder under
; his foot. One store to marioStartFall, no callees and no return value; the player-state
; reset picks the trigger up on the next frame, puts Mario airborne with zero initial
; velocity, snapshots his height and clears the trigger -- so this single flag is what
; actually launches the fall.
triggerMarioFall:
2ACD: 3E 01           LD      A,$01               
2ACF: 32 21 62        LD      (workRam+221),A     ; raise the one-shot that launches the fall
2AD2: C9              RET                         

; Carry Mario along whichever 50m conveyor row he is standing on. The board runs three
; horizontally-drifting platform objects, one per row, each publishing a signed X-step
; for the frame; those three step publishers run just before this in the object update.
; The row is picked by EXACT marioY -- 0x50, 0x78 and 0xC8, one height per object rather
; than a band. Rows 1 and 3 carry him by m50Obj1Step and m50Obj3Step; row 2's object
; publishes a plus/minus pair and that row's own arm selects between them by Mario's X
; before carrying him. On any other Y he is on no moving row and is not carried at all.
carryMarioOnConveyorRow:
2AD3: 3A 03 62        LD      A,(workRam+203)     ; his X, the value each mover adds its step to
2AD6: 47              LD      B,A                 
2AD7: 3A 05 62        LD      A,(workRam+205)     ; which platform row is he standing on?
2ADA: FE 50           CP      $50                 ; the object-1 row
2ADC: CA EA 2A        JP      Z,loc_2aea          ; 
2ADF: FE 78           CP      $78                 ; the object-2 row
2AE1: CA F6 2A        JP      Z,selectConveyorStepAndMoveMario; 
2AE4: FE C8           CP      $C8                 ; the object-3 row
2AE6: CA F0 2A        JP      Z,loc_2af0          ; 
2AE9: C9              RET                         ; on no moving row: he is not carried

loc_2aea:
2AEA: 3A A3 63        LD      A,(workRam+3A3)     ; that row's published drift step
2AED: C3 02 2B        JP      moveMarioX          ; carry him by it

loc_2af0:
2AF0: 3A A6 63        LD      A,(workRam+3A6)     ; that row's published drift step
2AF3: C3 02 2B        JP      moveMarioX          ; 

; The moving-platform row arm for the row Mario stands on at Y 0x78. Object 2's signed
; drift step is published as two shadow bytes, m50Obj2StepPos and m50Obj2StepNeg, and
; marioX selects which applies: from the far-right half of the range (X >= 0x80) the
; positive step, otherwise the negative one. The chosen step goes to the shared X mover
; as the drift velocity, which advances marioX by it and holds him inside the horizontal
; limits. 50m is the conveyor board, so what this does on screen is carry Mario along a
; running conveyor.
selectConveyorStepAndMoveMario:
2AF6: 78              LD      A,B                 ; Mario's X picks the sign of this row's step
2AF7: FE 80           CP      $80                 
2AF9: 3A A5 63        LD      A,(workRam+3A5)     ; the positive-drift shadow
2AFC: D2 02 2B        JP      NC,moveMarioX       ; far-right half of the range: drift positive
2AFF: 3A A4 63        LD      A,(workRam+3A4)     ; otherwise the negative-drift shadow

; Advance Mario's X by the current velocity, then hold it inside the horizontal limits.
; It runs on the moving-platform boards, called from the platform-row mover, which is
; what picks the row's drift velocity for a Mario riding an elevator or a conveyor. The
; new X is stamped into BOTH marioX and the mirror of it in his sprite record, so the
; sprite tracks the move. The horizontal position gate then classifies that X: run off
; the far-right edge and it is nudged one pixel back left; at the far-left, or on the
; in-band default, one pixel right; on a blocked verdict it stays exactly where the
; velocity put it. The nudge lands on marioX ONLY -- the sprite-record mirror keeps the
; pre-nudge value.
moveMarioX:
2B02: 80              ADD     A,B                 ; new X = drift step + prior X
2B03: 32 03 62        LD      (workRam+203),A     ; move Mario
2B06: 32 4C 69        LD      (workRam+94C),A     ; and the sprite record that draws him
2B09: CD 1F 24        CALL    limitMarioHorizontalTravel; test the new X against the edges
2B0C: 21 03 62        LD      HL,$6203            
2B0F: 1D              DEC     E                   
2B10: CA 18 2B        JP      Z,loc_2b18          ; off the far right: nudge one pixel back
2B13: 15              DEC     D                   
2B14: CA 1A 2B        JP      Z,loc_2b1a          ; at the left, or in band: nudge one right
2B17: C9              RET                         ; blocked: leave him where the step put him

loc_2b18:
2B18: 35              DEC     (HL)                ; pull him back left (the sprite keeps the old X)
2B19: C9              RET                         

loc_2b1a:
2B1A: 34              INC     (HL)                ; push him right
2B1B: C9              RET                         

loc_2b1c:
2B1C: DD 21 00 62     LD      IX,$6200            ; the probe works off Mario's own record
2B20: CD 29 2B        CALL    probeMarioDescentLanding; run the descent probe
2B23: CD AF 29        CALL    loc_29af            ; then the board-gated object-collision check
2B26: AF              XOR     A                   
2B27: 47              LD      B,A                 ; hand the airborne handler a zeroed result
2B28: C9              RET                         

; The board split at the head of the player-versus-tilemap descent probe. On any board
; but 25m this routine does nothing itself: the whole probe belongs to the two-point form
; and its result is passed straight back. On 25m it probes a single point, (marioX,
; marioY + 7), through the tile classifier, with three outcomes. LANDED -- the classifier
; has already snapped marioY and unwound the collision walk, so this just propagates it.
; NO SURFACE -- nothing landable under the probe point, so abort carrying a zeroed
; result. SURFACE BUT STILL CLEAR OF IT -- measure how far the point sits past the
; boundary the classifier computed; four or more pixels is too far, but under four snap
; marioY to seven above the boundary and report landed. So the 25m arm lands Mario from
; up to three pixels short of the boundary the classifier itself waits for.
probeMarioDescentLanding:
2B29: 3A 27 62        LD      A,(workRam+227)     ; 25m takes the single-point probe below
2B2C: 3D              DEC     A                   
2B2D: C2 53 2B        JP      NZ,loc_2b53         ; every other board uses the two-point form
2B30: 3A 03 62        LD      A,(workRam+203)     ; probe point: straight under Mario
2B33: 67              LD      H,A                 
2B34: 3A 05 62        LD      A,(workRam+205)     ; seven pixels below his feet
2B37: C6 07           ADD     A,$07               
2B39: 6F              LD      L,A                 
2B3A: CD 9B 2B        CALL    probeTileForLanding ; classify the tile there

loc_2b3d:
2B3D: A7              AND     A                   
2B3E: CA 51 2B        JP      Z,loc_2b51          ; nothing landable: abandon the walk
2B41: 7B              LD      A,E                 
2B42: 91              SUB     C                   ; how far the probe sits past the surface
2B43: FE 04           CP      $04                 ; four pixels or more is too far to land
2B45: D2 74 2B        JP      NC,loc_2b74         ; 
2B48: 79              LD      A,C                 
2B49: D6 07           SUB     $07                 ; stand him seven above the surface
2B4B: 32 05 62        LD      (workRam+205),A     ; land Mario
2B4E: 3E 01           LD      A,$01               
2B50: 47              LD      B,A                 ; report the landing

loc_2b51:
2B51: E1              POP     HL                  ; drop a return level: the walk is over
2B52: C9              RET                         

loc_2b53:
2B53: 3A 03 62        LD      A,(workRam+203)     ; first probe: 3 pixels back along his X
2B56: D6 03           SUB     $03                 
2B58: 67              LD      H,A                 
2B59: 3A 05 62        LD      A,(workRam+205)     ; and seven below his feet
2B5C: C6 07           ADD     A,$07               
2B5E: 6F              LD      L,A                 
2B5F: CD 9B 2B        CALL    probeTileForLanding ; classify the tile there
2B62: FE 02           CP      $02                 ; surface found but not reached: snap his column
2B64: CA 7A 2B        JP      Z,loc_2b7a          ; 
2B67: 7A              LD      A,D                 ; second probe, seven further along X
2B68: C6 07           ADD     A,$07               
2B6A: 67              LD      H,A                 
2B6B: 6B              LD      L,E                 
2B6C: CD 9B 2B        CALL    probeTileForLanding ; classify that point too
2B6F: A7              AND     A                   
2B70: C8              RET     Z                   ; nothing under either: he keeps falling
2B71: C3 7A 2B        JP      loc_2b7a            ; 

loc_2b74:
2B74: 3E 00           LD      A,$00               ; no result this pass
2B76: 06 00           LD      B,$00               
2B78: E1              POP     HL                  ; drop a return level and abandon the walk
2B79: C9              RET                         

loc_2b7a:
2B7A: 3A 10 62        LD      A,(workRam+210)     ; route selector: airborne horizontal speed
2B7D: A7              AND     A                   
2B7E: 3A 03 62        LD      A,(workRam+203)     ; the X both routes snap
2B81: CA 8B 2B        JP      Z,loc_2b8b          ; no speed: the sibling snap arm
2B84: F6 07           OR      $07                 ; reach the right edge of his 8-pixel cell
2B86: D6 04           SUB     $04                 ; and step back to its middle
2B88: C3 91 2B        JP      loc_2b91            ; 

loc_2b8b:
2B8B: D6 08           SUB     $08                 ; the other route to the same snapped X
2B8D: F6 07           OR      $07                 
2B8F: C6 04           ADD     A,$04               

loc_2b91:
2B91: 32 03 62        LD      (workRam+203),A     ; put Mario on the snapped column
2B94: 32 4C 69        LD      (workRam+94C),A     ; so the sprite jumps there this frame
2B97: 3E 01           LD      A,$01               ; report the accept
2B99: E1              POP     HL                  ; drop a return level: the walk is over
2B9A: C9              RET                         

; The tile gate at the head of the airborne-descent collision probe. Given a pixel
; coordinate (y in the high byte, x in the low) it looks up the tile under that pixel
; and decides whether the pixel sits on a landable surface. It REJECTS a tile below
; 0xB0, a low nibble of 8 or more (the right half of the tile pair), and the tile 0xC0
; exactly. A tile below 0xC0 is a HIT on the "silent" boundary, x's 8-pixel column minus
; one. Above 0xC0 the tile band picks a column offset from its low nibble -- bands
; 0xC1..0xCF and 0xE0..0xEF subtract 9, bands 0xD0..0xDF and 0xF0..0xFF subtract 1 --
; and the boundary is x's column plus that offset, a HIT only when it lands strictly
; left of x. On a hit it hands off to the descent resolver, which measures the fall
; against that boundary and either keeps Mario airborne or snaps him onto the surface.
; The result code is 0 reject / 1 landed / 2 airborne, and a landing aborts the whole
; multi-probe collision walk.
probeTileForLanding:
2B9B: E5              PUSH    HL                  ; keep the pixel; its column is the reference
2B9C: CD F0 2F        CALL    tileAddrForPixel    ; the tilemap cell under the probe pixel
2B9F: D1              POP     DE                  
2BA0: 7E              LD      A,(HL)              ; the tile there
2BA1: FE B0           CP      $B0                 ; below the surface band: reject
2BA3: DA D9 2B        JP      C,loc_2bd9          ; 
2BA6: E6 0F           AND     $0F                 
2BA8: FE 08           CP      $08                 ; the tile pair's far half is no surface
2BAA: D2 D9 2B        JP      NC,loc_2bd9         ; 
2BAD: 7E              LD      A,(HL)              ; re-read the tile unmasked
2BAE: FE C0           CP      $C0                 ; the plain 0xC0 tile is excluded
2BB0: CA D9 2B        JP      Z,loc_2bd9          ; 
2BB3: DA DC 2B        JP      C,loc_2bdc          ; 0xB0-0xBF: the silent boundary
2BB6: FE D0           CP      $D0                 
2BB8: DA CB 2B        JP      C,loc_2bcb          ; 0xC1-0xCF: offset is the nibble less 9
2BBB: FE E0           CP      $E0                 
2BBD: DA C5 2B        JP      C,loc_2bc5          ; 0xD0-0xDF: offset is the nibble less 1
2BC0: FE F0           CP      $F0                 
2BC2: DA CB 2B        JP      C,loc_2bcb          ; 0xE0-0xEF: the nibble less 9 again

loc_2bc5:
2BC5: E6 0F           AND     $0F                 
2BC7: 3D              DEC     A                   ; the nibble less 1 -- this band's offset
2BC8: C3 CF 2B        JP      loc_2bcf            ; 

loc_2bcb:
2BCB: E6 0F           AND     $0F                 
2BCD: D6 09           SUB     $09                 ; the nibble less 9 -- this band's offset

loc_2bcf:
2BCF: 4F              LD      C,A                 
2BD0: 7B              LD      A,E                 ; the probe's own column
2BD1: E6 F8           AND     $F8                 ; rounded down to its 8-pixel column
2BD3: 81              ADD     A,C                 ; plus the band offset: the surface boundary
2BD4: 4F              LD      C,A                 ; the resolver reads the boundary from here
2BD5: BB              CP      E                   ; a hit only if it lands left of the probe
2BD6: DA E1 2B        JP      C,resolveAirborneTileLanding; 

loc_2bd9:
2BD9: AF              XOR     A                   ; reject: nothing landable here
2BDA: 47              LD      B,A                 
2BDB: C9              RET                         

loc_2bdc:
2BDC: 7B              LD      A,E                 ; the silent boundary, one left of the column
2BDD: E6 F8           AND     $F8                 
2BDF: 3D              DEC     A                   
2BE0: 4F              LD      C,A                 

; Resolve whether Mario's airborne descent has reached a tile surface. Entered once the
; tile classifier has built the tile column's surface boundary, it measures how far he
; has descended as
;   probe = marioAirPrevY - (the object record's Y at +5) + the classifier's row offset
; against that boundary. Probe ABOVE the boundary means he is still clear of this tile:
; report code 2, "still airborne, keep probing", and return so the collision walk
; continues. Probe at or below it means he has reached the surface: snap marioY to the
; boundary less 7, report code 1, and unwind two levels -- the landing aborts the whole
; multi-probe walk, not just this classifier call.
resolveAirborneTileLanding:
2BE1: 3A 0C 62        LD      A,(workRam+20C)     ; where he was before this frame's motion
2BE4: DD 96 05        SUB     (IX+$05)            ; measured against the record's current Y
2BE7: 83              ADD     A,E                 ; and the probe's own row
2BE8: B9              CP      C                   ; against the surface boundary
2BE9: CA EF 2B        JP      Z,loc_2bef          ; exactly on it: he has landed
2BEC: D2 F8 2B        JP      NC,loc_2bf8         ; still clear of the tile: keep falling

loc_2bef:
2BEF: 79              LD      A,C                 
2BF0: D6 07           SUB     $07                 ; stand him seven above the surface
2BF2: 32 05 62        LD      (workRam+205),A     ; land Mario on this tile
2BF5: C3 FD 2B        JP      loc_2bfd            ; 

loc_2bf8:
2BF8: 3E 02           LD      A,$02               ; report still airborne so the walk goes on
2BFA: 06 00           LD      B,$00               
2BFC: C9              RET                         

loc_2bfd:
2BFD: 3E 01           LD      A,$01               ; report the landing
2BFF: 47              LD      B,A                 
2C00: E1              POP     HL                  ; drop two return levels: the walk is over
2C01: E1              POP     HL                  
2C02: C9              RET                         

; The 25m release scheduler: decide, this pass, whether to dispatch into the slot-claim
; cluster that sends a barrel out, and by which route. Three gates first -- only on 25m,
; only while Mario is alive, and only while bit 0 of the cluster's event-gate scratch is
; clear -- and a live bonus of zero ends the pass. Then, with the bonus in hand: if
; bonusStart minus 2 has fallen below the live bonus, hand straight to the stepped-value
; entry; else if bit 1 of barrelClaimMode is set, hand to the clear-then-mode-3 entry;
; else run a periodic phase test, matching the low five bits of frame against a countdown
; of length difficulty, so the throw rate rises with difficulty. On a match, dispatch the
; cluster head if half of bonusStart has fallen below the live bonus, and otherwise fire
; only on odd spinCount frames. So the throws are paced against the bonus, not against a
; clock.
scheduleBarrelRelease:
2C03: 3E 01           LD      A,$01               ; board mask: 25m only
2C05: F7              RST     $30                 ; skip the routine on the other boards
2C06: D7              RST     $10                 ; and while Mario is dead
2C07: 3A 93 63        LD      A,(workRam+393)     ; the cluster's in-progress latch
2C0A: 0F              RRCA                        
2C0B: D8              RET     C                   ; one already went out this pass
2C0C: 3A B1 62        LD      A,(workRam+2B1)     ; the live bonus is what paces the throws
2C0F: A7              AND     A                   
2C10: C8              RET     Z                   ; no bonus left: nothing to schedule
2C11: 4F              LD      C,A                 
2C12: 3A B0 62        LD      A,(workRam+2B0)     ; the board's starting bonus
2C15: D6 02           SUB     $02                 ; one notch short of it
2C17: B9              CP      C                   
2C18: DA 7B 2C        JP      C,loc_2c7b          ; the first throws take the stepped-value route
2C1B: 3A 82 63        LD      A,(workRam+382)     ; the slot-claim mode byte
2C1E: CB 4F           BIT     1,A                 
2C20: C2 86 2C        JP      NZ,loc_2c86         ; bit 1 set: the clear-then-mode-3 entry
2C23: 3A 80 63        LD      A,(workRam+380)     ; the countdown's length is the difficulty
2C26: 47              LD      B,A                 
2C27: 3A 1A 60        LD      A,(workRam+1A)      ; phase test on the frame counter
2C2A: E6 1F           AND     $1F                 

loc_2c2c:
2C2C: B8              CP      B                   ; match the phase against the countdown
2C2D: CA 33 2C        JP      Z,loc_2c33          ; 
2C30: 10 FA           DJNZ    loc_2c2c            ; harder boards match more phases, so throw faster
2C32: C9              RET                         ; no match this frame

loc_2c33:
2C33: 3A B0 62        LD      A,(workRam+2B0)     ; half the starting bonus
2C36: CB 3F           SRL     A                   
2C38: B9              CP      C                   
2C39: DA 41 2C        JP      C,loc_2c41          ; past halfway: fire on every matching frame
2C3C: 3A 19 60        LD      A,(workRam+19)      ; earlier, fire only on odd spin-counter frames
2C3F: 0F              RRCA                        
2C40: D0              RET     NC                  

loc_2c41:
2C41: CD 57 00        CALL    stirRandomSeed      ; a 1-in-16 coin flip picks the claim mode
2C44: E6 0F           AND     $0F                 
2C46: C2 86 2C        JP      NZ,loc_2c86         ; 15 of 16: the mode-3 entry

loc_2c49:
2C49: 3E 01           LD      A,$01               ; 1 of 16: mode 1

loc_2c4b:
2C4B: 32 82 63        LD      (workRam+382),A     ; record the claim mode
2C4E: 3C              INC     A                   ; the scratch copy sits one above it

; One entry of the bonus-event slot-claim cluster: stash the caller's mode byte and a 1
; into a companion flag, then, once the bonus counter has reached its scheduled mark,
; step the mark and claim a free object slot. Unlike the cluster's other entries this one
; does NOT stamp the paired barrel-claim mode byte up front -- that byte is touched only
; later, and only if a slot is claimed. The periodic gate proceeds only when the
; next-event mark equals the bonus value passed in; otherwise it returns having done just
; the two scratch writes. On a hit it schedules the next fire by stepping the mark down
; by 8, then scans the five records of objArray64 for the first whose active byte is zero
; and, on finding one, raises the barrel-kind bit in barrelClaimMode. If all five are
; occupied it does nothing further.
armBarrelRelease:
2C4F: 32 8F 63        LD      (workRam+38F),A     ; stash the mode byte in engine scratch
2C52: 3E 01           LD      A,$01               
2C54: 32 92 63        LD      (workRam+392),A     ; raise the release-armed flag
2C57: 3A B2 62        LD      A,(workRam+2B2)     ; the bonus value this event is scheduled at
2C5A: B9              CP      C                   
2C5B: C0              RET     NZ                  ; not yet: only the two scratch writes happen
2C5C: D6 08           SUB     $08                 ; schedule the next event eight notches down
2C5E: 32 B2 62        LD      (workRam+2B2),A     ; 
2C61: 11 20 00        LD      DE,$0020            
2C64: 21 00 64        LD      HL,$6400            ; look for a free record in the object array
2C67: 06 05           LD      B,$05               ; five of them

loc_2c69:
2C69: 7E              LD      A,(HL)              
2C6A: A7              AND     A                   
2C6B: CA 72 2C        JP      Z,markNextBarrelAsAltKind; one free: tag the next barrel
2C6E: 19              ADD     HL,DE               
2C6F: 10 F8           DJNZ    loc_2c69            ; 
2C71: C9              RET                         ; all five in use: no tag

; Raise bit 7 of barrelClaimMode, leaving the low bits alone -- a mode-1 claim reads
; back as 0x81 afterwards. Bit 7 selects the barrel's KIND: the barrel released after
; this fires is stamped with a different sprite code, a different attribute and so a
; different palette, and a different family index, and that index is read again on
; several later paths, so the choice reaches behaviour and not only the picture. Both
; kinds can be on the board at the same time. It is NOT what makes a barrel drop
; straight down instead of rolling along the girders -- that is BIT 0 of the same byte,
; independent of bit 7 and left exactly as found here. Which named object either kind is
; has not been established.
markNextBarrelAsAltKind:
2C72: 3A 82 63        LD      A,(workRam+382)     ; the slot-claim mode byte
2C75: F6 80           OR      $80                 ; bit 7 selects the alternate barrel kind
2C77: 32 82 63        LD      (workRam+382),A     ; the mode value in the low bits survives
2C7A: C9              RET                         

loc_2c7b:
2C7B: C6 02           ADD     A,$02               ; step the caller's value back up
2C7D: B9              CP      C                   
2C7E: CA 49 2C        JP      Z,loc_2c49          ; on the mark: claim with mode 1
2C81: 3E 02           LD      A,$02               ; otherwise mode 2
2C83: C3 4B 2C        JP      loc_2c4b            ; 

loc_2c86:
2C86: AF              XOR     A                   
2C87: 32 82 63        LD      (workRam+382),A     ; wipe it, so nothing stale carries over
2C8A: 3E 03           LD      A,$03               ; ask for the release with mode 3
2C8C: C3 4F 2C        JP      armBarrelRelease    ; 

; The release half of the 25m barrel engine: while a barrel is already on its way out,
; step the release animation; otherwise, if a release is armed, find a free barrel
; record and hand the claim on. Four gates decide, and three of them simply end the
; frame's work here -- a board mask that admits the girder board only; Mario must be
; alive; an in-progress latch set means a barrel already went out this pass, so control
; goes straight to the release animation and NOTHING is scanned; and an armed latch
; clear means there is nothing to place. With all four open it walks the ten objArray67
; records and stops at the first whose active byte has BOTH low bits clear -- neither
; already in motion nor already claimed -- handing that record and the countdown to the
; claim. The walk COUNTS DOWN because the loop variable is not a cursor but the
; argument: the claim derives the record's slot index from what is LEFT.
driveBarrelRelease:
2C8F: 3E 01           LD      A,$01               ; board mask: the girder board only
2C91: F7              RST     $30                 
2C92: D7              RST     $10                 ; and only while Mario is alive
2C93: 3A 93 63        LD      A,(workRam+393)     ; is a barrel already on its way out?
2C96: 0F              RRCA                        
2C97: DA 15 2D        JP      C,loc_2d15          ; then only step its release animation
2C9A: 3A 92 63        LD      A,(workRam+392)     ; is a release armed?
2C9D: 0F              RRCA                        
2C9E: D0              RET     NC                  ; nothing armed: nothing to place
2C9F: DD 21 00 67     LD      IX,$6700            ; the ten barrel records
2CA3: 11 20 00        LD      DE,$0020            
2CA6: 06 0A           LD      B,$0A               ; the count doubles as the slot index

loc_2ca8:
2CA8: DD 7E 00        LD      A,(IX+$00)          ; this record's state byte
2CAB: 0F              RRCA                        
2CAC: DA B3 2C        JP      C,loc_2cb3          ; bit 0: already rolling
2CAF: 0F              RRCA                        
2CB0: D2 B8 2C        JP      NC,releaseBarrelIntoFreeSlot; free: take this record

loc_2cb3:
2CB3: DD 19           ADD     IX,DE               
2CB5: 10 F1           DJNZ    loc_2ca8            ; 
2CB7: C9              RET                         ; all ten in use

; Claim the free barrel slot the scan above stopped on and send a barrel out. The scan
; walks the ten records counting down and jumps here the moment it finds one whose low
; two flag bits are both clear. This publishes that record as renderObjPtr; stamps its
; active field with 2, meaning "occupied", which is what stops the next pass re-claiming
; it and which the movement code later replaces with 1, "active", when the barrel starts
; moving; works out the slot index -- the scan counts ten down to one across records
; 0..9, so ten minus the count is the index -- and stores the matching slot of
; actorSprites into renderDstPtr, so barrel record k renders into sprite slot k. It sets
; bit 0 of the cluster's event gate, a one-shot latch the renderer's terminator clears
; again. Finally it charges the release: post the deferred task that takes one notch off
; the on-screen readout, then decrement bonus itself. ON 25m THIS ROUTINE IS THE BONUS
; CLOCK -- the other boards run the counter down on a timer, but on the girder board the
; bonus falls per barrel released. A decrement that reaches zero raises bonusExpiredStep.
releaseBarrelIntoFreeSlot:
2CB8: DD 22 AA 62     LD      (workRam+2AA),IX    ; publish the claimed record
2CBC: DD 36 00 02     LD      (IX+$00),$02        ; mark it occupied so the next scan skips it
2CC0: 16 00           LD      D,$00               
2CC2: 3E 0A           LD      A,$0A               ; ten minus the count is the record's index
2CC4: 90              SUB     B                   
2CC5: 87              ADD     A,A                 
2CC6: 87              ADD     A,A                 ; times the 4-byte sprite-record stride
2CC7: 5F              LD      E,A                 
2CC8: 21 80 69        LD      HL,$6980            ; the sprite group the barrels draw into
2CCB: 19              ADD     HL,DE               
2CCC: 22 AC 62        LD      (workRam+2AC),HL    ; barrel k renders into sprite slot k
2CCF: 3E 01           LD      A,$01               
2CD1: 32 93 63        LD      (workRam+393),A     ; latch: a barrel went out this pass
2CD4: 11 01 05        LD      DE,$0501            ; task: take one notch off the bonus readout
2CD7: CD 9F 30        CALL    enqueueTask         ; 
2CDA: 21 B1 62        LD      HL,$62B1            
2CDD: 35              DEC     (HL)                ; charge the release -- on 25m the bonus falls per barrel
2CDE: C2 E6 2C        JP      NZ,loc_2ce6         ; 
2CE1: 3E 01           LD      A,$01               
2CE3: 32 86 63        LD      (workRam+386),A     ; bonus exhausted: start the expiry sequence

loc_2ce6:
2CE6: 7E              LD      A,(HL)              ; the bonus after the charge
2CE7: FE 04           CP      $04                 ; below four, retire one decoration sprite a notch
2CE9: D2 F6 2C        JP      NC,stampReleasedBarrelKind; 
2CEC: 21 A8 69        LD      HL,$69A8            ; the four-record sprite group
2CEF: 87              ADD     A,A                 
2CF0: 87              ADD     A,A                 
2CF1: 5F              LD      E,A                 
2CF2: 16 00           LD      D,$00               
2CF4: 19              ADD     HL,DE               
2CF5: 72              LD      (HL),D              ; zeroing a record's X takes it off the display

; Preset a freshly-claimed barrel record's appearance, then fall into the frame-gated
; renderer tick. With the record base in IX it stamps three of the record's fields with
; one of two presets, chosen by bit 7 of barrelClaimMode. Bit 7 clear gives sprite code
; 0x15, attribute 0x0B, mode 0x00 -- the ROLLING kind, whose code stays in the 0x15 /
; 0x16 / 0x17 family while X sweeps along the girders and Y creeps down. Bit 7 set gives
; 0x19, 0x0C, 0x01 -- the DROPPING kind, whose code walks 0x19, 0x1A, 0x1B while it
; descends with its X PINNED, a straight vertical fall down one column. The differing
; attribute selects a different palette, so the two are visually distinct, and both kinds
; can be active at once. Which named Donkey Kong object either kind is has not been
; established.
stampReleasedBarrelKind:
2CF6: DD 36 07 15     LD      (IX+$07),$15        ; the rolling barrel's sprite code
2CFA: DD 36 08 0B     LD      (IX+$08),$0B        ; and its palette
2CFE: DD 36 15 00     LD      (IX+$15),$00        ; kind 0: rolls along the girders
2D02: 3A 82 63        LD      A,(workRam+382)     ; bit 7 selects the barrel kind
2D05: 07              RLCA                        
2D06: D2 15 2D        JP      NC,loc_2d15         ; clear: keep the rolling preset
2D09: DD 36 07 19     LD      (IX+$07),$19        ; the dropping barrel's sprite code
2D0D: DD 36 08 0C     LD      (IX+$08),$0C        ; a different palette, so the two look apart
2D11: DD 36 15 01     LD      (IX+$15),$01        ; kind 1: descends with its X pinned

; The frame-gated step of a sprite waypoint walker. A down-counter is decremented on
; every entry and the routine returns until it underflows, so the body acts only once
; every 24 frames. On the acting frame it reloads that gate to 0x18 and reads an
; animation sub-counter: at zero it simply steps the object to the next waypoint and stops.
; Non-zero, it selects a 40-byte record from the animation table -- bit 0 of
; barrelClaimMode decides whether the record index is the sub-counter itself or one less
; -- and copies it into the sprite-object block. The sub-counter is then stepped down;
; while it stays non-zero the object steps to the next waypoint, and when it reaches zero the
; gate is shortened to a single frame and that same parity bit branches: set restarts
; the fixed table, clear steps to the next waypoint. Which barrel kind the
; record being dressed belongs to is not claimed; bit 0 is read only as a parity
; selector.
loc_2d15:
2D15: 21 AF 62        LD      HL,$62AF            
2D18: 35              DEC     (HL)                ; the frame gate -- the body acts once in 24
2D19: C0              RET     NZ                  
2D1A: 36 18           LD      (HL),$18            ; reload the gate for the next cycle
2D1C: 3A 8F 63        LD      A,(workRam+38F)     ; the animation sub-counter
2D1F: A7              AND     A                   
2D20: CA 51 2D        JP      Z,loc_2d51          ; zero: just take the next waypoint
2D23: 4F              LD      C,A                 
2D24: 21 32 39        LD      HL,$3932            ; the animation table, 40 bytes per record
2D27: 3A 82 63        LD      A,(workRam+382)     ; bit 0 picks which record index
2D2A: 0F              RRCA                        
2D2B: DA 2F 2D        JP      C,loc_2d2f          ; 
2D2E: 0D              DEC     C                   ; clear: one record earlier

loc_2d2f:
2D2F: 79              LD      A,C                 
2D30: 87              ADD     A,A                 
2D31: 87              ADD     A,A                 
2D32: 87              ADD     A,A                 
2D33: 4F              LD      C,A                 
2D34: 87              ADD     A,A                 
2D35: 87              ADD     A,A                 
2D36: 81              ADD     A,C                 ; index times forty
2D37: 5F              LD      E,A                 
2D38: 16 00           LD      D,$00               
2D3A: 19              ADD     HL,DE               
2D3B: CD 4E 00        CALL    loadSpriteObjectBlock; copy it into the sprite-object block
2D3E: 21 8F 63        LD      HL,$638F            
2D41: 35              DEC     (HL)                ; step the sub-counter
2D42: C2 51 2D        JP      NZ,loc_2d51         ; still running: take the next waypoint
2D45: 3E 01           LD      A,$01               
2D47: 32 AF 62        LD      (workRam+2AF),A     ; last record: act again on the next frame
2D4A: 3A 82 63        LD      A,(workRam+382)     ; the same bit 0 picks the ending
2D4D: 0F              RRCA                        
2D4E: DA 83 2D        JP      C,loc_2d83          ; set: restart the one-waypoint table

loc_2d51:
2D51: 2A A8 62        LD      HL,(workRam+2A8)    ; reload the path cursor

; Move a barrel to its next waypoint on the release path and publish that pose as a
; hardware sprite record, or, at the end of the path, hand off to release completion.
; The table it walks is (x, y) waypoint PAIRS, not text, and nothing here touches video
; RAM. One call consumes one pair: sprite X takes the first byte with its top bit
; stripped; the sprite code is the barrel's own, with its low two bits flipped first if
; that top bit was SET -- and the flipped value is written BACK into the barrel record,
; so the animation step persists into the following waypoints rather than being a
; one-frame pose; the attribute is copied straight through; and sprite Y takes the
; second byte. The cursor then advances two bytes and is stored back. A first byte of
; 0x7F ends the path, which is why the same 0x7F doubles as the mask that strips the
; flag bit off every other X.
stepBarrelAlongReleasePath:
2D54: 7E              LD      A,(HL)              ; this waypoint's X byte, flag bit and all
2D55: DD 2A AA 62     LD      IX,(workRam+2AA)    ; the barrel being moved
2D59: ED 5B AC 62     LD      DE,(workRam+2AC)    ; the sprite slot it publishes into
2D5D: FE 7F           CP      $7F                 ; 0x7F ends the path
2D5F: CA 8C 2D        JP      Z,activateReleasedBarrel; 
2D62: 4F              LD      C,A                 ; keep the flag bit
2D63: E6 7F           AND     $7F                 ; strip it off to get the sprite X
2D65: 12              LD      (DE),A              
2D66: DD 7E 07        LD      A,(IX+$07)          ; the barrel's own sprite code
2D69: CB 79           BIT     7,C                 ; the waypoint asks for the next frame
2D6B: CA 70 2D        JP      Z,loc_2d70          ; 
2D6E: EE 03           XOR     $03                 ; flip the two animation bits

loc_2d70:
2D70: 13              INC     DE                  
2D71: 12              LD      (DE),A              ; publish the sprite code
2D72: DD 77 07        LD      (IX+$07),A          ; and keep it, so the flip persists
2D75: DD 7E 08        LD      A,(IX+$08)          
2D78: 13              INC     DE                  
2D79: 12              LD      (DE),A              ; the barrel's palette, copied straight through
2D7A: 23              INC     HL                  
2D7B: 7E              LD      A,(HL)              ; the pair's second byte is the sprite Y
2D7C: 13              INC     DE                  
2D7D: 12              LD      (DE),A              
2D7E: 23              INC     HL                  
2D7F: 22 A8 62        LD      (workRam+2A8),HL    ; advance the cursor past this waypoint
2D82: C9              RET                         

loc_2d83:
2D83: 21 CC 39        LD      HL,$39CC            ; the one-waypoint path
2D86: 22 A8 62        LD      (workRam+2A8),HL    ; restart the walk at its head
2D89: C3 54 2D        JP      stepBarrelAlongReleasePath; 

; The release renderer's 0x7F terminator: close out the barrel record it was building.
; The renderer above walks a byte string writing 4-byte sprite records until it meets the
; terminator, and that jump lands here. It rewinds renderStrPtr back to the string's
; start; stamps the record's control fields -- an enable/mode byte at +1 chosen from bit
; 0 of barrelClaimMode (set keeps mode 1, clear selects mode 0 and marks +2 with 2), a
; fixed 1 in the active field and at +0x0F, a zeroed sub-block at +0x10..+0x14, and two
; cleared scratch bytes; copies the destination sprite record's X and Y into the record's
; own X and Y; then reloads the whole ten-record sprite-object block from its stored
; template and adds -4 to every record's Y. Which barrel the two mode arms produce is not
; established here.
activateReleasedBarrel:
2D8C: 21 C3 39        LD      HL,$39C3            ; rewind to the head of the four-waypoint path
2D8F: 22 A8 62        LD      (workRam+2A8),HL    ; 
2D92: DD 36 01 01     LD      (IX+$01),$01        ; the record's select byte
2D96: 3A 82 63        LD      A,(workRam+382)     ; bit 0 chose which path was walked
2D99: 0F              RRCA                        
2D9A: DA A5 2D        JP      C,loc_2da5          ; 
2D9D: DD 36 01 00     LD      (IX+$01),$00        ; clear: select 0
2DA1: DD 36 02 02     LD      (IX+$02),$02        ; and the mode bit that starts it rolling

loc_2da5:
2DA5: DD 36 00 01     LD      (IX+$00),$01        ; the barrel is running now, not just claimed
2DA9: DD 36 0F 01     LD      (IX+$0F),$01        
2DAD: AF              XOR     A                   
2DAE: DD 77 10        LD      (IX+$10),A          ; clear the record's motion sub-block
2DB1: DD 77 11        LD      (IX+$11),A          
2DB4: DD 77 12        LD      (IX+$12),A          
2DB7: DD 77 13        LD      (IX+$13),A          
2DBA: DD 77 14        LD      (IX+$14),A          
2DBD: 32 93 63        LD      (workRam+393),A     ; clear the went-out latch
2DC0: 32 92 63        LD      (workRam+392),A     ; and the armed latch
2DC3: 1A              LD      A,(DE)              ; the sprite's final X...
2DC4: DD 77 03        LD      (IX+$03),A          ; ...becomes the barrel's own X
2DC7: 13              INC     DE                  
2DC8: 13              INC     DE                  
2DC9: 13              INC     DE                  
2DCA: 1A              LD      A,(DE)              ; and its final Y...
2DCB: DD 77 05        LD      (IX+$05),A          ; ...likewise
2DCE: 21 5C 38        LD      HL,$385C            ; reload the sprite-object block from its template
2DD1: CD 4E 00        CALL    loadSpriteObjectBlock; 
2DD4: 21 0B 69        LD      HL,$690B            ; aim at the block's Y fields
2DD7: 0E FC           LD      C,$FC               ; and lift every record four pixels
2DD9: FF              RST     $38                 
2DDA: C9              RET                         

; On 50m and 100m, while Mario is alive, raise two one-shot spawn-request latches on a
; difficulty-scaled period. Two gates open it: a board test against the mask 0x0A, so 25m
; and 75m skip the routine entirely, and an alive test. The trigger is a low-bit mask on
; the frame counter that NARROWS as difficulty rises, so the requests come more often the
; harder the board. steps = (difficulty + 1) >> 1, plus one more on 50m, which is 1 to 4
; steps over the in-play range; the mask's run of set low bits folds down once per step,
; giving 0xFF, 0x7F, 0x3F, 0x1F; and the request fires on the frames where the frame
; counter lands zero under the mask, every 2^(9 - steps) frames. It only RAISES the
; latches -- never clears them, never checks whether the last one was serviced -- so an
; unconsumed request is simply re-asserted.
raisePeriodicObjectSpawnRequests:
2DDB: 3E 0A           LD      A,$0A               ; board mask: 50m and 100m
2DDD: F7              RST     $30                 
2DDE: D7              RST     $10                 ; and only while Mario is alive
2DDF: 3A 80 63        LD      A,(workRam+380)     ; the period narrows as difficulty rises
2DE2: 3C              INC     A                   
2DE3: A7              AND     A                   ; clear carry so the halving shifts in a zero
2DE4: 1F              RRA                         ; steps = (difficulty + 1) / 2
2DE5: 47              LD      B,A                 
2DE6: 3A 27 62        LD      A,(workRam+227)     ; 50m gets one extra step, so it fires twice as often
2DE9: FE 02           CP      $02                 
2DEB: 20 01           JR      NZ,loc_2dee         ; 
2DED: 04              INC     B                   

loc_2dee:
2DEE: 3E FE           LD      A,$FE               ; the trigger mask, seeded
2DF0: 37              SCF                         ; the first fold brings in a set bit, giving 0xFF

loc_2df1:
2DF1: 1F              RRA                         ; fold one more low bit away per step
2DF2: A7              AND     A                   ; and clear carry so later folds bring in zeros
2DF3: 10 FC           DJNZ    loc_2df1            ; 
2DF5: 47              LD      B,A                 
2DF6: 3A 1A 60        LD      A,(workRam+1A)      ; fire on the frames that land zero under the mask
2DF9: A0              AND     B                   
2DFA: C0              RET     NZ                  
2DFB: 3E 01           LD      A,$01               
2DFD: 32 A0 63        LD      (workRam+3A0),A     ; raise the insert request
2E00: 32 9A 63        LD      (workRam+39A),A     ; and the request that moves with it
2E03: C9              RET                         

; The actor-object scan loop. Two skip gates open it: the shared per-board gate with
; mask 0x04, which is the current board's bit only on board 3 (75m), so 25m, 50m and
; 100m skip the whole routine; and the shared alive gate, requiring Mario to be alive
; and being processed. With both open it seeds two scan cursors at objArray65 and
; actorSprites and calls the per-object updater ten times; that updater reads the
; current object and its paired sprite record through the cursors and, in its shared
; advance tail, steps the object cursor by 16 and the sprite cursor by 4, so ten calls
; sweep the whole array one object per pass. The board half is derivable right here --
; mask 0x04 is board 3 and nothing else. WHICH characters the ten records are is not
; identified.
update75mActorObjects:
2E04: 3E 04           LD      A,$04               ; board mask: 75m only
2E06: F7              RST     $30                 
2E07: D7              RST     $10                 ; and only while Mario is alive
2E08: DD 21 00 65     LD      IX,$6500            ; the ten spring records
2E0C: FD 21 80 69     LD      IY,$6980            ; and their paired sprite records
2E10: 06 0A           LD      B,$0A               ; one pass per record

; Advance ONE 75m spring by a frame, called once per slot by the ten-slot sweep with the
; spring's record and its paired sprite record already addressed. Four arms, in order:
; an INACTIVE slot goes to the spawn arm, which is what puts a new spring in it;
; otherwise, once every sixteen frames the low three bits of the paired sprite's tile
; code are flipped, a flicker on the drawn sprite only; state 4 goes to the retire arm;
; anything else is ordinary travel, inline here. THE TRAVEL ARM IS THE BOUNCE: X
; advances by a FIXED two pixels, so the horizontal crossing is constant and carries no
; shape at all, and everything the eye reads as a bounce comes from the vertical side,
; where the next byte of a signed delta string is ACCUMULATED into Y -- added, not
; stored. A walk pointer in the record picks that byte; its terminator sends control to
; the rewind handler, which is what makes the bounce a repeating cycle rather than a
; single arc.
advanceSpring:
2E12: DD 7E 00        LD      A,(IX+$00)          ; is this slot live?
2E15: 0F              RRCA                        
2E16: D2 A7 2E        JP      NC,spawnObjectIntoInactiveSlot; no: the spawn arm fills it
2E19: 3A 1A 60        LD      A,(workRam+1A)      ; once every sixteen frames...
2E1C: E6 0F           AND     $0F                 
2E1E: C2 29 2E        JP      NZ,loc_2e29         ; 
2E21: FD 7E 01        LD      A,(IY+$01)          ; ...flicker the drawn sprite
2E24: EE 07           XOR     $07                 
2E26: FD 77 01        LD      (IY+$01),A          

loc_2e29:
2E29: DD 7E 0D        LD      A,(IX+$0D)          ; the spring's state
2E2C: FE 04           CP      $04                 ; state 4 is the retire arm
2E2E: CA 84 2E        JP      Z,loc_2e84          ; 
2E31: DD 34 03        INC     (IX+$03)            ; travel: X advances a fixed two pixels a frame
2E34: DD 34 03        INC     (IX+$03)            
2E37: DD 6E 0E        LD      L,(IX+$0E)          ; the walk pointer into its height string
2E3A: DD 66 0F        LD      H,(IX+$0F)          
2E3D: 7E              LD      A,(HL)              ; the next signed height delta
2E3E: 4F              LD      C,A                 
2E3F: FE 7F           CP      $7F                 ; the terminator wraps the bounce
2E41: CA 9C 2E        JP      Z,loc_2e9c          ; 
2E44: 23              INC     HL                  
2E45: DD 86 05        ADD     A,(IX+$05)          ; the delta is ADDED to Y -- this is the bounce
2E48: DD 77 05        LD      (IX+$05),A          

; Carry one spring along its bounce arc, and drop it off the end of its travel. A
; convergence point inside the spring loop, reached once the loop has advanced this
; spring one step along its animation string -- either from the mid-string fall-through
; or from the arm that has just wrapped the string at its terminator. It first stores the
; current string pointer back into the record at +0x0E / +0x0F so the next pass resumes
; where this one left off: that is what continues the arc. Then a single end-of-travel
; test, taken only when BOTH halves hold -- the spring has reached the far X limit AND
; the last string byte read was the terminator. On that frame it is dropped: handed to
; the next state and given a transition sound, one latch cleared and another asserted for
; three frames. If either half is unmet the spring simply keeps going. Every path then
; mirrors the spring's position into its paired sprite record and advances both cursors.
advanceSpringArcAndDropAtTravelEnd:
2E4B: DD 75 0E        LD      (IX+$0E),L          ; store the walk pointer so the arc continues
2E4E: DD 74 0F        LD      (IX+$0F),H          
2E51: DD 7E 03        LD      A,(IX+$03)          ; has it crossed to the far side?
2E54: FE B7           CP      $B7                 
2E56: DA 6C 2E        JP      C,mirrorObjectPositionToSprite; 
2E59: 79              LD      A,C                 ; and did the height string just wrap?
2E5A: FE 7F           CP      $7F                 
2E5C: C2 6C 2E        JP      NZ,mirrorObjectPositionToSprite; 
2E5F: DD 36 0D 04     LD      (IX+$0D),$04        ; both: hand the spring to its retire state
2E63: AF              XOR     A                   
2E64: 32 83 60        LD      (workRam+83),A      ; silence the bounce sound line
2E67: 3E 03           LD      A,$03               
2E69: 32 84 60        LD      (workRam+84),A      ; and play the drop sound for three frames

; Copy the current object's X and Y into its paired sprite record -- so the hardware
; draws the object's sprite where the object currently IS -- then fall straight into the
; shared cursor advance, which steps both scan cursors on to the next object. A
; convergence tail of the per-object update loop, reached from two of that loop's state
; arms; the object record is addressed by the object-scan cursor and the sprite record by
; the paired sprite-scan cursor.
mirrorObjectPositionToSprite:
2E6C: DD 7E 03        LD      A,(IX+$03)          ; draw the object where it now is
2E6F: FD 77 00        LD      (IY+$00),A          
2E72: DD 7E 05        LD      A,(IX+$05)          
2E75: FD 77 03        LD      (IY+$03),A          

; The shared tail of the per-object update loop. However an object was processed --
; active, inactive, or in its rise/deactivate state -- every path converges here to move
; both scan cursors on by one record: the object-record cursor by its 16-byte stride and
; the paired sprite/animation cursor by its 4-byte stride. It writes NO memory. The
; cursors and the remaining-object count are handed back in registers, the same way the
; loop supplies them, and the 4 it leaves behind as the step amount is loop scratch
; nothing downstream reads.
advanceToNextObject:
2E78: 11 10 00        LD      DE,$0010            ; object records are 16 bytes apart
2E7B: DD 19           ADD     IX,DE               
2E7D: 1E 04           LD      E,$04               ; sprite records are 4
2E7F: FD 19           ADD     IY,DE               
2E81: 10 8F           DJNZ    advanceSpring       ; on to the next of the ten
2E83: C9              RET                         

loc_2e84:
2E84: 3E 03           LD      A,$03               ; the retire arm steps Y three a frame
2E86: DD 86 05        ADD     A,(IX+$05)          
2E89: DD 77 05        LD      (IX+$05),A          
2E8C: FE F8           CP      $F8                 ; off the bottom of its travel?
2E8E: DA 6C 2E        JP      C,mirrorObjectPositionToSprite; 
2E91: DD 36 03 00     LD      (IX+$03),$00        ; retire it: X zeroed, so it stops being drawn
2E95: DD 36 00 00     LD      (IX+$00),$00        ; and it stops being processed
2E99: C3 6C 2E        JP      mirrorObjectPositionToSprite; 

loc_2e9c:
2E9C: 21 AA 39        LD      HL,$39AA            ; rewind the walk to the string base
2E9F: 3E 03           LD      A,$03               
2EA1: 32 83 60        LD      (workRam+83),A      ; play the wrap sound for three frames
2EA4: C3 4B 2E        JP      advanceSpringArcAndDropAtTravelEnd; 

; The inactive-slot arm of the per-object update loop. It tests the one-shot spawn
; request, bit 0 of spawnRequest. With no spawn pending the slot stays inactive and the
; scan simply advances. With one pending it CONSUMES the request -- clearing it so no
; other inactive slot also spawns this pass -- and seeds the slot: a fixed initial Y, an
; initial X drawn from the freshly stirred random seed, its animation-string pointer
; aimed at the string base, and its state and active flags set. Either way it ends by
; advancing both scan cursors to the next object. Which object this spawns, and what it
; does in the game, are not established here.
spawnObjectIntoInactiveSlot:
2EA7: 3A 96 63        LD      A,(workRam+396)     ; is a spawn pending?
2EAA: 0F              RRCA                        
2EAB: D2 78 2E        JP      NC,advanceToNextObject; no: leave the slot empty
2EAE: AF              XOR     A                   
2EAF: 32 96 63        LD      (workRam+396),A     ; consume it, so only one slot fills this pass
2EB2: DD 36 05 50     LD      (IX+$05),$50        ; fixed starting height
2EB6: DD 36 0D 01     LD      (IX+$0D),$01        ; its first state
2EBA: CD 57 00        CALL    stirRandomSeed      ; the starting column is random
2EBD: E6 0F           AND     $0F                 
2EBF: C6 F8           ADD     A,$F8               ; spread over a 16-wide window around zero
2EC1: DD 77 03        LD      (IX+$03),A          
2EC4: DD 36 00 01     LD      (IX+$00),$01        ; bring the slot to life
2EC8: 21 AA 39        LD      HL,$39AA            ; aim its walk at the height-string base
2ECB: DD 75 0E        LD      (IX+$0E),L          
2ECE: DD 74 0F        LD      (IX+$0F),H          
2ED1: C3 78 2E        JP      advanceToNextObject ; 

; The per-frame hammer sprite and background-tune driver. Two skip gates first: the board
; gate, mask bits 0, 1 and 3 -- 25m, 50m and 100m, never 75m -- and the Mario-alive gate.
; It then picks which of the two hammer records to drive from bit 0 of the FIRST record's
; in-play field (set keeps object 1, clear picks object 2) and seeds that object's sprite
; displacement. If Mario is not holding a hammer it hands off to the pending-hammer build
; arm and stops. If he is, it clears the pending flag, switches the background tune to
; the hammer theme, stamps the object's collision half-extents, and builds the sprite
; from Mario's current pose -- the hammer tile faces the way he faces, and a swing code
; is derived from his pose. Bit 3 of hammerTimerLo selects between two swing poses, and
; the two poses stamp DIFFERENT half-extents, 0x06/0x03 and 0x05/0x06, which the board's
; hit check takes as its per-axis tolerances -- so the box the hammer smashes with
; changes shape frame to frame with the swing, and this is what changes it.
driveHammerSprite:
2ED4: 3E 0B           LD      A,$0B               ; board mask: 25m, 50m and 100m -- never 75m
2ED6: F7              RST     $30                 
2ED7: D7              RST     $10                 ; and only while Mario is alive
2ED8: 11 18 6A        LD      DE,$6A18            ; object 1's sprite slot
2EDB: DD 21 80 66     LD      IX,$6680            ; and its record
2EDF: DD 7E 01        LD      A,(IX+$01)          ; which of the two hammers is in play?
2EE2: 0F              RRCA                        
2EE3: DA ED 2E        JP      C,loc_2eed          ; 
2EE6: 11 1C 6A        LD      DE,$6A1C            ; the other hammer's sprite slot
2EE9: DD 21 90 66     LD      IX,$6690            ; and its record

loc_2eed:
2EED: DD 36 0E 00     LD      (IX+$0E),$00        ; this frame's offset from Mario
2EF1: DD 36 0F F0     LD      (IX+$0F),$F0        
2EF5: 3A 17 62        LD      A,(workRam+217)     ; is a hammer in his hands?
2EF8: 0F              RRCA                        
2EF9: D2 97 2F        JP      NC,buildPendingHammerSprite; 
2EFC: AF              XOR     A                   
2EFD: 32 18 62        LD      (workRam+218),A     ; in hand now, so no longer merely pending
2F00: 21 89 60        LD      HL,$6089            
2F03: 36 04           LD      (HL),$04            ; switch the background tune to the hammer theme
2F05: DD 36 09 06     LD      (IX+$09),$06        ; the swing's hit box, half-width...
2F09: DD 36 0A 03     LD      (IX+$0A),$03        ; ...and half-height
2F0D: 06 1E           LD      B,$1E               ; the hammer tile
2F0F: 3A 07 62        LD      A,(workRam+207)     ; the hammer faces the way Mario faces
2F12: CB 27           SLA     A                   
2F14: D2 1B 2F        JP      NC,loc_2f1b         ; 
2F17: F6 80           OR      $80                 ; restore his facing bit onto the swing code
2F19: CB F8           SET     7,B                 ; and onto the hammer tile

loc_2f1b:
2F1B: F6 08           OR      $08                 ; the fixed flag every swing code carries
2F1D: 4F              LD      C,A                 
2F1E: 3A 94 63        LD      A,(workRam+394)     ; bit 3 alternates the two swing poses
2F21: CB 5F           BIT     3,A                 
2F23: CA 43 2F        JP      Z,updateActiveHammer; 
2F26: CB C0           SET     0,B                 ; the alternate pose's tiles
2F28: CB C1           SET     0,C                 
2F2A: DD 36 09 05     LD      (IX+$09),$05        ; and its differently shaped hit box
2F2E: DD 36 0A 06     LD      (IX+$0A),$06        
2F32: DD 36 0F 00     LD      (IX+$0F),$00        ; this pose sits at a different offset
2F36: DD 36 0E F0     LD      (IX+$0E),$F0        
2F3A: CB 79           BIT     7,C                 ; on his facing bit, offset the other way
2F3C: CA 43 2F        JP      Z,updateActiveHammer; 
2F3F: DD 36 0E 10     LD      (IX+$0E),$10        ; swing to his other side

; Advance a held hammer one tick and lay down this frame's hammer sprite; end the hammer
; when its lifetime runs out. It stamps the caller's hammer tile code into Mario's sprite
; record, sets the record's attribute to the shared value, then ticks the 16-bit hammer
; duration counter and routes on how far it has run. Low byte advanced without wrapping:
; hand off to the arm that lays the record down. Low byte wrapped, high byte non-zero but
; short of the expiry value: hand off to the arm that flashes the attribute first. High
; byte at the expiry value: END the hammer -- zero the counter's high byte, clear
; marioHammerActive, deactivate the object, park its sprite at the screen origin by
; setting its X displacement to minus marioX, and restore Mario's sprite code and the
; tune saved in hammerSavedBgm. The limit is 512 frames: the updater runs once per frame.
updateActiveHammer:
2F43: 79              LD      A,C                 
2F44: 32 4D 69        LD      (workRam+94D),A     ; stamp the swing tile into Mario's sprite record
2F47: 0E 07           LD      C,$07               ; the attribute the record write stores
2F49: 21 94 63        LD      HL,$6394            
2F4C: 34              INC     (HL)                ; tick the hammer's duration counter
2F4D: C2 B7 2F        JP      NZ,selectHammerSpriteBlinkByTimer; 
2F50: 21 95 63        LD      HL,$6395            
2F53: 34              INC     (HL)                ; the low byte wrapped: carry into the high
2F54: 7E              LD      A,(HL)              
2F55: FE 02           CP      $02                 ; 512 counts is the hammer's whole life
2F57: C2 BE 2F        JP      NZ,blinkHammerSpriteOnFramePhase; not yet: flash and carry on
2F5A: AF              XOR     A                   
2F5B: 32 95 63        LD      (workRam+395),A     ; the hammer's time is up
2F5E: 32 17 62        LD      (workRam+217),A     ; it leaves his hands
2F61: DD 77 01        LD      (IX+$01),A          
2F64: 3A 03 62        LD      A,(workRam+203)     ; 
2F67: ED 44           NEG                         ; minus Mario's X...
2F69: DD 77 0E        LD      (IX+$0E),A          ; ...parks the hammer sprite at the origin
2F6C: 3A 07 62        LD      A,(workRam+207)     ; give Mario his ordinary pose back
2F6F: 32 4D 69        LD      (workRam+94D),A     ; 
2F72: DD 36 00 00     LD      (IX+$00),$00        ; and deactivate the hammer object
2F76: 3A 89 63        LD      A,(workRam+389)     ; restore the tune the hammer interrupted
2F79: 32 89 60        LD      (workRam+89),A      ; 

; The convergence point of the hammer/object sprite updater: write the finished sprite
; record, positioned at a fixed offset from Mario, and mirror that position back into
; the object record. X is Mario's X plus the object's X-displacement field, stored into
; the sprite record's X byte and mirrored into the object record's own X field, so the
; sprite tracks Mario at a constant horizontal offset; then the caller's tile code and
; attribute bytes; then Y is Mario's Y plus the object's Y-displacement field, likewise
; stored and mirrored. Nothing about it is hammer-specific -- the hammer arms merely
; happen to be its current callers. A leaf.
commitSpriteRecordAtMarioOffset:
2F7C: EB              EX      DE,HL               
2F7D: 3A 03 62        LD      A,(workRam+203)     ; the sprite sits at a fixed offset from Mario
2F80: DD 86 0E        ADD     A,(IX+$0E)          
2F83: 77              LD      (HL),A              
2F84: DD 77 03        LD      (IX+$03),A          ; mirror the position into the object record
2F87: 23              INC     HL                  
2F88: 70              LD      (HL),B              ; the caller's tile code
2F89: 23              INC     HL                  
2F8A: 71              LD      (HL),C              ; and its attribute
2F8B: 23              INC     HL                  
2F8C: 3A 05 62        LD      A,(workRam+205)     ; the other axis, likewise offset
2F8F: DD 86 0F        ADD     A,(IX+$0F)          
2F92: 77              LD      (HL),A              
2F93: DD 77 05        LD      (IX+$05),A          ; mirrored back too
2F96: C9              RET                         

; Put an un-taken hammer on screen. It gates on bit 0 of marioHammerPending: clear, and
; it does nothing at all. Set, and it prepares the object record the caller points at,
; then falls into the shared record write. The object's collision half-extents on X and
; Y are set to fixed values, which is the box Mario has to walk into to pick it up; its
; sprite is a fixed base tile carrying MARIO's current horizontal-facing bit plus a
; fixed attribute, so the waiting hammer is drawn facing the same way he is; and the
; current background tune is copied into hammerSavedBgm, so whatever swaps in the hammer
; theme can put the old one back afterwards. The shared write then lays the 4-byte
; sprite record down at Mario's position plus the object's displacement and mirrors that
; position back into the object record.
buildPendingHammerSprite:
2F97: 3A 18 62        LD      A,(workRam+218)     ; is a hammer waiting to be picked up?
2F9A: 0F              RRCA                        
2F9B: D0              RET     NC                  ; no: draw nothing
2F9C: DD 36 09 06     LD      (IX+$09),$06        ; the pickup box Mario must walk into...
2FA0: DD 36 0A 03     LD      (IX+$0A),$03        ; ...half-width and half-height
2FA4: 3A 07 62        LD      A,(workRam+207)     ; take Mario's facing bit
2FA7: 07              RLCA                        
2FA8: 3E 3C           LD      A,$3C               
2FAA: 1F              RRA                         ; so the waiting hammer faces the way he does
2FAB: 47              LD      B,A                 
2FAC: 0E 07           LD      C,$07               ; its attribute
2FAE: 3A 89 60        LD      A,(workRam+89)      ; remember the current tune...
2FB1: 32 89 63        LD      (workRam+389),A     ; ...so it can come back when the hammer ends
2FB4: C3 7C 2F        JP      commitSpriteRecordAtMarioOffset; 

; Pick which sprite-build path lays down this frame's hammer record, from how far the
; hammer's duration counter has run. It reads the counter's high byte and splits: zero,
; still inside the first 256 counts, commits the record directly with the caller's
; attribute unchanged, no blink; non-zero routes through the blink arm, which flashes the
; sprite's colour attribute on the frame counter's blink phase before committing the same
; record. The high byte sets at the halfway point of the hammer's roughly 512-count life.
; Either path lays down the same 4-byte record -- this arm only selects whether the
; attribute blinks, and the record's inputs pass through untouched.
selectHammerSpriteBlinkByTimer:
2FB7: 3A 95 63        LD      A,(workRam+395)     ; how far the hammer's life has run
2FBA: A7              AND     A                   
2FBB: CA 7C 2F        JP      Z,commitSpriteRecordAtMarioOffset; first half: no flash

; Flash the hammer sprite by overriding its colour on half of the frame counter's cycle.
; It reads frame and, on the half where bit 3 is set, forces the sprite record's
; attribute byte to 1; on the other half the caller's own attribute passes through
; untouched. Either way it then lays the finished record down. On screen that is the
; hammer sprite changing colour every 8 frames. This arm is only reached once the
; hammer's timer has run most of its course, but nothing here establishes that the flash
; is an about-to-expire warning.
blinkHammerSpriteOnFramePhase:
2FBE: 3A 1A 60        LD      A,(workRam+1A)      ; the flash runs off the frame counter
2FC1: CB 5F           BIT     3,A                 ; eight frames on, eight off
2FC3: CA 7C 2F        JP      Z,commitSpriteRecordAtMarioOffset; off half: keep the colour
2FC6: 0E 01           LD      C,$01               ; on half: force the sprite's colour
2FC8: C3 7C 2F        JP      commitSpriteRecordAtMarioOffset; 

; Run the clock down on the boards whose bonus is timed, once per frame. A per-board
; skip gate has the bits for boards 2, 3 and 4 set, so it opens on the 50m conveyor, 75m
; elevator and 100m rivet boards and closes on 25m, which paces its own bonus from
; barrel releases instead. bonusTick then counts down one per frame; until it reaches
; zero the current bonus period is still running and there is nothing more to do. When a
; period elapses it posts that period's deferred work -- a board-object spawn request
; written to spawnRequest and the bookkeeping byte that moves with it, plus a task-ring
; message -- and reloads bonusTick from bonusPeriod. bonus, the value the player sees,
; then drops by one; while it is still positive the board keeps its bonus, and when it
; reaches zero bonusExpiredStep is set to 1, which kicks off the bonus-expired sequence.
tickTimedBoardBonus:
2FCB: 3E 0E           LD      A,$0E               ; board mask: the three timed boards, not 25m
2FCD: F7              RST     $30                 
2FCE: 21 B4 62        LD      HL,$62B4            
2FD1: 35              DEC     (HL)                ; one tick off the current bonus period
2FD2: C0              RET     NZ                  ; period still running
2FD3: 3E 03           LD      A,$03               
2FD5: 32 B9 62        LD      (workRam+2B9),A     ; post this period's deferred work...
2FD8: 32 96 63        LD      (workRam+396),A     ; ...and request a board object
2FDB: 11 01 05        LD      DE,$0501            ; task: take one notch off the bonus readout
2FDE: CD 9F 30        CALL    enqueueTask         ; 
2FE1: 3A B3 62        LD      A,(workRam+2B3)     ; reload the tick for the next period
2FE4: 77              LD      (HL),A              
2FE5: 21 B1 62        LD      HL,$62B1            
2FE8: 35              DEC     (HL)                ; step the on-screen bonus down one
2FE9: C0              RET     NZ                  ; still positive: the board keeps its bonus
2FEA: 3E 01           LD      A,$01               
2FEC: 32 86 63        LD      (workRam+386),A     ; bonus exhausted: start the expiry sequence
2FEF: C9              RET                         

; Map a screen pixel (y, x) to the video-RAM address of the tile that CONTAINS it, so
; every tile probe and tile write can address "the cell under this pixel" without
; repeating the arithmetic:
;   col = (x >> 3) & 0x1f;   row = (255 - y) >> 3;   address = vram base + row*32 + col
; THE COMPLEMENT IS THE INTERESTING PART: y is complemented before the divide, so the
; game's OWN address arithmetic is vertically MIRRORED. The 180-degree rotation the
; video path renders is not something imposed on top of a conventionally addressed
; tilemap -- the game computes flipped addresses itself. The divide on x is a rotate
; rather than a shift, so the low three bits of x wrap into the top of the byte and are
; masked off, which is why the mask is five column bits and not six.
tileAddrForPixel:
2FF0: 7D              LD      A,L                 ; the pixel's column coordinate
2FF1: 0F              RRCA                        
2FF2: 0F              RRCA                        
2FF3: 0F              RRCA                        ; divide by eight -- a rotate, so low bits wrap up
2FF4: E6 1F           AND     $1F                 ; and are masked off, leaving five column bits
2FF6: 6F              LD      L,A                 
2FF7: 7C              LD      A,H                 ; the other coordinate
2FF8: 2F              CPL                         ; complemented: the game's own addressing is mirrored
2FF9: E6 F8           AND     $F8                 ; drop the sub-cell bits
2FFB: 5F              LD      E,A                 
2FFC: AF              XOR     A                   
2FFD: 67              LD      H,A                 
2FFE: CB 13           RL      E                   
3000: 17              RLA                         
3001: CB 13           RL      E                   
3003: 17              RLA                         ; scale the row up to 32 cells per row
3004: C6 74           ADD     A,$74               ; the tilemap page
3006: 57              LD      D,A                 
3007: 19              ADD     HL,DE               ; base + row*32 + column: the cell holding the pixel
3008: C9              RET                         

; A bit-field lookup over a packed table of four 2-bit fields, keyed by an input byte and
; a 2-bit selector. Bit 0 of the input picks a family of two packed bytes and bit 2 picks
; within it; on the bit-0-set family only, a selector with bit 2 set is decremented
; first. It then rotates the packed byte right two bits at a time until its low two bits
; match the selector, and returns the field AFTER the match, carry set. A next-field of 3
; instead clears bit 2 of the original input, decrements it, and returns 3 while that is
; still non-zero or 4 otherwise, carry clear. Three of the four packed bytes are
; permutations of {0,1,2,3}; the fourth is {0,0,1,2} and has no 3, so a selector of 3
; against it -- or any selector above 3 -- never matches and the rotate loops forever. A
; leaf, and pure: it reads and writes no memory. Nothing here says the result is an
; ANIMATION step; that reading comes from how callers consume it.
nextAnimationStep:
3009: 57              LD      D,A                 ; keep the input for the exit test
300A: 0F              RRCA                        ; bit 0 of the input picks the family
300B: DA 22 30        JP      C,loc_3022          ; 
300E: 0E 93           LD      C,$93               ; this family's first packed byte
3010: 0F              RRCA                        
3011: 0F              RRCA                        ; bit 2 picks within the family
3012: D2 17 30        JP      NC,loc_3017         ; 
3015: 0E 6C           LD      C,$6C               ; the other packed byte of this family

loc_3017:
3017: 07              RLCA                        
3018: DA 31 30        JP      C,loc_3031          ; 
301B: 79              LD      A,C                 
301C: E6 F0           AND     $F0                 ; 0x93 is used as 0x90 -- fields 0, 0, 1, 2
301E: 4F              LD      C,A                 
301F: C3 31 30        JP      loc_3031            ; 

loc_3022:
3022: 0E B4           LD      C,$B4               ; the other family's first packed byte
3024: 0F              RRCA                        
3025: 0F              RRCA                        
3026: D2 2B 30        JP      NC,loc_302b         ; 
3029: 0E 1E           LD      C,$1E               ; its second packed byte

loc_302b:
302B: CB 50           BIT     2,B                 ; only this family adjusts the selector
302D: CA 31 30        JP      Z,loc_3031          ; 
3030: 05              DEC     B                   

loc_3031:
3031: 79              LD      A,C                 
3032: 0F              RRCA                        
3033: 0F              RRCA                        ; walk the packed byte one 2-bit field on
3034: 4F              LD      C,A                 
3035: E6 03           AND     $03                 
3037: B8              CP      B                   ; stop on the field matching the selector
3038: C2 31 30        JP      NZ,loc_3031         ; no match yet -- 0x90 has no 3, so this can hang
303B: 79              LD      A,C                 
303C: 0F              RRCA                        
303D: 0F              RRCA                        
303E: E6 03           AND     $03                 ; the field AFTER the match is the answer
3040: FE 03           CP      $03                 
3042: C0              RET     NZ                  ; under 3: return it with carry set
3043: CB 92           RES     2,D                 ; a 3 takes the two-stage exit on the input
3045: 15              DEC     D                   
3046: C0              RET     NZ                  ; still counting: report 3
3047: 3E 04           LD      A,$04               ; exhausted: report 4
3049: C9              RET                         

; Slide one indexed pair of playfield cells up a row, for the opening Kong-climb intro --
; the intro animates by sliding a strip of the playfield up the screen a tilemap row at a
; time, and this is one cell-pair of that slide. It reads introScrollIndex and uses it as
; the offset into two fixed video cell columns two rows apart, copying each indexed cell
; to the cell one 32-column row above it, then steps the index down. Calling it
; repeatedly walks the index down and slides a whole run of cells up a row, cell by cell.
; Both copies go through a shared displaced-copy primitive that leaves the index and the
; displacement alone, so they are set once here and reused.
scrollClimbGraphicStep:
304A: 11 E0 FF        LD      DE,$FFE0            ; minus one tilemap row -- the slide's step
304D: 3A 8E 63        LD      A,(workRam+38E)     ; which cell of the strip moves this call
3050: 4F              LD      C,A                 
3051: 06 00           LD      B,$00               
3053: 21 00 76        LD      HL,$7600            ; the first of the two cell columns
3056: CD 64 30        CALL    copyByteDisplaced   ; slide that cell one row up
3059: 21 C0 75        LD      HL,$75C0            ; the strip's other column, two rows away
305C: CD 64 30        CALL    copyByteDisplaced   ; 
305F: 21 8E 63        LD      HL,$638E            
3062: 35              DEC     (HL)                ; step the index down for the next call
3063: C9              RET                         

; A generic addressing primitive. Three values arrive from the caller -- a base address,
; an index to add to it, and a signed displacement -- and the byte at base-plus-index is
; copied to base-plus-index-plus-displacement, so the destination is simply the source
; moved by the displacement. Both additions are 16-bit and WRAP. In play it slides a
; tilemap cell by one row, by being handed minus one row's worth of cells; the display
; is rotated a quarter turn, so a tilemap row is not a row on the glass. The routine
; itself knows nothing about rows or the screen -- which is why it is
; named for the addressing it does rather than for the effect the caller gets out of it.
copyByteDisplaced:
3064: 09              ADD     HL,BC               ; source = base plus the caller's index
3065: 7E              LD      A,(HL)              ; read the byte to be moved
3066: 19              ADD     HL,DE               ; destination = source plus the displacement
3067: 77              LD      (HL),A              ; drop the copy at the displaced address
3068: C9              RET                         

; A gated INDIRECT increment, and the small indirection that lets several sequences
; share one timer. Each call ticks substateTimer down by one; only on the frame it
; reaches zero does the body run and bump a step byte. The target is reached indirectly:
; the WORD in seqAdvancePtr names an address, and the byte at THAT address is
; incremented, the pointer cell itself left untouched. Setup routines re-point it at
; whichever sequence is running -- introStep during the opening cutscene,
; boardAdvanceStep during the board-cleared interlude -- so this one helper advances
; whichever is currently armed. The increment wraps at 8 bits. This is the single-level
; gate: it ticks substateTimer alone, not the prescaler its sibling runs through.
advanceSequenceStepWhenTimerExpires:
3069: DF              RST     $18                 ; tick the sub-state timer; nothing else until it expires
306A: 2A C0 63        LD      HL,(workRam+3C0)    ; the word names whichever step is armed
306D: 34              INC     (HL)                ; advance that sequence one step
306E: C9              RET                         

; Advance one animation frame of the ten-record spriteObjBlock, once every eight calls.
; Every call bumps a private 1-in-8 phase counter, and seven of eight return at once. On
; the eighth it edits the ten 4-byte records: scroll the whole group up 4 pixels by
; adding -4 to all ten Y bytes; flip and animate four of them by exclusive-oring 0x81 --
; bit 7 the horizontal flip, bit 0 the tile's low bit -- into the code bytes of records 0
; and 1, and of records 5 and 6; then stir the random seed and exclusive-or bit 7 of the
; fresh value into record 9's code byte, so that one sprite's flip toggles on a coin-flip
; at each animation step.
animateSpriteObjectBlock:
306F: 21 AF 62        LD      HL,$62AF            ; the private 1-in-8 animation phase counter
3072: 34              INC     (HL)                ; bump it on every call
3073: 7E              LD      A,(HL)              
3074: E6 07           AND     $07                 
3076: C0              RET     NZ                  ; seven calls in eight stop here
3077: 21 0B 69        LD      HL,$690B            ; the Y byte of the first of the ten records
307A: 0E FC           LD      C,$FC               ; minus four: scroll the whole group up 4 pixels
307C: FF              RST     $38                 ; add it into all ten Y bytes; leaves the stride at 4
307D: 0E 81           LD      C,$81               ; mask: bit 7 the flip, bit 0 the tile's low bit
307F: 21 09 69        LD      HL,$6909            ; code byte of record 0 -- the pair 0 and 1
3082: CD 96 30        CALL    xorMaskStridedPair  ; toggle flip and tile bit in both
3085: 21 1D 69        LD      HL,$691D            ; code byte of record 5 -- the pair 5 and 6
3088: CD 96 30        CALL    xorMaskStridedPair  ; 
308B: CD 57 00        CALL    stirRandomSeed      ; fresh entropy for the coin flip below
308E: E6 80           AND     $80                 ; keep only the flip bit of the fresh value
3090: 21 2D 69        LD      HL,$692D            ; record 9's code byte
3093: AE              XOR     (HL)                
3094: 77              LD      (HL),A              ; that sprite's flip toggles on a coin flip
3095: C9              RET                         

; Toggle the same bit pattern in two bytes a fixed distance apart. It reads a byte, XORs
; the caller's mask into it, writes it back, steps forward by the caller's stride and
; does it once more; the count is fixed at two. Because it XORs rather than stores it
; flips exactly the bits the mask selects and leaves the rest of each byte alone, so
; running it twice on the same pair restores them. Address, mask and stride all come from
; the caller, and the stride in particular is INHERITED -- nothing on the way in reloads
; it, so a caller that has not set it gets whatever the last routine to touch it left
; behind.
xorMaskStridedPair:
3096: 06 02           LD      B,$02               ; always exactly two bytes

loc_3098:
3098: 79              LD      A,C                 
3099: AE              XOR     (HL)                ; flip the bits the caller's mask selects
309A: 77              LD      (HL),A              
309B: 19              ADD     HL,DE               ; step to the second byte by the caller's stride
309C: 10 FA           DJNZ    loc_3098            ; 
309E: C9              RET                         

; Post a two-byte [opcode, argument] message onto the task ring, the game's
; deferred-work queue. Anything that wants a unit of work done later -- add to the
; score, start a sound, bump a counter -- posts it here instead of doing it inline, and
; the main loop drains the ring and dispatches each message; dozens of call sites feed
; it. The ring is 32 two-byte slots, all inside one page, and taskTail holds only the
; LOW byte of the next slot to write. A slot is FREE only while bit 7 of its opcode byte
; is SET: the empty marker is 0xFF, not zero, and boot fills the whole ring with it. If
; the slot at the tail is occupied the ring is full there and the post is silently
; DROPPED -- nothing is written and the tail does not move. Otherwise the pair goes into
; the slot and the tail advances by two, pinned back to the ring's first slot when it
; steps past the end.
enqueueTask:
309F: E5              PUSH    HL                  
30A0: 21 C0 60        LD      HL,$60C0            ; base of the 32-slot task ring
30A3: 3A B0 60        LD      A,(workRam+B0)      ; low byte of the next slot to write
30A6: 6F              LD      L,A                 ; address that slot
30A7: CB 7E           BIT     7,(HL)              ; a slot is free only while its opcode's bit 7 is set
30A9: CA BB 30        JP      Z,loc_30bb          ; ring full here -- drop the message
30AC: 72              LD      (HL),D              ; store the message opcode
30AD: 2C              INC     L                   
30AE: 73              LD      (HL),E              ; and its argument
30AF: 2C              INC     L                   
30B0: 7D              LD      A,L                 
30B1: FE C0           CP      $C0                 ; did the tail step off the end of the ring?
30B3: D2 B8 30        JP      NC,loc_30b8         ; 
30B6: 3E C0           LD      A,$C0               ; it wrapped -- pin it back to the first slot

loc_30b8:
30B8: 32 B0 60        LD      (workRam+B0),A      ; publish the advanced tail

loc_30bb:
30BB: E1              POP     HL                  
30BC: C9              RET                         

; Zero the X byte of four fixed groups of sprite records, parking those sprites at the
; left edge. Four back-to-back stride-4 zero fills, every target on a record boundary so
; each run blanks field +0:
;   spriteBuffer+0x50,  2 records   -- records 20-21
;   spriteBuffer+0x80,  10 records  -- records 32-41
;   spriteBuffer+0xb8,  11 records  -- records 46-56
;   spriteBuffer+0x10c, 5 records   -- records 67-71
; 28 records' X byte in all. It runs at two moments in play: as Mario's death animation
; is seeded, and from the board-advance interlude once a board is cleared. The mechanism
; is exact; that the intent is to hide those particular sprite groups is inference, and
; the record identities are not pinned here.
clearSpriteColumns:
30BD: 21 50 69        LD      HL,$6950            ; first group: sprite records 20-21
30C0: 06 02           LD      B,$02               
30C2: CD E4 30        CALL    clearStridedBytes   ; zero their X byte
30C5: 2E 80           LD      L,$80               ; second group: records 32-41
30C7: 06 0A           LD      B,$0A               
30C9: CD E4 30        CALL    clearStridedBytes   ; 
30CC: 2E B8           LD      L,$B8               ; third group: records 46-56
30CE: 06 0B           LD      B,$0B               
30D0: CD E4 30        CALL    clearStridedBytes   ; 
30D3: 21 0C 6A        LD      HL,$6A0C            ; fourth group: records 67-71
30D6: 06 05           LD      B,$05               
30D8: C3 E4 30        JP      clearStridedBytes   ; tail-clear the last group

loc_30db:
30DB: 21 4C 69        LD      HL,$694C            ; the X byte of Mario's sprite record
30DE: 36 00           LD      (HL),$00            ; blank it, parking that sprite at the left edge
30E0: 2E 58           LD      L,$58               ; then six more records of the shadow buffer
30E2: 06 06           LD      B,$06               

; Zero B bytes at stride 4, starting at HL. The +4 step is applied to the LOW address
; byte alone, never as a 16-bit add, so an overflow wraps within the current 256-byte
; page and never carries into the high byte -- the clear is confined to one page, and
; that confinement is the routine's defining property. The count is decremented then
; tested, so the body always runs at least once and B = 0 on entry means 256 passes
; rather than none. Every target the game hands it lies inside the sprite shadow buffer,
; so in practice it blanks a stride-4 column -- one field of each 4-byte sprite record --
; across a run of records, during board and cutscene setup and when a player dies.
clearStridedBytes:
30E4: 7D              LD      A,L                 ; walk the LOW address byte only -- the clear stays in one page

loc_30e5:
30E5: 36 00           LD      (HL),$00            ; blank this record's field
30E7: C6 04           ADD     A,$04               ; step on one record, stride 4
30E9: 6F              LD      L,A                 
30EA: 10 F9           DJNZ    loc_30e5            ; 
30EC: C9              RET                         

; One frame of the fire service, in four steps. It computes nothing itself; what it
; contributes is the ORDER and the two gates that can end the frame's fire work early.
; First the difficulty gate: a frame that fails it is simply not one of the frames the
; fires move on, and nothing below runs -- which is how the fires get faster as the game
; gets harder without any of the code below knowing about difficulty. Then the census:
; sweep the five fire records, tally the live ones and admit at most one pending spawn;
; an empty array ends the frame here too. Then the state walk, visiting the same five
; records and running the per-fire body on each occupied one. Then the publish, gathering
; the five records into their five drawn sprite records. Both early exits are taken
; inside the callee that decides them.
updateFires:
30ED: CD FA 30        CALL    gateFireUpdateByDifficulty; difficulty paces the pass

loc_30f0:
30F0: CD 3C 31        CALL    spawnRequestedFireAndRecolorLiveFires; 
30F3: CD B1 31        CALL    advanceLiveFires    ; advance each live fire one frame
30F6: CD F3 34        CALL    publishFireSprites  ; gather the five records into sprites
30F9: C9              RET                         

; Pick this difficulty's frame gate and hand its proceed/skip decision straight back to
; the caller. It reads difficulty, clamps it to the six slots the table has, and runs
; the gate that slot names; each gate reads the low bits of the frame counter, so the
; pair paces the caller to a duty cycle that widens as difficulty climbs -- 0 and 1
; every other frame, 2 five frames in eight, 3 and 4 three in four, 5 and up seven in
; eight. Two of the six slots are DUPLICATES, which is why four gates cover six
; difficulties; that is the table's own shape. The clamp is a genuine guard but never
; fires in normal play, since difficulty's own writer already caps it at 5. It writes no
; memory at all -- its entire output is a decision it does not itself use.
gateFireUpdateByDifficulty:
30FA: 3A 80 63        LD      A,(workRam+380)     ; which frame gate paces the fires
30FD: FE 06           CP      $06                 ; the table holds only six slots
30FF: 38 02           JR      C,loc_3103          ; 
3101: 3E 05           LD      A,$05               ; anything higher shares the last, widest gate

loc_3103:
3103: EF              RST     $28                 ; run that gate; its answer is the caller's

; ---- $3104-$310F: jump table ----
3104: 10 31 10 31 1B 31 26 31 26 31 31 31

loc_3110:
3110: 3A 1A 60        LD      A,(workRam+1A)      ; the frame counter drives the duty cycle
3113: E6 01           AND     $01                 ; one bit -- a two-frame cycle
3115: FE 01           CP      $01                 
3117: C8              RET     Z                   ; odd frame -- run the fire pass
3118: 33              INC     SP                  ; even frame: unwind past the whole pass
3119: 33              INC     SP                  
311A: C9              RET                         

loc_311b:
311B: 3A 1A 60        LD      A,(workRam+1A)      ; 
311E: E6 07           AND     $07                 ; the low three bits -- an eight-frame cycle
3120: FE 05           CP      $05                 ; the first five phases of eight proceed
3122: F8              RET     M                   ; in the window -- run the fire pass
3123: 33              INC     SP                  ; otherwise unwind past the whole pass
3124: 33              INC     SP                  
3125: C9              RET                         

loc_3126:
3126: 3A 1A 60        LD      A,(workRam+1A)      ; 
3129: E6 03           AND     $03                 ; the low two bits of the frame counter
312B: FE 03           CP      $03                 ; both set on one frame in four
312D: F8              RET     M                   ; the other three run the fire pass
312E: 33              INC     SP                  ; on the fourth, unwind past the whole pass
312F: 33              INC     SP                  
3130: C9              RET                         

loc_3131:
3131: 3A 1A 60        LD      A,(workRam+1A)      ; 
3134: E6 07           AND     $07                 ; the low three bits of the frame counter
3136: FE 07           CP      $07                 ; all set on one frame in eight
3138: F8              RET     M                   ; the other seven run the fire pass
3139: 33              INC     SP                  ; on the eighth, unwind past the whole pass
313A: 33              INC     SP                  
313B: C9              RET                         

; Sweep the five objArray64 fire records at stride 0x20 and tally the live ones into
; objLiveCount. Each LIVE record has its sprite-attribute byte flagged on -- or off
; while marioHammerActive is set, so the hammer visibly recolours every live fire. Each
; EMPTY record may honour a pending insert request in eventReq313c: off 50m, or on 50m
; while difficulty does not EQUAL the running count, a raised request activates that
; free slot, consumes the request and bumps the count. That 50m early exit is an EXACT
; EQUALITY, not a population cap: it is tested only at empty records while the count is
; bumped at live ones, so the count can step straight PAST difficulty without the test
; ever seeing the boundary. On that exit the request is deliberately NOT cleared. After
; the scan the request is cleared and the count inspected: non-zero returns normally,
; zero discards the caller's return address and returns a level up, skipping the rest of
; the object pass. This is the only routine that spawns one of these during play --
; records are also activated outright at board build.
spawnRequestedFireAndRecolorLiveFires:
313C: DD 21 00 64     LD      IX,$6400            ; the first of the five fire records
3140: AF              XOR     A                   
3141: 32 A1 63        LD      (workRam+3A1),A     ; start the live tally at zero
3144: 06 05           LD      B,$05               ; five records to sweep
3146: 11 20 00        LD      DE,$0020            ; the stride between them

loc_3149:
3149: DD 7E 00        LD      A,(IX+$00)          ; the record's occupancy flag
314C: FE 00           CP      $00                 
314E: CA 7C 31        JP      Z,loc_317c          ; empty slot -- go and see if a spawn is pending
3151: 3A A1 63        LD      A,(workRam+3A1)     ; live record: the running tally
3154: 3C              INC     A                   
3155: 32 A1 63        LD      (workRam+3A1),A     ; count it
3158: 3E 01           LD      A,$01               
315A: DD 77 08        LD      (IX+$08),A          ; flag the fire's sprite attribute on
315D: 3A 17 62        LD      A,(workRam+217)     ; is Mario swinging a hammer?
3160: FE 01           CP      $01                 
3162: C2 6A 31        JP      NZ,loc_316a         ; 
3165: 3E 00           LD      A,$00               
3167: DD 77 08        LD      (IX+$08),A          ; hammer up: recolour every live fire

loc_316a:
316A: DD 19           ADD     IX,DE               ; on to the next record
316C: 10 DB           DJNZ    loc_3149            ; 
316E: 21 A0 63        LD      HL,$63A0            ; the pending insert request
3171: 36 00           LD      (HL),$00            ; consume it, honoured or not
3173: 3A A1 63        LD      A,(workRam+3A1)     ; the finished tally
3176: FE 00           CP      $00                 
3178: C0              RET     NZ                  ; something is live -- carry on with the pass
3179: 33              INC     SP                  ; array empty: unwind past the rest of the pass
317A: 33              INC     SP                  
317B: C9              RET                         

loc_317c:
317C: 3A A1 63        LD      A,(workRam+3A1)     ; empty slot: how many are live so far
317F: FE 05           CP      $05                 
3181: CA 6A 31        JP      Z,loc_316a          ; array already full -- nothing can go in
3184: 3A 27 62        LD      A,(workRam+227)     ; 50m is the only board that caps spawns here
3187: FE 02           CP      $02                 
3189: C2 95 31        JP      NZ,loc_3195         ; 
318C: 3A A1 63        LD      A,(workRam+3A1)     ; 
318F: 4F              LD      C,A                 
3190: 3A 80 63        LD      A,(workRam+380)     ; 
3193: B9              CP      C                   ; on 50m stop the moment the tally equals difficulty
3194: C8              RET     Z                   ; and leave the request standing for a later frame

loc_3195:
3195: 3A A0 63        LD      A,(workRam+3A0)     ; is an insert being asked for?
3198: FE 01           CP      $01                 
319A: C2 6A 31        JP      NZ,loc_316a         ; no request -- leave the slot empty
319D: DD 77 00        LD      (IX+$00),A          ; activate the slot
31A0: DD 77 18        LD      (IX+$18),A          ; mark it for the board-keyed insert walker
31A3: AF              XOR     A                   
31A4: 32 A0 63        LD      (workRam+3A0),A     ; consume the request -- one insert per sweep
31A7: 3A A1 63        LD      A,(workRam+3A1)     ; 
31AA: 3C              INC     A                   
31AB: 32 A1 63        LD      (workRam+3A1),A     ; count the new fire
31AE: C3 6A 31        JP      loc_316a            ; 

; Walk the five records of objArray64 once a frame and advance each live one. It decides
; which records get a turn and nothing about what happens to one. First the
; difficulty-and-entropy gated arm runs, stamping a field on two records of this same
; array. Then the sweep index is zeroed and objIterPtr is seeded one stride BELOW the
; array base -- the pointer is advanced before each record is read, so the first record
; visited is the array base itself and the seeded value is never read. Five iterations
; follow: advance the pointer and store it back, then, when that record's active flag is
; non-zero, run the per-object advance. An empty record is skipped but still consumes an
; iteration, so the five map one-to-one onto the five records. The pointer lives in
; memory because that is how the record base reaches the per-object advance, which loads
; its record pointer back out of objIterPtr.
advanceLiveFires:
31B1: CD DD 31        CALL    armAlternateFireModeAtHighDifficulty; 
31B4: AF              XOR     A                   
31B5: 32 A2 63        LD      (workRam+3A2),A     ; start the sweep index at zero
31B8: 21 E0 63        LD      HL,$63E0            ; one stride BELOW the array base
31BB: 22 C8 63        LD      (workRam+3C8),HL    ; where the per-fire body finds its record

loc_31be:
31BE: 2A C8 63        LD      HL,(workRam+3C8)    ; re-read it -- a callee may have moved it
31C1: 01 20 00        LD      BC,$0020            
31C4: 09              ADD     HL,BC               ; step on to the next record
31C5: 22 C8 63        LD      (workRam+3C8),HL    ; publish it before the body is entered
31C8: 7E              LD      A,(HL)              ; the record's occupancy flag
31C9: A7              AND     A                   
31CA: CA D0 31        JP      Z,loc_31d0          ; empty slot -- it still costs an iteration
31CD: CD 02 32        CALL    advanceFire         ; advance this one fire

loc_31d0:
31D0: 3A A2 63        LD      A,(workRam+3A2)     ; 
31D3: 3C              INC     A                   
31D4: 32 A2 63        LD      (workRam+3A2),A     ; 
31D7: FE 05           CP      $05                 ; five records to a sweep
31D9: C2 BE 31        JP      NZ,loc_31be         ; 
31DC: C9              RET                         

; Stamp mode 2 into one field of two fire records, but only on a hard board and only on a
; rare entropy draw. Two gates, both of which must open on the same pass. First,
; difficulty at least 3: the test is a SIGNED one on (difficulty - 3), so in play it
; means 3, 4 or 5, and faithfully it closes again once difficulty reaches 131, where the
; signed difference turns negative -- a value normal play never reaches. Second, an
; entropy draw that comes up exactly 1: the draw is the low two bits of random, or the
; frame counter substituted in when those two bits are exactly 1, so the draw is 1 only
; when both are. Past both gates the constant 2 is stamped into field +0x19 of records 1
; and 3 of objArray64, and nothing else is written on any path. What mode 2 makes a fire
; DO is not established -- no reader of that field appears here.
armAlternateFireModeAtHighDifficulty:
31DD: 3A 80 63        LD      A,(workRam+380)     ; difficulty gates this arming pass
31E0: FE 03           CP      $03                 
31E2: F8              RET     M                   ; below difficulty 3 nothing is armed
31E3: CD F6 31        CALL    loc_31f6            ; take the entropy draw
31E6: FE 01           CP      $01                 
31E8: C0              RET     NZ                  ; only a draw of exactly 1 opens the second gate
31E9: 21 39 64        LD      HL,$6439            ; field +0x19 of fire record 1
31EC: 3E 02           LD      A,$02               
31EE: 77              LD      (HL),A              ; stamp mode 2 into it
31EF: 21 79 64        LD      HL,$6479            ; the same field of record 3
31F2: 3E 02           LD      A,$02               
31F4: 77              LD      (HL),A              
31F5: C9              RET                         

loc_31f6:
31F6: 3A 18 60        LD      A,(workRam+18)      ; the entropy draw's first source
31F9: E6 03           AND     $03                 
31FB: FE 01           CP      $01                 
31FD: C0              RET     NZ                  ; 0, 2 or 3 is the draw itself
31FE: 3A 1A 60        LD      A,(workRam+1A)      ; exactly 1 substitutes the frame counter
3201: C9              RET                         

; Advance ONE fire by a frame. The record arrives through objIterPtr and is deliberately
; re-read at three points, because a callee may have moved it on. ROUTING: a record
; whose insert-requested field is 1 is not a live fire yet and belongs to the
; board-keyed insert walker. Otherwise a record on the LOW side of the state split gets
; one of two timer ticks, chosen by a record field. BOTH arms reach the heading state
; machine, by different routes: the arm the field selects with 2 ticks a dwell counter and
; rejoins PAST the random gate, unconditionally. Only the other arm -- the reroll timer --
; passes through the gate, and then only on a pass that finds the low two bits of random
; clear. THAT IS THE REROLL: the timer decides a new heading is due and the random gate
; keeps the rerolls on that arm off a fixed cadence. MOVEMENT: the working X steps ONE pixel the way the
; state field points, and the tile 12px below the new position is judged -- out of band
; and the step is UNDONE and the heading reversed; in band, only the X edges 16 and 240
; can re-arm it. PUBLISH: the working X becomes the drawn X and the working Y plus a
; table byte the drawn Y, the table index counting down every pass and reloading at 0.
; What that table holds is not established.
advanceFire:
3202: DD 2A C8 63     LD      IX,(workRam+3C8)    ; the record the walk is pointing at
3206: DD 7E 18        LD      A,(IX+$18)          ; is this record still waiting to be inserted?
3209: FE 01           CP      $01                 
320B: CA 7A 32        JP      Z,loc_327a          ; not a live fire yet -- the insert walker takes it
320E: DD 7E 0D        LD      A,(IX+$0D)          ; the fire's state
3211: FE 04           CP      $04                 
3213: F2 30 32        JP      P,loc_3230          ; the climbing states skip the timers below
3216: DD 7E 19        LD      A,(IX+$19)          ; which of the two timer ticks this record uses
3219: FE 02           CP      $02                 
321B: CA 7E 32        JP      Z,loc_327e          ; the dwell-counter arm
321E: CD 0F 33        CALL    tickFireTimerAndRerollDirection; reroll the heading

loc_3221:
3221: 3A 18 60        LD      A,(workRam+18)      ; keep the rerolls off a fixed cadence
3224: E6 03           AND     $03                 
3226: C2 33 32        JP      NZ,loc_3233         ; not a reroll pass -- straight on to the step

loc_3229:
3229: DD 7E 0D        LD      A,(IX+$0D)          
322C: A7              AND     A                   
322D: CA 57 32        JP      Z,loc_3257          ; a fire back at state 0 has nothing to move

loc_3230:
3230: CD 3D 33        CALL    driveFireLadderClimb; decide about leaving for a ladder

loc_3233:
3233: DD 7E 0D        LD      A,(IX+$0D)          
3236: FE 04           CP      $04                 
3238: F2 91 32        JP      P,loc_3291          ; on a ladder -- the vertical arm instead
323B: CD AD 33        CALL    walkFireOneStep     ; one pixel along the current heading
323E: CD 8C 29        CALL    turnFireAtGroundEdge; has the ground below run out?
3241: FE 01           CP      $01                 
3243: CA 97 32        JP      Z,loc_3297          ; at an edge -- undo the step and turn round
3246: DD 2A C8 63     LD      IX,(workRam+3C8)    ; re-read the record; the probe may have moved it
324A: DD 7E 0E        LD      A,(IX+$0E)          ; the working X after the step
324D: FE 10           CP      $10                 
324F: DA 8C 32        JP      C,loc_328c          ; past the low bound -- send it back the other way
3252: FE F0           CP      $F0                 
3254: D2 84 32        JP      NC,loc_3284         ; past the high bound -- likewise

loc_3257:
3257: DD 7E 13        LD      A,(IX+$13)          ; index into the drawn-Y offset table
325A: FE 00           CP      $00                 
325C: C2 B9 32        JP      NZ,loc_32b9         ; 
325F: 3E 11           LD      A,$11               ; the index reloads at 17 when it runs out

loc_3261:
3261: DD 77 13        LD      (IX+$13),A          ; store the stepped index
3264: 16 00           LD      D,$00               
3266: 5F              LD      E,A                 
3267: 21 7A 3A        LD      HL,$3A7A            ; the table the offset comes from
326A: 19              ADD     HL,DE               
326B: 7E              LD      A,(HL)              ; this pass's Y offset
326C: DD 46 0E        LD      B,(IX+$0E)          
326F: DD 70 03        LD      (IX+$03),B          ; the working X becomes the drawn X
3272: DD 4E 0F        LD      C,(IX+$0F)          
3275: 81              ADD     A,C                 
3276: DD 77 05        LD      (IX+$05),A          ; working Y plus the offset becomes the drawn Y
3279: C9              RET                         

loc_327a:
327A: CD BD 32        CALL    loc_32bd            ; hand the record to the board-keyed insert walker
327D: C9              RET                         

loc_327e:
327E: CD D6 32        CALL    loc_32d6            ; the other arm: the dwell counter
3281: C3 29 32        JP      loc_3229            ; rejoin past the random gate

loc_3284:
3284: 3E 02           LD      A,$02               ; the heading that steps the working X down

loc_3286:
3286: DD 77 0D        LD      (IX+$0D),A          ; store the new heading
3289: C3 57 32        JP      loc_3257            ; 

loc_328c:
328C: 3E 01           LD      A,$01               ; the heading that steps the working X up
328E: C3 86 32        JP      loc_3286            ; 

loc_3291:
3291: CD E7 33        CALL    loc_33e7            ; climbing: animate and step the height
3294: C3 57 32        JP      loc_3257            ; 

loc_3297:
3297: DD 2A C8 63     LD      IX,(workRam+3C8)    ; re-read the record after the ground probe
329B: DD 7E 0D        LD      A,(IX+$0D)          
329E: FE 01           CP      $01                 
32A0: C2 B1 32        JP      NZ,loc_32b1         ; 
32A3: 3E 02           LD      A,$02               ; it was heading one way -- reverse it
32A5: DD 35 0E        DEC     (IX+$0E)            ; and undo the pixel just taken

loc_32a8:
32A8: DD 77 0D        LD      (IX+$0D),A          ; store the reversed heading
32AB: CD C3 33        CALL    settleFireOnGirderSlope; re-settle onto the girder slope
32AE: C3 57 32        JP      loc_3257            ; 

loc_32b1:
32B1: 3E 01           LD      A,$01               ; the other heading, reversed the same way
32B3: DD 34 0E        INC     (IX+$0E)            ; undo the pixel
32B6: C3 A8 32        JP      loc_32a8            ; 

loc_32b9:
32B9: 3D              DEC     A                   ; step the table index down one
32BA: C3 61 32        JP      loc_3261            ; 

loc_32bd:
32BD: 3A 27 62        LD      A,(workRam+227)     ; which board decides the walker
32C0: FE 01           CP      $01                 
32C2: CA CE 32        JP      Z,loc_32ce          ; 25m: the flat-table walk
32C5: FE 02           CP      $02                 
32C7: CA D2 32        JP      Z,loc_32d2          ; 50m: its direction-selected twin
32CA: CD B9 34        CALL    loc_34b9            ; any other board: seed the record from a template
32CD: C9              RET                         

loc_32ce:
32CE: CD 2C 34        CALL    loc_342c            ; 
32D1: C9              RET                         

loc_32d2:
32D2: CD 78 34        CALL    loc_3478            ; 
32D5: C9              RET                         

loc_32d6:
32D6: DD 7E 1C        LD      A,(IX+$1C)          ; the record's interval down-counter
32D9: FE 00           CP      $00                 
32DB: C2 FD 32        JP      NZ,loc_32fd         ; still counting -- just step it down
32DE: DD 7E 1D        LD      A,(IX+$1D)          ; run out: is the position compare armed?
32E1: FE 01           CP      $01                 
32E3: C2 0B 33        JP      NZ,loc_330b         ; not armed -- hand on to the periodic timer
32E6: DD 36 1D 00     LD      (IX+$1D),$00        ; disarm it; the compare happens once
32EA: 3A 05 62        LD      A,(workRam+205)     ; where Mario's row is now
32ED: DD 46 0F        LD      B,(IX+$0F)          
32F0: 90              SUB     B                   ; against the object's own limit
32F1: DA 03 33        JP      C,loc_3303          ; Mario short of it -- let the counter drain
32F4: DD 36 1C FF     LD      (IX+$1C),$FF        ; otherwise reload the counter to its maximum

loc_32f8:
32F8: DD 36 0D 00     LD      (IX+$0D),$00        ; and hold the object in state 0
32FC: C9              RET                         

loc_32fd:
32FD: DD 35 1C        DEC     (IX+$1C)            ; tick the interval down
3300: C2 F8 32        JP      NZ,loc_32f8         ; still running -- hold state 0 and stop

loc_3303:
3303: DD 36 19 00     LD      (IX+$19),$00        ; spent: clear the two exit fields
3307: DD 36 1C 00     LD      (IX+$1C),$00        

loc_330b:
330B: CD 0F 33        CALL    tickFireTimerAndRerollDirection; tick the periodic timer
330E: C9              RET                         

; Tick one fire's periodic timer, the record's countdown field at +0x16. While that is
; still counting, the decrement is the whole routine. On the pass that finds it already
; at zero the timer is reloaded and the record's state field at +0x0d is reset to 0;
; then, ONLY when the low bit of random is set, the state is advanced to 1. The shared
; decrement then runs on the freshly reloaded value, leaving the timer at 42, so the
; cycle repeats every 43 passes -- a coin flip at each expiry moving the state between
; its only two values, the erratic half of the object's motion. The original also
; carries a branch that would set the state to 2, guarded by "state == 1"; the reset to
; 0 happens immediately before that test, so it can never match.
tickFireTimerAndRerollDirection:
330F: DD 7E 16        LD      A,(IX+$16)          ; the fire's periodic countdown
3312: FE 00           CP      $00                 
3314: C2 32 33        JP      NZ,loc_3332         ; still counting -- only the tick below happens
3317: DD 36 16 2B     LD      (IX+$16),$2B        ; expired: reload 43 ticks
331B: DD 36 0D 00     LD      (IX+$0D),$00        ; and reset the heading to 0
331F: 3A 18 60        LD      A,(workRam+18)      ; a coin flip decides the new heading
3322: 0F              RRCA                        
3323: D2 32 33        JP      NC,loc_3332         ; tails -- leave the heading at 0
3326: DD 7E 0D        LD      A,(IX+$0D)          
3329: FE 01           CP      $01                 
332B: CA 36 33        JP      Z,loc_3336          ; the reset just above makes this unreachable
332E: DD 36 0D 01     LD      (IX+$0D),$01        ; heads -- the other heading

loc_3332:
3332: DD 35 16        DEC     (IX+$16)            ; every path ends by ticking the timer down
3335: C9              RET                         

loc_3336:
3336: DD 36 0D 02     LD      (IX+$0D),$02        ; the arm that can never be entered
333A: C3 32 33        JP      loc_3332            ; 

; Decide when a fire leaves a girder for a ladder, and watch for its arrival. It never
; moves the fire itself -- it only chooses a destination and a direction; the fire's
; state byte picks which half runs. ON FOOT: a height guard comes first, and on the three
; lower boards a fire that has risen above that line abandons the climb decision
; outright, while on the top board the height test does not apply at all. Past it the
; fire's X is looked up in the type-0 object table, whose entries pair each key with TWO
; bytes; the discriminator is the fire's own Y base, so the lookup returns whichever of
; the pair it is NOT standing on. That byte becomes the destination and the tag becomes
; the direction. The descent is conditional where the ascent is not: it is taken only
; while the fire's row is above Mario's, so a fire level with him or below never sets off
; downward. TRAVELLING: both states poll the biased Y base against the stored destination
; and drop back to on-foot on the nose. The ascent alone, and only in one mode, raises an
; arrival mark.
driveFireLadderClimb:
333D: DD 7E 0D        LD      A,(IX+$0D)          ; the fire's state picks which half runs
3340: FE 08           CP      $08                 
3342: CA 71 33        JP      Z,loc_3371          ; already climbing -- just watch for arrival
3345: FE 04           CP      $04                 
3347: CA 8A 33        JP      Z,loc_338a          ; already descending -- likewise
334A: CD A1 33        CALL    loc_33a1            ; on foot: the board gate and height guard
334D: DD 7E 0F        LD      A,(IX+$0F)          ; the fire's own height
3350: C6 08           ADD     A,$08               ; biased the way the table's heights are
3352: 57              LD      D,A                 ; the end it is standing on, for the lookup
3353: DD 7E 0E        LD      A,(IX+$0E)          ; its X is the key into the ladder table
3356: 01 15 00        LD      BC,$0015            ; twenty-one entries to search
3359: CD 6E 23        CALL    findOppositeLadderEnd; the other end of this ladder
335C: A7              AND     A                   ; the tag says which end it started from
335D: CA 99 33        JP      Z,loc_3399          ; the far end -- set off upward
3360: DD 70 1F        LD      (IX+$1F),B          ; the height this climb ends at
3363: 3A 05 62        LD      A,(workRam+205)     ; Mario's row
3366: 47              LD      B,A                 
3367: DD 7E 0F        LD      A,(IX+$0F)          
336A: 90              SUB     B                   ; is the fire's row above Mario's?
336B: D0              RET     NC                  ; level with him or below -- never head down
336C: DD 36 0D 04     LD      (IX+$0D),$04        ; above him -- start descending
3370: C9              RET                         

loc_3371:
3371: DD 7E 0F        LD      A,(IX+$0F)          ; climbing: where the fire has got to
3374: C6 08           ADD     A,$08               
3376: DD 46 1F        LD      B,(IX+$1F)          ; against the height it set off for
3379: B8              CP      B                   
337A: C0              RET     NZ                  ; not there yet
337B: DD 36 0D 00     LD      (IX+$0D),$00        ; arrived -- back on foot
337F: DD 7E 19        LD      A,(IX+$19)          
3382: FE 02           CP      $02                 
3384: C0              RET     NZ                  
3385: DD 36 1D 01     LD      (IX+$1D),$01        ; an arriving ascent in mode 2 raises its mark
3389: C9              RET                         

loc_338a:
338A: DD 7E 0F        LD      A,(IX+$0F)          ; descending: where the fire has got to
338D: C6 08           ADD     A,$08               
338F: DD 46 1F        LD      B,(IX+$1F)          
3392: B8              CP      B                   
3393: C0              RET     NZ                  ; not on the destination yet
3394: DD 36 0D 00     LD      (IX+$0D),$00        ; arrived -- back on foot, and no mark
3398: C9              RET                         

loc_3399:
3399: DD 70 1F        LD      (IX+$1F),B          ; the height above to climb to
339C: DD 36 0D 08     LD      (IX+$0D),$08        ; set off up the ladder
33A0: C9              RET                         

loc_33a1:
33A1: 3E 07           LD      A,$07               ; mask: 25m, 50m and 75m -- not the rivet board
33A3: F7              RST     $30                 ; closed on 100m -- skip the height test and carry on
33A4: DD 7E 0F        LD      A,(IX+$0F)          ; the fire's height
33A7: FE 59           CP      $59                 ; against the 89-pixel line
33A9: D0              RET     NC                  ; at or past it -- the climb decision goes ahead
33AA: 33              INC     SP                  ; risen above the line: abandon the climb decision
33AB: 33              INC     SP                  
33AC: C9              RET                         

; Step one fire a single position along its current heading. Its state byte picks the
; heading, and the same branch does two things at once: it steps the fire's working X one
; pixel and it sets or clears the sprite tile code's flip bit, so the mirrored sprite
; always faces the way the fire is moving. The "step up" state steps X up and sets the
; flip bit; every other state steps X down and clears it. The animation clock then runs
; over the SAME sprite code byte and a per-fire down-counter, so the ORDER matters -- the
; flip bit is written first and the animation step lands on top of it. Control then falls
; into the slope tail, which on the girder board re-snaps the working Y to the sloped
; girder under the fire's new X and does nothing elsewhere. The stepped coordinate is the
; WORKING X, one stage upstream of the drawn X it is copied into.
walkFireOneStep:
33AD: DD 7E 0D        LD      A,(IX+$0D)          ; the heading the state field carries
33B0: FE 01           CP      $01                 
33B2: CA D9 33        JP      Z,loc_33d9          ; the one state that steps the working X up
33B5: DD 7E 07        LD      A,(IX+$07)          ; the fire's sprite tile code
33B8: E6 7F           AND     $7F                 ; clear the flip bit to face this way
33BA: DD 77 07        LD      (IX+$07),A          
33BD: DD 35 0E        DEC     (IX+$0E)            ; step the working X down one pixel

loc_33c0:
33C0: CD 09 34        CALL    stepObjectSpriteFrame; advance the sprite animation

; A short tail, guarded on board being the girder board: re-step one coordinate of the
; fire the caller pointed at, so its height settles onto the slope of the girder it is
; standing on. It reads the companion coordinate, the coordinate to be stepped and the
; record's state field, hands them to the girder-slope single-step, and stores the
; result back into the same field. On any other board it does nothing at all -- the
; caller's own field work still happens, but this step is gated.
settleFireOnGirderSlope:
33C3: 3A 27 62        LD      A,(workRam+227)     ; only the girder board has a slope to settle onto
33C6: FE 01           CP      $01                 
33C8: C0              RET     NZ                  ; anywhere else this tail does nothing
33C9: DD 66 0E        LD      H,(IX+$0E)          ; the fire's X, which the slope is keyed to
33CC: DD 6E 0F        LD      L,(IX+$0F)          ; the height to be settled
33CF: DD 46 0D        LD      B,(IX+$0D)          
33D2: CD 33 23        CALL    snapYToGirder       ; one step along the girder slope
33D5: DD 75 0F        LD      (IX+$0F),L          ; store the settled height back
33D8: C9              RET                         

loc_33d9:
33D9: DD 7E 07        LD      A,(IX+$07)          ; the sprite tile code
33DC: F6 80           OR      $80                 ; set the flip bit for this heading
33DE: DD 77 07        LD      (IX+$07),A          
33E1: DD 34 0E        INC     (IX+$0E)            ; step the working X up one pixel
33E4: C3 C0 33        JP      loc_33c0            ; join the animation step and the slope tail

loc_33e7:
33E7: CD 09 34        CALL    stepObjectSpriteFrame; advance the sprite animation first
33EA: DD 7E 0D        LD      A,(IX+$0D)          ; the object's state
33ED: FE 08           CP      $08                 
33EF: C2 05 34        JP      NZ,loc_3405         ; any state but 8 steps the counter up
33F2: DD 7E 14        LD      A,(IX+$14)          ; state 8: the sub-timer that paces the down-step
33F5: A7              AND     A                   
33F6: C2 01 34        JP      NZ,loc_3401         ; still running -- only tick it
33F9: DD 36 14 02     LD      (IX+$14),$02        ; reload 2 -- the counter falls once in three calls
33FD: DD 35 0F        DEC     (IX+$0F)            ; and step the counter down
3400: C9              RET                         

loc_3401:
3401: DD 35 14        DEC     (IX+$14)            ; tick the sub-timer
3404: C9              RET                         

loc_3405:
3405: DD 34 0F        INC     (IX+$0F)            ; step the counter up
3408: C9              RET                         

; Advance an object's animation sprite tile on a period-3 timer. A per-object
; down-counter at record byte +0x15 counts every call, and while it is still running the
; routine only ticks it down and stops. When it reaches 0 it is reloaded to 2 and the
; object's sprite code at +0x07 is stepped forward by one; that path returns WITHOUT also
; decrementing, so the counter runs 0 -> 2 -> 1 -> 0 and the sprite advances once every
; three calls, not every other one. On the steps whose sprite code comes up with an all-ones low
; nibble, once every sixteen, bit 1 of the code is TOGGLED; because the nibble is all
; ones at that point the bit is set, so the toggle clears it -- a flip, not a blind set.
; Which object this services is not established here.
stepObjectSpriteFrame:
3409: DD 7E 15        LD      A,(IX+$15)          ; the per-object animation down-counter
340C: A7              AND     A                   
340D: C2 28 34        JP      NZ,loc_3428         ; still counting -- the sprite holds this frame
3410: DD 36 15 02     LD      (IX+$15),$02        ; reload 2 -- the sprite steps once in three calls
3414: DD 34 07        INC     (IX+$07)            ; step the sprite tile code on one frame
3417: DD 7E 07        LD      A,(IX+$07)          
341A: E6 0F           AND     $0F                 
341C: FE 0F           CP      $0F                 ; only the all-ones low nibble carries on
341E: C0              RET     NZ                  
341F: DD 7E 07        LD      A,(IX+$07)          
3422: EE 02           XOR     $02                 ; once every sixteen steps, clear bit 1 of the code
3424: DD 77 07        LD      (IX+$07),A          
3427: C9              RET                         

loc_3428:
3428: DD 35 15        DEC     (IX+$15)            ; tick the animation counter down
342B: C9              RET                         

loc_342c:
342C: DD 6E 1A        LD      L,(IX+$1A)          ; the saved walk pointer, low byte
342F: DD 66 1B        LD      H,(IX+$1B)          ; and high
3432: AF              XOR     A                   
3433: 01 00 00        LD      BC,$0000            
3436: ED 4A           ADC     HL,BC               ; test the saved pointer for zero
3438: C2 42 34        JP      NZ,loc_3442         ; non-zero -- resume where the last pass stopped
343B: 21 8C 3A        LD      HL,$3A8C            ; a fresh walk: the start of the path table
343E: DD 36 03 26     LD      (IX+$03),$26        ; and stamp the object's starting X

loc_3442:
3442: DD 34 03        INC     (IX+$03)            ; march the X one step this frame

loc_3445:
3445: 7E              LD      A,(HL)              ; the next entry of the path table
3446: FE AA           CP      $AA                 
3448: CA 56 34        JP      Z,loc_3456          ; the end-of-table marker finishes the walk
344B: DD 77 05        LD      (IX+$05),A          ; an ordinary entry becomes the object's Y
344E: 23              INC     HL                  
344F: DD 75 1A        LD      (IX+$1A),L          ; save the advanced pointer for the next pass
3452: DD 74 1B        LD      (IX+$1B),H          
3455: C9              RET                         

loc_3456:
3456: AF              XOR     A                   
3457: DD 77 13        LD      (IX+$13),A          ; walk finished: clear the animation-state bytes
345A: DD 77 18        LD      (IX+$18),A          
345D: DD 77 0D        LD      (IX+$0D),A          
3460: DD 77 1C        LD      (IX+$1C),A          
3463: DD 7E 03        LD      A,(IX+$03)          
3466: DD 77 0E        LD      (IX+$0E),A          ; latch the final X
3469: DD 7E 05        LD      A,(IX+$05)          
346C: DD 77 0F        LD      (IX+$0F),A          ; and the final Y
346F: DD 36 1A 00     LD      (IX+$1A),$00        ; rewind the pointer -- the next pass reads as fresh
3473: DD 36 1B 00     LD      (IX+$1B),$00        
3477: C9              RET                         

loc_3478:
3478: DD 6E 1A        LD      L,(IX+$1A)          ; the saved walk pointer, low byte
347B: DD 66 1B        LD      H,(IX+$1B)          ; and high
347E: AF              XOR     A                   
347F: 01 00 00        LD      BC,$0000            
3482: ED 4A           ADC     HL,BC               ; test it for zero
3484: C2 9A 34        JP      NZ,loc_349a         ; already walking -- keep the direction it chose
3487: 21 AC 3A        LD      HL,$3AAC            ; a fresh walk: this twin's own path table
348A: 3A 03 62        LD      A,(workRam+203)     ; the context byte the direction comes from
348D: CB 7F           BIT     7,A                 
348F: CA A8 34        JP      Z,loc_34a8          ; 
3492: DD 36 0D 01     LD      (IX+$0D),$01        ; bit set: mark the walk forward
3496: DD 36 03 7E     LD      (IX+$03),$7E        ; and seed the X it starts from

loc_349a:
349A: DD 7E 0D        LD      A,(IX+$0D)          ; the direction mark
349D: FE 01           CP      $01                 
349F: C2 B3 34        JP      NZ,loc_34b3         ; 
34A2: DD 34 03        INC     (IX+$03)            ; forward -- march the X up
34A5: C3 45 34        JP      loc_3445            ; the shared tail supplies the Y

loc_34a8:
34A8: DD 36 0D 02     LD      (IX+$0D),$02        ; bit clear: mark the walk backward
34AC: DD 36 03 80     LD      (IX+$03),$80        ; with the other starting X
34B0: C3 9A 34        JP      loc_349a            ; 

loc_34b3:
34B3: DD 35 03        DEC     (IX+$03)            ; backward -- march the X down
34B6: C3 45 34        JP      loc_3445            ; 

loc_34b9:
34B9: 3A 27 62        LD      A,(workRam+227)     ; 
34BC: FE 03           CP      $03                 
34BE: C8              RET     Z                   ; 75m seeds nothing here
34BF: 3A 03 62        LD      A,(workRam+203)     ; Mario's screen half picks the template
34C2: CB 7F           BIT     7,A                 
34C4: C2 ED 34        JP      NZ,loc_34ed         ; 
34C7: 21 C4 3A        LD      HL,$3AC4            ; the template table for one half

loc_34ca:
34CA: 06 00           LD      B,$00               
34CC: 3A 19 60        LD      A,(workRam+19)      ; the spin counter picks one of its four entries
34CF: E6 06           AND     $06                 
34D1: 4F              LD      C,A                 
34D2: 09              ADD     HL,BC               ; index the two-byte entry
34D3: 7E              LD      A,(HL)              
34D4: DD 77 03        LD      (IX+$03),A          ; stamp it as the object's X
34D7: DD 77 0E        LD      (IX+$0E),A          ; and into the working X beside it
34DA: 23              INC     HL                  
34DB: 7E              LD      A,(HL)              
34DC: DD 77 05        LD      (IX+$05),A          ; the second byte is the object's Y
34DF: DD 77 0F        LD      (IX+$0F),A          ; and its working Y
34E2: AF              XOR     A                   
34E3: DD 77 0D        LD      (IX+$0D),A          ; clear the state
34E6: DD 77 18        LD      (IX+$18),A          ; and the two trailing fields
34E9: DD 77 1C        LD      (IX+$1C),A          
34EC: C9              RET                         

loc_34ed:
34ED: 21 D4 3A        LD      HL,$3AD4            ; the other half's template table
34F0: C3 CA 34        JP      loc_34ca            ; 

; Publish the five records of objArray64 into five 4-byte sprite records in the shadow
; buffer. For each record whose occupancy flag is non-zero it copies four fields in a
; fixed order -- X to +0, sprite code to +1, attribute to +2, Y to +3 -- which is the
; object-to-sprite mapping used across this game. The occupancy flag itself is not
; copied. An EMPTY record is skipped but STILL consumes a destination record, so
; destinations stay aligned one-to-one with sources: five objects always produce five
; destination records, and an empty one simply leaves its four bytes untouched. The
; destination sits inside spriteBuffer, the block the DMA copies into sprite RAM every
; vblank, so these twenty bytes are what the video hardware reads.
publishFireSprites:
34F3: 21 00 64        LD      HL,$6400            ; the first of the five fire records
34F6: 11 D0 69        LD      DE,$69D0            ; and the five sprite records they publish into
34F9: 06 05           LD      B,$05               

loc_34fb:
34FB: 7E              LD      A,(HL)              ; the record's occupancy flag
34FC: A7              AND     A                   
34FD: CA 1E 35        JP      Z,loc_351e          ; empty -- its destination is left untouched
3500: 2C              INC     L                   
3501: 2C              INC     L                   
3502: 2C              INC     L                   ; step to the record's X field
3503: 7E              LD      A,(HL)              
3504: 12              LD      (DE),A              ; X into the sprite record's first byte
3505: 3E 04           LD      A,$04               ; plus four lands on the record's sprite tile code
3507: 85              ADD     A,L                 
3508: 6F              LD      L,A                 
3509: 1C              INC     E                   
350A: 7E              LD      A,(HL)              
350B: 12              LD      (DE),A              ; the tile code
350C: 2C              INC     L                   
350D: 1C              INC     E                   
350E: 7E              LD      A,(HL)              
350F: 12              LD      (DE),A              ; the attribute byte
3510: 2D              DEC     L                   
3511: 2D              DEC     L                   
3512: 2D              DEC     L                   ; back to the record's Y field
3513: 1C              INC     E                   
3514: 7E              LD      A,(HL)              
3515: 12              LD      (DE),A              ; and the Y
3516: 13              INC     DE                  ; on to the next sprite record

loc_3517:
3517: 3E 1B           LD      A,$1B               ; and on from the Y field to the next fire record
3519: 85              ADD     A,L                 
351A: 6F              LD      L,A                 
351B: 10 DE           DJNZ    loc_34fb            ; 
351D: C9              RET                         

loc_351e:
351E: 3E 05           LD      A,$05               ; empty record: step the source past it
3520: 85              ADD     A,L                 
3521: 6F              LD      L,A                 
3522: 3E 04           LD      A,$04               ; and the destination on by one sprite record
3524: 83              ADD     A,E                 
3525: 5F              LD      E,A                 
3526: C3 17 35        JP      loc_3517            ; 

; ---- $3529-$3E6F: data ----
3529: 00 00 00 00 01 00 00 02 00 00 03 00 00 04 00 00
3539: 05 00 00 06 00 00 07 00 00 08 00 00 09 00 00 00
3549: 00 00 10 00 00 20 00 00 30 00 00 40 00 00 50 00
3559: 00 60 00 00 70 00 00 80 00 00 90 00 94 77 01 23
3569: 24 10 10 00 00 07 06 05 00 10 10 10 10 10 10 10
3579: 10 10 10 10 10 10 10 3F 00 50 76 00 F4 76 96 77
3589: 02 1E 14 10 10 00 00 06 01 00 00 10 10 10 10 10
3599: 10 10 10 10 10 10 10 10 10 3F 00 00 61 00 F6 76
35A9: 98 77 03 22 14 10 10 00 00 05 09 05 00 10 10 10
35B9: 10 10 10 10 10 10 10 10 10 10 10 3F 00 50 59 00
35C9: F8 76 9A 77 04 24 18 10 10 00 00 05 00 05 00 10
35D9: 10 10 10 10 10 10 10 10 10 10 10 10 10 3F 00 50
35E9: 50 00 FA 76 9C 77 05 24 18 10 10 00 00 04 03 00
35F9: 00 10 10 10 10 10 10 10 10 10 10 10 10 10 10 3F
3609: 00 00 43 00 FC 76 3B 5C 4B 5C 5B 5C 6B 5C 7B 5C
3619: 8B 5C 9B 5C AB 5C BB 5C CB 5C 3B 6C 4B 6C 5B 6C
3629: 6B 6C 7B 6C 8B 6C 9B 6C AB 6C BB 6C CB 6C 3B 7C
3639: 4B 7C 5B 7C 6B 7C 7B 7C 8B 7C 9B 7C AB 7C BB 7C
3649: CB 7C 8B 36 01 00 98 36 A5 36 B2 36 BF 36 06 00
3659: CC 36 08 00 E6 36 FD 36 0B 00 15 37 1C 37 30 37
3669: 38 37 47 37 5D 37 73 37 8B 37 00 61 22 61 44 61
3679: 66 61 88 61 9E 37 B6 37 D2 37 E1 37 1D 00 00 3F
3689: 09 3F 96 76 17 11 1D 15 10 10 1F 26 15 22 3F 94
3699: 76 20 1C 11 29 15 22 10 30 32 31 3F 94 76 20 1C
36A9: 11 29 15 22 10 30 33 31 3F 80 76 18 19 17 18 10
36B9: 23 13 1F 22 15 3F 9F 75 13 22 15 14 19 24 10 10
36C9: 10 10 3F 5E 77 18 1F 27 10 18 19 17 18 10 13 11
36D9: 1E 10 29 1F 25 10 17 15 24 10 FB 10 3F 29 77 1F
36E9: 1E 1C 29 10 01 10 20 1C 11 29 15 22 10 12 25 24
36F9: 24 1F 1E 3F 29 77 01 10 1F 22 10 02 10 20 1C 11
3709: 29 15 22 23 10 12 25 24 24 1F 1E 3F 27 76 20 25
3719: 23 18 3F 06 77 1E 11 1D 15 10 22 15 17 19 23 24
3729: 22 11 24 19 1F 1E 3F 88 76 1E 11 1D 15 2E 3F E9
3739: 75 2D 2D 2D 10 10 10 10 10 10 10 10 10 3F 0B 77
3749: 11 10 12 10 13 10 14 10 15 10 16 10 17 10 18 10
3759: 19 10 1A 3F 0D 77 1B 10 1C 10 1D 10 1E 10 1F 10
3769: 20 10 21 10 22 10 23 10 24 3F 0F 77 25 10 26 10
3779: 27 10 28 10 29 10 2A 10 2B 10 2C 44 45 46 47 48
3789: 10 3F F2 76 22 15 17 19 10 24 19 1D 15 10 10 30
3799: 03 00 31 10 3F 92 77 22 11 1E 1B 10 10 23 13 1F
37A9: 22 15 10 10 1E 11 1D 15 10 10 10 10 3F 72 77 29
37B9: 1F 25 22 10 1E 11 1D 15 10 27 11 23 10 22 15 17
37C9: 19 23 24 15 22 15 14 42 3F A7 76 19 1E 23 15 22
37D9: 24 10 13 1F 19 1E 10 3F 0A 77 10 10 20 1C 11 29
37E9: 15 22 10 10 10 10 13 1F 19 1E 3F FC 76 49 4A 10
37F9: 1E 19 1E 24 15 1E 14 1F 10 10 10 10 3F 7C 75 01
3809: 09 08 01 3F 02 97 38 68 38 02 DF 54 10 54 02 EF
3819: 6D 20 6D 02 DF 8E 10 8E 02 EF AF 20 AF 02 DF D0
3829: 10 D0 02 EF F1 10 F1 00 53 18 53 54 00 63 18 63
3839: 54 00 93 38 93 54 00 83 54 83 F1 00 93 54 93 F1
3849: AA 8D 7D 8C 6F 00 7C 6E 00 7C 6D 00 7C 6C 00 7C
3859: 8F 7F 8E 47 27 08 50 2F A7 08 50 3B 25 08 50 00
3869: 70 08 48 3B 23 07 40 46 A9 08 44 00 70 08 48 30
3879: 29 08 44 00 70 08 48 00 70 0A 48 6F 10 09 23 6F
3889: 11 0A 33 50 34 08 3C 00 35 08 3C 53 32 08 40 63
3899: 33 08 40 00 70 08 48 53 36 08 50 63 37 08 50 6B
38A9: 31 08 41 00 70 08 48 6A 14 0A 48 FD FD FD FD FD
38B9: FD FD FE FE FE FE FE FE FF FF FF FF 00 00 01 01
38C9: 01 7F FF FF FF FF FF 00 FF 00 00 01 00 01 01 01
38D9: 01 01 7F 04 7F F0 10 F0 02 DF F2 70 F8 02 6F F8
38E9: 10 F8 AA 04 DF D0 90 D0 02 DF DC 20 D1 AA FF FF
38F9: FF FF FF 04 DF A8 20 A8 04 5F B0 20 B0 02 DF B0
3909: 20 BB AA 04 DF 88 30 88 04 DF 90 B0 90 02 DF 9A
3919: 20 8F AA 04 BF 68 20 68 04 3F 70 20 70 02 DF 6E
3929: 20 79 AA 02 DF 58 A0 55 AA 00 70 08 44 2B AC 08
3939: 4C 3B AE 08 4C 3B AF 08 3C 4B B0 07 3C 4B AD 08
3949: 4C 00 70 08 44 00 70 08 44 00 70 08 44 00 70 0A
3959: 44 47 27 08 4C 2F A7 08 4C 3B 25 08 4C 00 70 08
3969: 44 3B 23 07 3C 4B 2A 08 3C 4B 2B 08 4C 2B AA 08
3979: 3C 2B AB 08 4C 00 70 0A 44 00 70 08 44 4B 2C 08
3989: 4C 3B 2E 08 4C 3B 2F 08 3C 2B 30 07 3C 2B 2D 08
3999: 4C 00 70 08 44 00 70 08 44 00 70 08 44 00 70 0A
39A9: 44 FD FD FD FE FE FE FE FF FF 00 FF 00 00 01 00
39B9: 01 01 02 02 02 02 03 03 03 7F 1E 4E BB 4C D8 4E
39C9: 59 4E 7F BB 4D 7F 47 27 08 50 2D 26 08 50 3B 25
39D9: 08 50 00 70 08 48 3B 24 07 40 4B 28 08 40 00 70
39E9: 08 48 30 29 08 44 00 70 08 48 00 70 0A 48 49 A6
39F9: 08 50 2F A7 08 50 3B 25 08 50 00 70 08 48 3B 24
3A09: 07 40 46 A9 08 44 00 70 08 48 2B A8 08 40 00 70
3A19: 08 48 00 70 0A 48 73 A7 88 60 8B 27 88 60 7F 25
3A29: 88 60 00 70 88 68 7F 24 87 70 74 29 88 6C 00 70
3A39: 88 68 8A A9 88 6C 00 70 88 68 00 70 8A 68 05 AF
3A49: F0 50 F0 AA 05 AF E8 50 E8 AA 05 AF E0 50 E0 AA
3A59: 05 AF D8 50 D8 AA 05 B7 58 48 58 AA 01 04 01 03
3A69: 04 01 02 03 04 01 02 01 03 04 01 02 01 03 01 04
3A79: 7F FF 00 FF FF FE FE FE FE FE FE FE FE FE FE FE
3A89: FF FF 00 E8 E5 E3 E2 E1 E0 DF DE DD DD DC DC DC
3A99: DC DC DC DD DD DE DF E0 E1 E2 E3 E4 E5 E7 E9 EB
3AA9: ED F0 AA 80 7B 78 76 74 73 72 71 70 70 6F 6F 6F
3AB9: 70 70 71 72 73 74 75 76 77 78 AA EE F0 DB A0 E6
3AC9: C8 D6 78 EB F0 DB A0 E6 C8 E6 C8 1B C8 23 A0 2B
3AD9: 78 12 F0 1B C8 23 A0 12 F0 1B C8 02 97 38 68 38
3AE9: 02 9F 54 10 54 02 DF 58 A0 55 02 EF 6D 20 79 02
3AF9: DF 9A 10 8E 02 EF AF 20 BB 02 DF DC 10 D0 02 FF
3B09: F0 80 F7 02 7F F8 00 F8 00 CB 57 CB 6F 00 CB 99
3B19: CB B1 00 CB DB CB F3 00 63 18 63 54 01 63 D5 63
3B29: F8 00 33 78 33 90 00 33 BA 33 D2 00 53 18 53 54
3B39: 01 53 92 53 B8 00 5B 76 5B 92 00 73 B6 73 D6 00
3B49: 83 95 83 B5 00 93 38 93 54 01 BB 70 BB 98 01 6B
3B59: 54 6B 75 AA 06 8F 90 70 90 06 8F 98 70 98 06 8F
3B69: A0 70 A0 00 63 18 63 58 00 63 80 63 A8 00 63 D0
3B79: 63 F8 00 53 18 53 58 00 53 A8 53 D0 00 9B 80 9B
3B89: A8 00 9B D0 9B F8 01 23 58 23 80 01 DB 58 DB 80
3B99: 00 2B 80 2B A8 00 D3 80 D3 A8 00 A3 A8 A3 D0 00
3BA9: 2B D0 2B F8 00 D3 D0 D3 F8 00 93 38 93 58 02 97
3BB9: 38 68 38 03 EF 58 10 58 03 F7 80 88 80 03 77 80
3BC9: 08 80 02 A7 A8 50 A8 02 E7 A8 B8 A8 02 3F A8 18
3BD9: A8 03 EF D0 10 D0 02 EF F8 10 F8 AA 00 63 18 63
3BE9: 58 00 63 88 63 D0 00 53 18 53 58 00 53 88 53 D0
3BF9: 00 E3 68 E3 90 00 E3 B8 E3 D0 00 CB 90 CB B0 00
3C09: B3 58 B3 78 00 9B 80 9B A0 00 93 38 93 58 00 23
3C19: 88 23 C0 00 1B C0 1B E8 02 97 38 68 38 02 B7 58
3C29: 10 58 02 EF 68 E0 68 02 D7 70 C8 70 02 BF 78 B0
3C39: 78 02 A7 80 90 80 02 67 88 48 88 02 27 88 10 88
3C49: 02 EF 90 C8 90 02 A7 A0 98 A0 02 BF A8 B0 A8 02
3C59: D7 B0 C8 B0 02 EF B8 E0 B8 02 27 C0 10 C0 02 EF
3C69: D0 D8 D0 02 67 D0 50 D0 02 CF D8 C0 D8 02 B7 E0
3C79: A8 E0 02 9F E8 88 E8 02 27 E8 10 E8 02 EF F8 10
3C89: F8 AA 00 7B 80 7B A8 00 7B D0 7B F8 00 33 58 33
3C99: 80 00 53 58 53 80 00 AB 58 AB 80 00 CB 58 CB 80
3CA9: 00 2B 80 2B A8 00 D3 80 D3 A8 00 23 A8 23 D0 00
3CB9: 5B A8 5B D0 00 A3 A8 A3 D0 00 DB A8 DB D0 00 1B
3CC9: D0 1B F8 00 E3 D0 E3 F8 05 B7 30 48 30 05 CF 58
3CD9: 30 58 05 D7 80 28 80 05 DF A8 20 A8 05 E7 D0 18
3CE9: D0 05 EF F8 10 F8 AA 10 82 85 8B 10 85 80 8B 10
3CF9: 87 85 8B 81 80 80 8B 81 82 85 8B 81 85 80 8B 05
3D09: 88 77 01 68 77 01 6C 77 03 49 77 05 08 77 01 E8
3D19: 76 01 EC 76 05 C8 76 05 88 76 02 69 76 02 4A 76
3D29: 05 28 76 05 E8 75 01 CA 75 03 A9 75 01 88 75 01
3D39: 8C 75 05 48 75 01 28 75 01 2A 75 01 2C 75 01 08
3D49: 75 01 0A 75 01 0C 75 03 C8 74 03 AA 74 03 88 74
3D59: 05 2F 77 05 0F 77 02 F0 76 02 CF 76 02 D2 76 05
3D69: 8F 76 05 6F 76 01 4F 76 01 53 76 05 2F 76 05 EF
3D79: 75 02 D0 75 02 B1 75 05 8F 75 03 50 75 05 2F 75
3D89: 01 0F 75 01 13 75 01 EF 74 01 F1 74 01 F3 74 02
3D99: D1 74 00 00 00 23 68 01 11 00 00 00 10 DB 68 01
3DA9: 40 00 00 08 01 01 01 01 01 01 01 01 01 00 00 00
3DB9: 00 00 00 80 01 C0 FF 01 FF FF 34 C3 39 00 67 80
3DC9: 69 1A 01 00 00 00 00 00 00 00 00 04 00 10 00 00
3DD9: 00 00 00 1E 18 0B 4B 14 18 0B 4B 1E 18 0B 3B 14
3DE9: 18 0B 3B 3D 01 03 02 4D 01 04 01 27 70 01 E0 00
3DF9: 00 7F 40 01 78 02 00 27 49 0C F0 7F 49 0C 88 1E
3E09: 07 03 09 24 64 BB C0 23 8D 7B B4 1B 8C 7C 64 4B
3E19: 0E 04 02 23 46 03 68 DB 46 03 68 17 50 00 5C E7
3E29: D0 00 5C 8C 50 00 84 73 D0 00 84 17 50 00 D4 E7
3E39: D0 00 D4 53 73 0A A0 8B 74 0A F0 DB 75 0A A0 5B
3E49: 73 0A C8 E3 74 0A 60 1B 75 0A 80 DB 73 0A C8 93
3E59: 74 0A F0 33 75 0A 50 44 03 08 04 37 F4 37 C0 37
3E69: 8C 77 70 77 A4 77 D8

; Pick one of three score-popup awards from the low bits of the accumulator -- the "how
; many did he jump over at once" tier. It is reached with the overlap thermometer already
; shifted right once, and is a small first-clear-bit-wins encoder: bit 0 clear takes task
; argument 1 and sprite code 0x7B; else bit 1 clear takes argument 3 and code 0x7D; else
; argument 5 and code 0x7F. Against the score table those three arguments are 100, 300
; and 500 points. It then tail-jumps into the shared stamp, which enqueues the task,
; stamps a 4-byte sprite record anchored on Mario's position, and cues a board-gated
; sound.
pickAwardTierByObjectCount:
3E70: 11 01 00        LD      DE,$0001            ; the 100-point award: task argument 1
3E73: 06 7B           LD      B,$7B               ; and the glyph the popup shows
3E75: 1F              RRA                         ; shift the next bit of the overlap tally out
3E76: D2 28 1E        JP      NC,awardScorePopup  ; bit clear -- a single overlap, award 100
3E79: 1E 03           LD      E,$03               ; argument 3 -- the 300-point award
3E7B: 06 7D           LD      B,$7D               ; its glyph
3E7D: 1F              RRA                         
3E7E: D2 28 1E        JP      NC,awardScorePopup  ; two at once -- award 300
3E81: 1E 05           LD      E,$05               ; argument 5 -- the 500-point award
3E83: 06 7F           LD      B,$7F               ; its glyph
3E85: C3 28 1E        JP      awardScorePopup     ; three or more at once -- award 500

; Vector to the current board's collision-search arm, handing it the caller's bounds
; word across the dispatch. It reads board and vectors through a six-entry inline table:
; entries 0 and 5 are reset-vector guards for an out-of-range board; 2, 3 and 4 are
; those boards' collision arms; and 1, the 25m arm, COUNTS object overlaps rather than
; running the plain girder collision, which is what makes this dispatch worth having as
; its own routine. THE BOUNDS WORD IS HANDED OVER THROUGH THE STACK, not a register: the
; shared dispatch trampoline clobbers the register pair while it recovers its own table
; base, so the word is stacked first and the arm's opening move lifts it back off as its
; collision bounds. That hand-off is genuine data, not call plumbing -- dropping it
; feeds the arm a garbage bounds word.
dispatchBoardOverlapSearch:
3E88: 3A 27 62        LD      A,(workRam+227)     ; the board picks the collision arm
3E8B: E5              PUSH    HL                  ; stack the caller's bounds word -- the arm lifts it back
3E8C: EF              RST     $28                 ; vector through the table that follows

; ---- $3E8D-$3E98: jump table ----
3E8D: 00 00 99 3E B0 28 E0 28 01 29 00 00

loc_3e99:
3E99: E1              POP     HL                  ; lift the bounds word: the search window
3E9A: AF              XOR     A                   
3E9B: 32 60 60        LD      (workRam+60),A      ; both scans share one tally
3E9E: 06 0A           LD      B,$0A               ; ten barrel records
3EA0: 11 20 00        LD      DE,$0020            ; the stride between them
3EA3: DD 21 00 67     LD      IX,$6700            ; the barrel array
3EA7: CD C3 3E        CALL    countObjectOverlaps ; count the ones crowding the probe

loc_3eaa:
3EAA: 06 05           LD      B,$05               ; then the five fire records
3EAC: DD 21 00 64     LD      IX,$6400            
3EB0: CD C3 3E        CALL    countObjectOverlaps ; add them to the same tally
3EB3: 3A 60 60        LD      A,(workRam+60)      ; the total across both arrays
3EB6: A7              AND     A                   
3EB7: C8              RET     Z                   ; nothing near him -- code 0
3EB8: FE 01           CP      $01                 
3EBA: C8              RET     Z                   ; exactly one -- code 1
3EBB: FE 03           CP      $03                 ; is the total under three?
3EBD: 3E 03           LD      A,$03               ; two -- code 3, two bits of the thermometer
3EBF: D8              RET     C                   
3EC0: 3E 07           LD      A,$07               ; three or more -- code 7, the top of the scale
3EC2: C9              RET                         

; Count how many objects in an array overlap a probe point, within a per-object
; rectangular window. Walks a run of fixed-stride records, skipping any whose field +0
; has bit 0 clear. For an active record:
;   first axis:  |probeA - record+5| + 1 must be under the first threshold, or past that
;                still under the record's own window at +0x0a;
;   second axis: tried only if the first overlapped -- |probe+3 - record+3| must be
;                under the second threshold, or past it under the record's window at +9.
; Both axes overlapping bumps overlapCount. The caller clears that counter first and
; reads it back afterwards as an overlap-severity code, so the counter is this routine's
; only observable effect. Each distance is the subtract-then-negate-on-borrow idiom,
; byte-wide, so the unsigned distance falls out without a signed compare. A count of 0
; scans 256 records.
countObjectOverlaps:
3EC3: DD CB 00 46     BIT     0,(IX+$00)          ; the record's active flag
3EC7: CA FA 3E        JP      Z,loc_3efa          ; inactive -- nothing to test
3ECA: 79              LD      A,C                 ; the probe's first-axis coordinate
3ECB: DD 96 05        SUB     (IX+$05)            ; distance from the record's own position
3ECE: D2 D3 3E        JP      NC,loc_3ed3         ; 
3ED1: ED 44           NEG                         ; negate on borrow -- the unsigned distance

loc_3ed3:
3ED3: 3C              INC     A                   ; the test is written on distance plus one
3ED4: 95              SUB     L                   ; against the caller's window on this axis
3ED5: DA DE 3E        JP      C,loc_3ede          ; inside it -- try the second axis
3ED8: DD 96 0A        SUB     (IX+$0A)            ; past it: try the record's own extent
3EDB: D2 FA 3E        JP      NC,loc_3efa         ; outside that too -- this record misses

loc_3ede:
3EDE: FD 7E 03        LD      A,(IY+$03)          ; the probe's second-axis coordinate
3EE1: DD 96 03        SUB     (IX+$03)            ; distance from the record's
3EE4: D2 E9 3E        JP      NC,loc_3ee9         ; 
3EE7: ED 44           NEG                         ; unsigned distance again

loc_3ee9:
3EE9: 94              SUB     H                   ; against the caller's window on this axis
3EEA: DA F3 3E        JP      C,loc_3ef3          ; inside it -- both axes overlap
3EED: DD 96 09        SUB     (IX+$09)            ; past it: the record's own extent
3EF0: D2 FA 3E        JP      NC,loc_3efa         ; outside -- no overlap

loc_3ef3:
3EF3: 3A 60 60        LD      A,(workRam+60)      ; 
3EF6: 3C              INC     A                   
3EF7: 32 60 60        LD      (workRam+60),A      ; both axes overlapped -- tally this object

loc_3efa:
3EFA: DD 19           ADD     IX,DE               ; on to the next record
3EFC: 10 C5           DJNZ    countObjectOverlaps ; 
3EFE: C9              RET                         

; ---- $3EFF-$3F23: data ----
3EFF: 00 5C 76 49 4A 01 09 08 01 3F 7D 77 1E 19 1E 24
3F0F: 15 1E 14 1F 10 1F 16 10 11 1D 15 22 19 13 11 10
3F1F: 19 1E 13 2B 3F

; Paint a fixed two-tile decoration into the tilemap: tile code 0x9F into one video-RAM
; cell and 0x9E into the cell 0x20 lower in address, unconditionally. The two are one
; screen row apart and the lower-addressed one sits BELOW the other, so the pair is a
; two-tall glyph stamped at a fixed spot. No inputs at all -- it reads no register and no
; memory. A leaf.
stampFixedTilePair:
3F24: 21 AF 74        LD      HL,$74AF            ; the upper cell of the two-tall glyph
3F27: 11 E0 FF        LD      DE,$FFE0            ; minus one screen row
3F2A: 36 9F           LD      (HL),$9F            ; paint the top tile
3F2C: 19              ADD     HL,DE               ; step to the cell below it on screen
3F2D: 36 9E           LD      (HL),$9E            ; and the bottom tile
3F2F: C9              RET                         

; ---- $3F30-$3F9F: data ----
3F30: 50 52 4F 47 52 41 4D 2C 57 45 20 57 4F 55 4C 44
3F40: 20 54 45 41 43 48 20 59 4F 55 2E 2A 2A 2A 2A 2A
3F50: 54 45 4C 2E 54 4F 4B 59 4F 2D 4A 41 50 41 4E 20
3F60: 30 34 34 28 32 34 34 29 32 31 35 31 20 20 20 20
3F70: 45 58 54 45 4E 54 49 4F 4E 20 33 30 34 20 20 20
3F80: 53 59 53 54 45 4D 20 44 45 53 49 47 4E 20 20 20
3F90: 49 4B 45 47 41 4D 49 20 43 4F 2E 20 4C 49 4D 2E

loc_3fa0:
3FA0: CD A6 3F        CALL    stamp50mBoardTiles  ; 50m only; nothing stamped elsewhere
3FA3: C3 5F 0D        JP      loc_0d5f            ; then the common board-setup continuation

; During board setup, stamp four tilemap cells -- but only on the 50m conveyor board. It
; is reached unconditionally from every board's setup pass, and its first act is the
; per-board gate with the mask 0x02, which opens only when the current board is 2; off
; 50m it writes nothing at all. On 50m it stamps a fixed two-tile motif into two
; video-RAM cell pairs, tile 0x10 then tile 0xC0 in each pair. The two cells of a pair
; are two bytes apart because tilemap columns are stride 2 in this address layout. Every
; stored value is a constant.
stamp50mBoardTiles:
3FA6: 3E 02           LD      A,$02               ; mask bit 1 -- the 50m conveyor board
3FA8: F7              RST     $30                 ; off 50m the gate closes and nothing is stamped
3FA9: 06 02           LD      B,$02               ; two cell pairs to stamp
3FAB: 21 6C 77        LD      HL,$776C            ; the first pair

loc_3fae:
3FAE: 36 10           LD      (HL),$10            ; the first tile of the motif
3FB0: 23              INC     HL                  
3FB1: 23              INC     HL                  ; tilemap columns are two bytes apart here
3FB2: 36 C0           LD      (HL),$C0            ; and the second tile
3FB4: 21 8C 74        LD      HL,$748C            ; the second pair, loaded inside the loop
3FB7: 10 F5           DJNZ    loc_3fae            ; 
3FB9: C9              RET                         

; ---- $3FBA-$3FBF: data ----
3FBA: 00 00 00 00 00 00

; Force the CODE byte of marioSpriteRecord to a fixed value of 3, and hand back a
; pointer to that record's Y field. Mario's sprite code packs a horizontal-mirror flag
; in the top bit and an animation code in the low bits, so a bare 3 is one of the
; climb-frame codes with the mirror flag cleared: it pins one specific climb pose
; whatever the byte held before. The attribute byte in between is deliberately stepped
; over and never written, and the returned pointer is consumed on the very next step. It
; claims no direction -- nothing here moves Mario, and whether the climb is up or down
; was decided before the call.
pinMarioClimbPose:
3FC0: 21 4D 69        LD      HL,$694D            ; the code byte of Mario's sprite record
3FC3: 36 03           LD      (HL),$03            ; pin one climb pose, mirror flag clear
3FC5: 2C              INC     L                   ; step over the attribute byte, left untouched
3FC6: 2C              INC     L                   ; leaving a pointer to the record's Y field
3FC7: C9              RET                         

; ---- $3FC8-$3FFF: data ----
3FC8: 00 00 41 7F 7F 41 00 00 00 7F 7F 18 3C 76 63 41
3FD8: 00 00 7F 7F 49 49 49 41 00 1C 3E 63 41 49 79 79
3FE8: 00 7C 7E 13 11 13 7E 7C 00 7F 7F 0E 1C 0E 7F 7F
3FF8: 00 00 41 7F 7F 41 00 00