; Codename: Reflex ; by Lee W. Fastenau ; 03/26/2004 ; Assemble with DASM using: ; dasm.exe reflex.asm -f3 -oreflex.bin ; (Ensure vcs.h and macro.h are in the same folder or specify the include path using the -I option) ; TO-DO ; -- HIGH PRIORITY -- ; OK Fix var allocation bug ; OK Driving control support ; W Add sound ; ? Death sound ; ? Game over sound ; OK Add ball velocity table ; OK Add ball velocity multiplier ; OK Two-player controller support ; OK Two-player paddle update ; OK Two-player paddle tracking ; OK Two-player score kernel ; OK Two-player game kernel ; - Switch ball start position in two-player game ; OK Better paddle reflection ; OK Paddle "english" ; OK Optimize paddle "english" code ; W Board clear (win) state ; OK Improve ball out detection ; OK Add "lives" counter ; OK Add PAL50/60 support ; OK "Bootup" video config (NTSC/PAL50/PAL60) ; ? Fire button starts game ; OK Build online level editor ; OK Add level decompressor ; OK Level selection on hitting Game Reset repeatedly ; OK Implement difficulty switches ; OK Add unique board designs ; W Intro music ; W Gameplay features/balancing ; W Ball speed increase/decrease ; OK Score tuning ; W Extra lives ; W Optimize program structure ; OK Move game kernel to beginning of ROM ; OK Remove all ALIGNs ; OK Change game kernel to use skipdraw ; - Make single-use JSR's inline ; - Miscellaneous optimizations ; -- LOW PRIORITY -- ; ? Improve state handling ; ? Add "New game" state ; ? Improve pregame state ; ? Improve ball out state ; ? Re-code ball effects ; ? Fun scrolly/static bottom message (As ROM permits) ; OK Improve ALT_CHAR_SET data ; OK Char set design ; OK Better SECAM support (When Assembly Option set) ; - Test PAL support ; - Test SECAM support ; -- FOR OKGE04 PREVIEW -- ; OK One-player paddle ; OK Two-player paddles ; OK Board clear detect and reset board ; OK Initial ball speed/direction ; OK Ball speed increase during game ; OK Two-player scoring ; OK Two-player win detect ; CLEANUP REMINDERS ; * Bring PF priority to front ; * Check default modes ; * Check debug constants ; * Check state delay values ; * Check for debug "ALIGN 256" or "ds.b" (in RAM is okay) processor 6502 include "vcs.h" include "macro.h" ; ------------------------------------ ; Global Macros ; ------------------------------------ MAC WARN_BOUNDARY .start SET {1} .end SET * IF >.start != >.end echo "Page boundary crossed (",{2},.start,"-",.end,")" ENDIF ENDM MAC REQUIRED_BOUNDARY .start SET {1} .end SET * IF >.start == >.end echo "Expected page boundary not crossed (",{2},.start,"-",.end,")" ENDIF ENDM ; ------------------------------------ ; Assembly Options ; ------------------------------------ SECAM_PALETTE equ 0 ; 0 = PAL palette, 1 = SECAM palette DEFAULT_GAME_MODE equ 0 ; See gameModes tab (currently modes 0-5 available) CHAR_SET equ 0 ; 0 = default, 1 = original, 2 = arcadish VU_METERS equ 1 ; 0 = No VU meters, 1 = VU meters on title screen (38 bytes) TITLE_SONG equ 0 ; 0 = Devil's Workshop (55 bytes) ; 1 = Lost in Legoland (114 bytes) ; ------------------------------------ ; DEBUG_CONSTANTS ; ------------------------------------ NO_MUSIC equ 0 ; Disable music (used for remaining sane during development) TEST_PADDLE equ 0 ; Temporary test paddle JOYSTICK_BALL equ 0 ; Control ball with joystick STATIC_BALL equ 0 ; Ball doesn't move NO_COLLISION equ 0 ; Turn collision/reflection off (always off when JOYSTICK_BALL is 1) NO_REMOVE equ 0 ; Turn brick removal off ;NO_PADDLE equ 0 ; Solid border, no paddle (retired) FAST_BALL equ 0 ; Make ball move really fast DEBUG_COLLISION equ 0 ; Use leftmost digit of score to show last collision INFINITE_LIVES equ 0 ; Hmm... what's this do? ; ------------------------------------ ; Constant declarations ; ------------------------------------ white equ $0E ; The NTSC color palette green equ $CC yellow equ $2E orange equ $2A darkgray equ $02 black equ $00 lightblue equ $8A lightred equ $3A scoreColor equ yellow scoreBgColor equ black bgColor equ darkgray paddleColor equ yellow livesColor equ orange ballHighlight equ white ballColor equ green ballFlashColor equ white paddle0Color equ lightblue paddle1Color equ lightred IF !SECAM_PALETTE PAL_white equ $0E ; Und die PAL Farbe Palette PAL_green equ $58 PAL_yellow equ $2E PAL_orange equ $2A PAL_darkgray equ $02 PAL_black equ $00 PAL_lightblue equ $BA PAL_lightred equ $6A PAL_scoreColor equ PAL_yellow PAL_scoreBgColor equ PAL_black PAL_bgColor equ PAL_darkgray PAL_paddleColor equ PAL_yellow PAL_livesColor equ PAL_orange PAL_ballHighlight equ PAL_white PAL_ballColor equ PAL_green PAL_ballFlashColor equ PAL_white PAL_paddle0Color equ PAL_lightblue PAL_paddle1Color equ PAL_lightred ELSE PAL_white equ $0E ; La palette de couleur de SECAM PAL_green equ $08 PAL_yellow equ $0C PAL_orange equ $0C PAL_darkgray equ $00 PAL_black equ $00 PAL_lightblue equ $02 PAL_lightred equ $04 PAL_scoreColor equ PAL_yellow PAL_scoreBgColor equ PAL_black PAL_bgColor equ PAL_darkgray PAL_paddleColor equ PAL_white PAL_livesColor equ PAL_white PAL_ballHighlight equ PAL_white PAL_ballColor equ PAL_green PAL_ballFlashColor equ PAL_white PAL_paddle0Color equ PAL_lightblue PAL_paddle1Color equ PAL_lightred ENDIF brickScore equ $0020 ; Score is BCD, so use hex... (weird, huh?) deathDelay equ 50 pregameDelay equ 255 clearDelay equ 100 playfieldWidth equ 32 playfieldHeight equ 16 paddleBound equ 46 ;paddle1Width equ 16 ;paddle2Width equ 6 rowHeight equ 8 flashDuration equ 5 ballStartX equ 83 ballStartY equ 22 startingLives equ 3 state_options equ 0 state_newgame equ 1 state_newlevel equ 2 state_pregame equ 3 state_ingame equ 4 state_ballout equ 5 state_gameover equ 6 state_clear equ 7 sfxBrickFreq equ 4 sfxPaddleFreq equ 9 sfxEchoDelay equ 4 IF !NO_MUSIC musicVolume equ 15 ELSE musicVolume equ 0 ENDIF p1score equ score p1scoreDigit equ digit5 p2score equ score+1 p2scoreDigit equ digit3 ball_minSpeed equ 4 ; Base multiplier for speed ball_maxSpeed equ 9 ; Max multiplier for speed ball_easyDelay equ 22 ; Initial number of paddle/wall hits before speedup on easy level ball_hardDelay equ 15 ; Initial number of paddle/wall hits before speedup on difficult level ball_minDelay equ 6 ; Minimum delay reachable -1 mode_preset equ %00011111 ; Inverted mode_flags equ %11101100 ; Inverted mode_switched equ %00000100 ; 1=switched last frame, 0=not switched mode_twoplayers equ %00000001 ; 1=two players, 0=one player mode_twoscores equ %00000010 ; 1=competition, 0=cooperative/one player mode_driving equ %00010000 ; 1=driving controller, 0=joystick (ALWAYS d4) mode_oneplayer equ 0 ; This is here to make the gameModes tab more readable mode_onescore equ 0 ; This is here to make the gameModes tab more readable mode_joystick equ 0 ; This is here to make the gameModes tab more readable flag_lives equ %00000111 ; Life counter flag_lastcbsw equ %00001000 ; Last color/bw switch state flag_palColors equ %10000000 ; PAL color palette flag flag_pal50 equ %01000000 ; PAL 50Hz flag (else 60Hz) seg.u vars variables org $80 ; ------------------------------------ ; Variable declarations ; ------------------------------------ blockPtr dc.w ; Pointer for removing blocks blockData block1l ds.b playfieldHeight ; Columns 0-7 block2l ds.b playfieldHeight ; Columns 8-15 block2r ds.b playfieldHeight ; Columns 16-23 block1r ds.b playfieldHeight ; Columns 24-31 temp dc.b ; Temp storage gameMode dc.b ; See mode bits above gameState dc.b ; See state values above gameFlags dc.b ; Other game flags stateTimer dc.b ; General timer levelByte dc.b ; Compressed level data byte index levelBit dc.b ; Compressed level data bit index ballx dc.w ; Ball x pos bally dc.w ; Ball y pos ballmx dc.w ; Ball x move ballmy dc.w ; Ball y move ballDirection dc.b ; Ball direction ballSpeed dc.b ; Ball speed ballAccelDelay dc.b ; Number of hits before ball speedup ballCounter dc.b ; Tracks number of hits before next speedup ballFlashTimer dc.b ; Counter for ball flash effect lineCount ; Shares space with blockX blockX dc.b ; Ball block x location blockY dc.b ; Ball block y location offsetX dc.b ; Collide x offset offsetY dc.b ; Collide y offset tryX dc.b ; Ball collision PF bit pattern tryY dc.b ; Y offset for ball collision tryX_store dc.b oldSWCHA dc.b ; Controller nibbles (Player 1 & 2) paddle0 dc.b ; Paddle movement (Player 1) paddle1 dc.b ; Paddle movement (Player 2) paddlePos dc.w ; Paddle position (player 1 & 2) paddleSize dc.w ; Paddle size -4 (player 1 & 2) blockSection dc.b ; Section 0-3 pcol dc.b ; Paddle color for 1-player mode / wall color p0col dc.b ; Paddle colors for 2-player mode p1col dc.b digit0 dc.w ; Score digit pointers digit1 dc.w digit2 dc.w digit3 dc.w digit4 dc.w digit5 dc.w score ds.b 3 ; BCD score (1 nibble per digit) blockCount dc.b ; Number of bricks remaining sfxTimer0 dc.b sfxEcho0 dc.b sfxVolume0 dc.b noteTimer equ score noteCounter equ score+2 noteVolume equ sfxEcho0 stack ds.w 2 ; Reserved for 2 levels of JSR's echo "------------------------------------" echo "RAM Bytes Used:",[*-variables]d echo "RAM Bytes Free:",[$100-*]d seg prog program org $F000 ; ; gameArea (130 scanlines) ; gameArea subroutine lda #rowHeight-1 ;+2 sta lineCount ;+3 lda #>rowColor ;+2 sta tryY ;+3 ,setColors bit gameFlags bmi .palColors lda # Consume 5 cycles by guaranteeing we cross a page boundary sta HMP0,X ; 17 Store the fine adjustment value. sta RESP0,X ; 21/ 26/31/36/41/46/51/56/61/66/71 - Set the rough position. sta WSYNC sta HMOVE rts ; ------------------------------------ ; Data ; ------------------------------------ ballShape dc.b %00001000 ; Ball dc.b %00010100 dc.b %00011000 dc.b %00011100 dc.b %00011100 dc.b %00001000 ballHeight equ *-ballShape rowColorData rowColor dc.b paddleColor ; Paddle and brick color (NTSC) dc.b paddleColor dc.b paddleColor dc.b paddleColor dc.b lightred+4 dc.b lightred+2 dc.b lightred dc.b lightred-2 dc.b lightblue+4 dc.b lightblue+2 dc.b lightblue dc.b lightblue-2 dc.b paddleColor dc.b paddleColor dc.b paddleColor dc.b paddleColor IF !SECAM_PALETTE PAL_rowColor dc.b PAL_paddleColor ; Paddle and brick color (PAL) dc.b PAL_paddleColor dc.b PAL_paddleColor dc.b PAL_paddleColor dc.b PAL_lightred+4 dc.b PAL_lightred+2 dc.b PAL_lightred dc.b PAL_lightred-2 dc.b PAL_lightblue+4 dc.b PAL_lightblue+2 dc.b PAL_lightblue dc.b PAL_lightblue-2 dc.b PAL_paddleColor dc.b PAL_paddleColor dc.b PAL_paddleColor dc.b PAL_paddleColor ELSE PAL_rowColor dc.b PAL_paddleColor ; Paddle and brick color (SECAM) dc.b PAL_paddleColor dc.b PAL_paddleColor dc.b PAL_paddleColor dc.b PAL_lightred dc.b PAL_lightred dc.b PAL_lightred dc.b PAL_lightred dc.b PAL_lightblue dc.b PAL_lightblue dc.b PAL_lightblue dc.b PAL_lightblue dc.b PAL_paddleColor dc.b PAL_paddleColor dc.b PAL_paddleColor dc.b PAL_paddleColor ENDIF WARN_BOUNDARY rowColorData,"rowColorData" titleData titlePF1l dc.b %10000101 dc.b %10000101 dc.b %10000101 dc.b %10000101 dc.b %10000110 dc.b %10000101 dc.b %10000101 dc.b %10000111 titlePF2l dc.b %00101110 dc.b %00100010 dc.b %00100010 dc.b %00100010 dc.b %01100110 dc.b %00100010 dc.b %00100010 dc.b %11101110 titlePF2r dc.b %01101110 dc.b %01001000 dc.b %01001000 dc.b %01001000 dc.b %01001100 dc.b %01001000 dc.b %01001000 dc.b %01001110 titlePF1r dc.b %10000101 dc.b %10000101 dc.b %10000101 dc.b %10000101 dc.b %10000010 dc.b %10000101 dc.b %10000101 dc.b %10000101 ;WARN_BOUNDARY titleData,"titleData" ; (Page crossing OK here) livesTab dc.b %00000000 ; Also used for VU meters dc.b %01000000 dc.b %01010000 dc.b %01010100 dc.b %01010101 ;WARN_BOUNDARY livesTab,"livesTab" ; (Page crossing OK here) singleBrickTab dc.b %10000000 ; Thanks Manuel! :) dc.b %01000000 dc.b %00100000 dc.b %00010000 dc.b %00001000 dc.b %00000100 dc.b %00000010 dc.b %00000001 ;WARN_BOUNDARY singleBrickTab,"singleBrickTab" ; (Page crossing OK here) doubleBrickTab dc.b %11000000 dc.b %11000000 dc.b %00110000 dc.b %00110000 dc.b %00001100 dc.b %00001100 dc.b %00000011 dc.b %00000011 ;WARN_BOUNDARY doubleBrickTab,"doubleBrickTab" ; (Page crossing OK here) dx1 equ 8 ; Ball velocity lookup dx2 equ 22 dx3 equ 33 dx4 equ 39 dy1 equ (dx4*2) dy2 equ (dx3*2) dy3 equ (dx2*2) dy4 equ (dx1*2) deltaXTab dc.b dx1,dx2,dx3,dx4 deltaYTab dc.b dy1,dy2,dy3,dy4 ;englishTab dc.b -2 ; "English" as in paddle english, ; dc.b -1 ; not language... okay? ; dc.b 0 ; dc.b 0 ; dc.b 0 ; dc.b 0 ; dc.b 1 ; dc.b 2 gameModes dc.b mode_joystick | mode_oneplayer | mode_onescore ; Game mode 0 dc.b mode_joystick | mode_twoplayers | mode_twoscores ; Game mode 1 dc.b mode_joystick | mode_twoplayers | mode_onescore ; Game mode 2 dc.b mode_driving | mode_oneplayer | mode_onescore ; Game mode 3 dc.b mode_driving | mode_twoplayers | mode_twoscores ; Game mode 4 dc.b mode_driving | mode_twoplayers | mode_onescore ; Game mode 5 ;WARN_BOUNDARY gameModes,"gameModes" ; (Page crossing OK here) gameModesAvailable dc.b *-gameModes spinRightTab dc.b $DD ; 1100 -> 1101 dc.b $FF ; 1101 -> 1111 dc.b $CC ; 1110 -> 1100 dc.b $EE ; 1111 -> 1110 ;WARN_BOUNDARY spinRightTab,"spinRightTab" spinLeftTab dc.b $EE ; 1100 -> 1110 dc.b $CC ; 1101 -> 1100 dc.b $FF ; 1110 -> 1111 dc.b $DD ; 1111 -> 1101 ;WARN_BOUNDARY spinLeftTab,"spinLeftTab" playerMask dc.b $F0 dc.b $0F ;WARN_BOUNDARY playerMask,"playerMask" IF CHAR_SET==0 digitData hex 7C FE C6 C6 C6 FE 7C 00 ;0 hex 18 18 18 18 18 18 38 38 ;1 hex FE FE C0 FC 7E 06 FE FC ;2 hex FC FE 06 FE FE 06 FE FC ;3 hex 06 06 06 7E FE C6 C6 C6 ;4 hex FC FE 06 FE FC C0 FE FE ;5 hex 7C FE C6 FE FC C0 FC 7C ;6 hex 06 06 06 06 06 06 FE FC ;7 hex 7C FE C6 FE FE C6 FE 7C ;8 hex 7C 7E 06 7E FE C6 FE 7C ;9 ; hex 00 FE 86 AA AA AA FE 00 ;WIN ; hex 00 FE C6 DE DE DE FE 00 ;LOSE WARN_BOUNDARY digitData,"digitData" ENDIF IF CHAR_SET==1 digitData hex 7C C6 C6 C6 C6 C6 C6 7C ;0 hex FE 18 18 18 18 78 38 18 ;1 hex FE C0 60 3C 06 06 C6 7C ;2 hex 7C C6 06 06 3C 06 C6 7C ;3 hex 0C 0C FE CC CC CC 6C 0C ;4 hex 7C C6 06 06 FC C0 C0 FE ;5 hex 7C C6 C6 C6 FC C0 60 38 ;6 hex 30 30 30 30 18 0C 06 FE ;7 hex 7C C6 C6 C6 7C C6 C6 7C ;8 hex 38 0C 06 7E C6 C6 C6 7C ;9 ; hex 00 FE 86 AA AA AA FE 00 ;WIN ; hex 00 FE C6 DE DE DE FE 00 ;LOSE WARN_BOUNDARY digitData,"digitData" ENDIF IF CHAR_SET==2 digitData hex 7C C6 C6 C6 C6 C6 66 3C ;0 hex FE 18 18 18 18 78 18 18 ;1 hex FE C0 60 38 0C 06 C6 7C ;2 hex 7C C6 C6 06 1C 06 C6 7C ;3 hex 0C 0C FE CC 6C 3C 1C 0C ;4 hex 7C C6 06 06 7C C0 C0 FE ;5 hex 7C C6 C6 C6 FC C0 60 3C ;6 hex 30 30 30 30 18 0C C6 FE ;7 hex 7C C6 C6 C6 7C C6 66 3C ;8 hex 78 0C 06 7E C6 C6 C6 7C ;9 ; hex 00 FE 86 AA AA AA FE 00 ;WIN ; hex 00 FE C6 DE DE DE FE 00 ;LOSE WARN_BOUNDARY digitData,"digitData" ENDIF fineAdjustBegin dc.b %01110000 ; Left 7 dc.b %01100000 ; Left 6 dc.b %01010000 ; Left 5 dc.b %01000000 ; Left 4 dc.b %00110000 ; Left 3 dc.b %00100000 ; Left 2 dc.b %00010000 ; Left 1 dc.b %00000000 ; No movement. dc.b %11110000 ; Right 1 dc.b %11100000 ; Right 2 dc.b %11010000 ; Right 3 dc.b %11000000 ; Right 4 dc.b %10110000 ; Right 5 dc.b %10100000 ; Right 6 dc.b %10010000 ; Right 7 fineAdjustTable EQU fineAdjustBegin - %11110001 ; NOTE: %11110001 = -15 WARN_BOUNDARY fineAdjustBegin,"fineAdjustBegin" optionData optionJS dc.b %01111110 dc.b %10100001 dc.b %11010001 dc.b %10100001 dc.b %11111111 dc.b %11100111 dc.b %11011011 dc.b %11111111 dc.b %01010000 dc.b %00011000 dc.b %00010000 dc.b %00011000 dc.b %00010000 dc.b %00011000 dc.b %00010000 dc.b %00011000 optionDC dc.b %00011000 dc.b %00111100 dc.b %01111110 dc.b %01011010 dc.b %11011011 dc.b %10011001 dc.b %10011001 dc.b %10111101 dc.b %11111111 dc.b %11000011 dc.b %10000001 dc.b %11000011 dc.b %01000010 dc.b %01100110 dc.b %00111100 dc.b %00011000 WARN_BOUNDARY optionData,"optionData" levelData hex 39 E7 D4 80 7C E7 9F 53 ; 20 encoded levels hex 48 02 66 E2 4F 5A 9D 75 hex 48 A5 1C F8 97 D9 99 3F hex AF FD 1E BB BB E2 42 FF hex FB E2 A2 22 23 18 A2 87 hex 7B A0 27 39 A9 06 90 65 hex 8A F0 0B 49 DD D3 CA 22 hex 01 5E 58 85 A4 59 72 65 hex 97 57 99 D4 E7 55 3F 3E hex 3C 33 C2 8F 43 CB 99 AF hex 15 BA D6 E9 E6 79 EB 5D hex 8F 7E 97 6E 9E E3 FF E4 hex 0A F8 EF D6 EE B8 7C levelDataTable hex 82 98 84 90 86 03 06 88 hex 08 8A 15 8C 16 8E 10 60 hex 40 92 0D 94 18 96 09 14 hex 9A AA 9C A2 19 9E A0 1E hex 0A 13 A4 0F A6 A8 0E 02 hex 1A 12 AC B6 AE 01 1F B0 hex B2 05 B4 1D 0B 1B 07 B8 hex BA 00 BC BE 04 0C 11 1C levelBrickTable dc.b %00000000 ; Brick layout lookup table dc.b %11000000 dc.b %00110000 dc.b %11110000 dc.b %00001100 dc.b %11001100 dc.b %00111100 dc.b %11111100 dc.b %00000011 dc.b %11000011 dc.b %00110011 dc.b %11110011 dc.b %00001111 dc.b %11001111 dc.b %00111111 dc.b %11111111 echo "Level Data Length:",[*-levelData]d IF TITLE_SONG==0 ; Devil's Workshop echo "Title Song: Devil's Workshop" melodyChannel dc.b 4 ; High notes dc.b 10 ; Low notes beatChannel equ 8 ; White noise beatHi equ $80 | 8 ; Snare drum beatLo equ $80 | 31 ; Bass drum noteData0 dc.b -96 dc.b 30,96 dc.b 27,48 dc.b 25,48 dc.b 30,72 dc.b 27,24 dc.b -96 dc.b 30,96 dc.b 25,24 dc.b 25,24 dc.b 25,48 dc.b 27,72 dc.b 27,24 dc.b -96 dc.b 30,96 dc.b 27,96 dc.b 25,96 dc.b -96 dc.b 30,96 dc.b 27,96 dc.b 25,96 dc.b -96 dc.b -96 measureLength0 equ *-noteData0 noteData1 dc.b 17,23 dc.b beatLo,1 dc.b beatHi,24 dc.b 15,24 dc.b 15,23 dc.b beatLo,1 measureLength1 equ *-noteData1 noteOffset dc.b 0 measureLength dc.b measureLength0 dc.b measureLength1 echo "Music Data Length:",[*-noteData0]d ENDIF IF TITLE_SONG==1 ; Lost in Legoland echo "Title Song: Lost in Legoland" melodyChannel dc.b 7 ; High notes dc.b 6 ; Low notes beatChannel equ 8 ; White noise beatHi equ $80 | 8 ; Snare drum beatLo equ $80 | 31 ; Bass drum noteData0 IF 0 ; 0 = Music, 1 = Silence dc.b -96 ELSE dc.b 16,80 dc.b 21,20 dc.b 17,40 dc.b 17,40 dc.b 16,60 dc.b 19,20 dc.b 14,40 dc.b 16,60 dc.b 14,40 dc.b 21,20 dc.b 17,40 dc.b 17,40 dc.b 16,60 dc.b 19,20 dc.b 14,40 dc.b -20 dc.b 16,80 dc.b 21,20 dc.b 17,40 dc.b 17,40 dc.b 16,60 dc.b 19,20 dc.b 14,40 dc.b 16,60 dc.b 14,40 dc.b 21,20 dc.b 17,40 dc.b 17,40 dc.b 16,60 dc.b 19,20 dc.b 14,40 dc.b -20 dc.b -80 dc.b -80 dc.b -80 dc.b -80 dc.b -80 dc.b -80 dc.b -80 dc.b -80 ENDIF measureLength0 equ *-noteData0 noteData1 bassLo equ 19 bassHi equ 9 IF 0 ; 0 = Music, 1 = Silence dc.b -96 ELSE dc.b bassHi,17 dc.b beatHi,3 dc.b -7 dc.b beatLo,3 dc.b bassHi,10 dc.b bassHi,9 dc.b beatLo,1 dc.b -9 dc.b beatLo,1 dc.b bassLo,9 dc.b beatHi,1 dc.b bassLo,10 dc.b -10 dc.b bassLo,18 dc.b beatLo,2 dc.b -9 dc.b beatHi,1 dc.b -9 dc.b beatLo,1 dc.b bassLo,8 dc.b beatHi,2 dc.b bassHi,19 dc.b beatLo,1 ENDIF measureLength1 equ *-noteData1 noteOffset dc.b 0 measureLength dc.b measureLength0 dc.b measureLength1 echo "Music Data Length:",[*-noteData0]d ENDIF IF TITLE_SONG==2 ; new echo "Title Song: new" melodyChannel dc.b 4 ; High notes dc.b 10 ; Low notes beatChannel equ 8 ; White noise beatHi equ $80 | 8 ; Snare drum beatLo equ $80 | 31 ; Bass drum noteData0 dc.b 28,12 dc.b 28,12 dc.b 28,12 dc.b 28,12 dc.b 27,12 dc.b 27,12 dc.b 27,12 dc.b 27,12 dc.b 25,12 dc.b 25,12 dc.b 25,12 dc.b 25,12 dc.b 30,12 dc.b 30,12 dc.b 30,12 dc.b 30,12 measureLength0 equ *-noteData0 noteData1 dc.b beatHi,24 dc.b -23 dc.b beatLo,1 dc.b beatHi,24 dc.b -23 dc.b beatLo,1 measureLength1 equ *-noteData1 noteOffset dc.b 0 measureLength dc.b measureLength0 dc.b measureLength1 echo "Music Data Length:",[*-noteData0]d ENDIF ; ; startCart ; startCart subroutine CLEAN_START ; Clear mem/setup registers resetGame ldx #$ff ; Reset stack pointer txs jsr initGame jsr startOptions jmp mainLoop ; ; initGame ; initGame subroutine lda #%00000101 ; Reflect playfield and bring to front sta CTRLPF lda #$00 ; PF0 will always be empty sta PF0 lda #bgColor ; Initialize background color sta COLUBK ; sta collideFlag ; Clear collision flag sta CXCLR ; Clear collision registers lda #>digitData ; Set the MSB for each of our digit pointers sta digit0+1 ; They are all the same since they don't sta digit1+1 ; cross any page boundaries sta digit2+1 sta digit3+1 sta digit4+1 sta digit5+1 lda #0 sta SWACNT sta SWBCNT lda SWCHB ; Check color/bw switch for ntsc/pal switching and #flag_lastcbsw ; Isolate color/bw switch ora gameFlags ; Set color/bw flag in gameFlags based on switch sta gameFlags ; Basically initialize the flag to prevent unwanted mode switch lda SWCHB ; Now check for power-on video mode lsr ; Check for Reset button (PAL50) bcs .checkSelect ; If carry set, then button not held lda gameFlags ; Else, set gameFlags to PAL50 ora #%11000000 sta gameFlags bne .setDefaults .checkSelect lsr ; Check for Select button (PAL60) bcs .setDefaults ; If carry set, then button not held lda gameFlags ; Else, set gameFlags to PAL60 and #%00111111 ora #%10000000 sta gameFlags .setDefaults ldy #DEFAULT_GAME_MODE ; Set default game mode flags lda gameModes,y sta gameMode lda #DEFAULT_GAME_MODE asl asl asl asl asl ora gameMode ora #mode_switched sta gameMode jsr setPaddleColors .continue jsr updateScore ; Initialize score pointers ;lda gameFlags ; Force 50Hz ;ora #flag_pal50 ;sta gameFlags rts ; ; mainLoop ; mainLoop subroutine lda gameState ; gameState determines flow of each frame and #$0F ; ; optionsFrame ; optionsFrame subroutine cmp #state_options bne .checkNext jsr startVBlank jsr calcBallSpeed jsr moveBall jsr playMusic jsr endVBlank jsr optionsKernel jsr startOverscan jsr reflectAndRemove jsr readConsole jsr endOverscan jmp mainLoop .checkNext ; ; pregameFrame ; pregameFrame subroutine cmp #state_pregame bne .checkNext jsr startVBlank jsr updateSounds jsr pregameTimer jsr readControllers jsr updatePaddles jsr endVBlank jsr gameKernel jsr startOverscan jsr readConsole jsr endOverscan jmp mainLoop .checkNext ; ; ingameFrame ; ingameFrame subroutine cmp #state_ingame bne .checkNext jsr startVBlank jsr updateSounds jsr readControllers jsr updatePaddles IF JOYSTICK_BALL jsr joystickBall ENDIF IF !STATIC_BALL jsr calcBallSpeed jsr moveBall ENDIF jsr endVBlank jsr gameKernel jsr startOverscan jsr reflectAndRemove jsr readConsole jsr endOverscan jmp mainLoop .checkNext ; ; balloutFrame ; balloutFrame subroutine cmp #state_ballout bne .checkNext jsr startVBlank jsr updateSounds lda #0 jsr updatePaddles jsr endVBlank jsr gameKernel jsr startOverscan jsr readConsole jsr deathTimer jsr endOverscan jmp mainLoop .checkNext ; ; clearFrame ; clearFrame subroutine cmp #state_clear bne .checkNext jsr startVBlank jsr updateSounds jsr clearTimer jsr endVBlank jsr gameKernel jsr startOverscan jsr readConsole jsr endOverscan jmp mainLoop .checkNext ; ; invalid state... ; brk ; If this gets executed, then I screwed up somewhere! ; BRK will reset everything. ; ; startVBlank ; startVBlank subroutine VERTICAL_SYNC bit gameFlags ; Let's see if the 50Hz flag is set bvs .pal50 ; It's stored in the overflow bit, so check that lda #43 ; 60Hz mode sta TIM64T rts .pal50 lda #73 ; 50Hz mode sta TIM64T rts ; ; endVBlank ; endVBlank subroutine .wait lda INTIM ; Wait for timer to expire bne .wait sta WSYNC sta VBLANK rts ; ; startOptions ; startOptions subroutine lda #11 sta ballSpeed lda #13 sta ballDirection lda #ballStartX ; Set ball starting position sta ballx lda #ballStartY sta bally lda #0 sta ballx+1 sta bally+1 lda #$FF .topborder sta block1l sta block2l sta block2r sta block1r .bottomborder sta block1l+15 sta block2l+15 sta block2r+15 sta block1r+15 lda #$80 ldx #14 .sideborders sta block1l,x sta block1r,x dex bne .sideborders ldy #7 ; Draw REFLEX title screen .loop lda titlePF1l,y sta block1l+4,y lda titlePF2l,y sta block2l+4,y lda titlePF2r,y sta block2r+4,y lda titlePF1r,y sta block1r+4,y dey bpl .loop lda #state_options sta gameState lda #0 ; Silence! sta AUDV0 sta AUDV1 sta noteCounter sta noteCounter+1 lda #1 sta noteTimer sta noteTimer+1 rts ; ; playMusic ; playMusic subroutine ; This fun "little" music routine was inspired by the ; one Kirk Israel wrote in his excellent game JoustPong. ; http://www.alienbill.com/joustpong/ ; It's a bit heavier than Kirk's, I think, but it ; does volume decay and allows notes and percussion ; in both channels. The data's also a little compressed since ; rests are stored as a single negative byte while notes ; and percussion are stored as a word (freq,duration). ; For a note to be percussion, set freq to a negative value. ; As usual, music data is stored backwards. ldx #1 ; Loop through audio channels 1 and 0 .loop dec noteTimer,x ; Decrement the note timer bne .noteContinue ; If not 0, then note continues to play lda noteCounter,x ; Else examine the note counter bne .noteChange ; If positive, then get next note lda measureLength,x ; Else reset the counter sta noteCounter,x ; ...and save it .noteChange dec noteCounter,x ; Decrease the note counter lda noteCounter,x ; ...and load it clc ; Get ready for addition adc noteOffset,x ; Add the note data offset value for channel X tay ; Transfer to Y for indexing lda noteData0,y ; Get new note length bmi .rest ; If negative, then it's a rest sta noteTimer,x ; Else, store it in the note timer dec noteCounter,x ; Prepare to get note pitch dey ; Decrease the note data index lda noteData0,y ; Get new note pitch bmi .percussion ; If negative, then it's percussion sta AUDF0,x ; Else, set pitch lda melodyChannel,x sta AUDC0,x ; ...and channel (note waveform) bne .setVolume ; Unconditional branch .percussion and #%01111111 ; Mask out negative bit sta AUDF0,x ; Set frequency lda #beatChannel sta AUDC0,x ; Set channel (percussion waveform) .setVolume lda #musicVolume ; Read in constant (basically max volume) sta noteVolume,x ; Save it sta AUDV0,x ; ...and set volume bne .skipVolume ; Unconditional branch .rest eor #$FF ; If rest, then get 2's complement clc adc #1 sta noteTimer,x ; Save rest duration lda #0 sta noteVolume,x sta AUDV0,x ; Set the volume to 0 for rest beq .skipVolume .noteContinue ldy noteVolume,x ; See if we should decrease the note volume... beq .skipVolume ; If volume at 0, then do nothing dey ; Else decrease volume sty noteVolume,x ; Save it sty AUDV0,x ; Set volume .skipVolume dex ; Decrease channel counter bpl .loop ; Loop if positive rts ; ; decompressLevel ; decompressLevel subroutine block equ blockX ; Base block pointer blockExtra equ tryX ; Base "extra" block pointer blockIndex equ offsetX ; Board offset index lda #7 sta blockIndex ; Start at the top of the board lda #0 ; Init vars... sta block+1 ; Initialize high bytes of block pointers sta blockExtra+1 lda #block2l+4 ; Initialize low bytes of block pointers sta block lda #block1l+4 sta blockExtra ldy #0 ; Y tracks the levelDataTable (tree) pointer .decompressLoop ldx levelByte ; Prepare to grab a byte of compressed data lda levelData,x ; Load A with compressed byte and levelBit ; Isolate the bit we want beq .zeroBit ; If 0, then don't increment the pointer iny ; Increment tree pointer .zeroBit lda levelDataTable,y ; Get value in tree .incPointer lsr levelBit ; Shift the level bit to the right bne .incDone ; If it's still non-zero, then done ror levelBit ; Else, rotate the carried bit into left side again inc levelByte ; and increment the level byte .incDone tax ; Copy to X register for retrieval later (and set N flag) bpl .processLeaf ; If negative bit is not set, then it's a leaf, not a branch .processBranch and #%01111111 ; Strip out negative bit (branch flag) tay ; And save result as new tree index bne .decompressLoop ; Unconditional branch .processLeaf asl ; Shift to test the "level end" bit bmi .levelEnd ; If set, then jump to levelEnd txa ; else, get the original levelDataTable value back sty temp ; Save level data index and #%00001111 ; Isolate 4-block lookup index tay ; Prepare for table indexing lda levelBrickTable,y ; Get the brick layout ldy blockIndex ; Get destination offset index sta (block),y ; Save brick layout to board! txa ; Restore levelDataTable value and #%00010000 ; Isolate "extra" brick bit beq .storeBrick ; If zero, then save it lda #%00000011 ; Else, turn on "extra" brick .storeBrick sta (blockExtra),y ; Save "extra" brick to board! .decBlockIdx ldy #0 ; Reset the tree pointer dec blockIndex ; Prepare to fill next block bpl .decompressLoop ; If >= 0, then get next byte lda #block2l+4 ; Else, check to see if still drawing on left side cmp block beq .setRight ; If on left side of screen, jump to right bne .decompressLoop ; Else, finished drawing board (go fetch that end byte) .setRight lda #block2r+4 ; Set low bytes of block pointers... sta block lda #block1r+4 sta blockExtra lda #7 sta blockIndex ; Start at the top of the board bne .decompressLoop ; Get the next byte .levelEnd asl bpl .mirror lda #0 sta levelByte lda #%10000000 sta levelBit echo "Level Decompressor Length:",(*-decompressLevel)d .mirror ldx #3 ; X is used to index copy source on left side .copy ldy blockIndex ; Y is used to index copy destination on both sides lda #block2l+4 ; Check to see which copying function to use cmp block bne .copyRight ; If block ptr on the right, then branch .copyLeft inx ; Copy left side... easy stuff lda block2l+4,x ; Now that I'm done with the decompression routine sta (block),y ; I need to build the compression routine... lda block1l+4,x ; Which will live in an external app... probably online sta (blockExtra),y ; I've done it before, but right now, I need to sleep. Zzzzzz... jmp .moveCopyPtr ; (ps: compression routine is DONE! It's in JavaScript.) .copyRight cpy #-1 beq .endMirror lda block2l+4,y ; Copy right side... easy stuff sta (block),y lda block1l+4,y sta (blockExtra),y .moveCopyPtr dec blockIndex ; Prepare to fill next block bpl .copy ; If >= 0, then continue to reset index lda #block2l+4 ; Else, check to see if still drawing on left side cmp block bne .endMirror ; If on right side of screen, finish up .setRight2 lda #block2r+4 ; Set low bytes of block pointers sta block lda #block1r+4 sta blockExtra lda #7 sta blockIndex ; Start at the top of the board bne .copy .endMirror .countBlocks lda #0 sta blockCount ldx #1 ; This is our block counter .horizontalLoop cpx #0 beq .countRight .countLeft lda #block2l+4 ; Initialize low bytes of block pointers sta block lda #block1l+4 sta blockExtra bne .startVertLp .countRight lda #block2r+4 ; Initialize low bytes of block pointers sta block lda #block1r+4 sta blockExtra .startVertLp ldy #7 ; Start counting! .verticalLoop lda (blockExtra),y and #%00000011 beq .countSkip1 inc blockCount .countSkip1 lda (block),y lsr lsr bcc .countSkip2 inc blockCount .countSkip2 lsr lsr bcc .countSkip3 inc blockCount .countSkip3 lsr lsr bcc .countSkip4 inc blockCount .countSkip4 lsr lsr bcc .countSkip5 inc blockCount .countSkip5 dey bpl .verticalLoop dex bpl .horizontalLoop ; lda #01 ; Debug level end stuff ; sta blockCount rts ; ; startPregame ; startPregame subroutine lda #ball_minSpeed sta ballSpeed lda #2 sta ballDirection ELSE lda #3 sta ballmx lda #3 sta ballmy ENDIF lda #ballStartX ; Set ball starting position sta ballx lda #ballStartY sta bally lda #state_pregame sta gameState lda #pregameDelay sta stateTimer lda #0 ; Silence! sta AUDV0 sta AUDV1 lda #12 sta AUDC0 rts ; ; calcBallSpeed ; calcBallSpeed subroutine lda ballDirection ; Convert the 4-bit direction into a 2-bit lookup value and #%00000100 ; Check for odd/even quadrant beq .noInvert ; If even, then just isolate 2-bit lookup value lda ballDirection ; Else, invert lookup value eor #%00000011 ; Invert lookup value bpl .getIndex ; Uncondition jump .noInvert lda ballDirection ; Reload direction since it was demolished with the AND .getIndex and #%00000011 ; Isolate the lookup value tay ; And transfer it to Y for indexing lda deltaXTab,y ; Get X delta value sta ballmx+1 ; Save it in the ball X movement register (low order) sta blockX ; And also in blockX for temporary storage lda deltaYTab,y ; Get Y delta value sta ballmy+1 ; Save it in the ball Y movement register (low order) sta blockX+2 ; And also in blockX+2 for temporary storage lda #0 ; Zeroize the high order movement bytes sta ballmx ; Update ball X movement register (high order) sta ballmy ; Update ball Y movement register (high order) ldy #2 ; Prepare to loop through X and Y movement registers .loop ldx ballSpeed ; Load up the ball speed multiplier .loopx clc ; Prepare for addition (multiple times!) lda ballmx+1,y ; Grab low order byte for X or Y movement register adc blockX,y ; Add that looked-up value sta ballmx+1,y ; And save the result lda #0 ; Reset A to prep for adding the carry bit adc ballmx,y ; Add carry bit to the high order byte of the register sta ballmx,y ; And save the result dex ; Decrease multiplier counter bne .loopx ; Loop if > 0 dey ; Decrease Y counter dey ; ... twice, because the movement registers are word length bpl .loop ; And loop if it's positive (in this case, zero) lda ballDirection and #%00001000 beq .skipNegateX sec lda #0 sbc ballmx+1 sta ballmx+1 lda #0 sbc ballmx sta ballmx .skipNegateX lda ballDirection clc adc #4 and #%00001000 beq .skipNegateY sec lda #0 sbc ballmy+1 sta ballmy+1 lda #0 sbc ballmy sta ballmy .skipNegateY rts ; ; pregameTimer ; pregameTimer subroutine dec stateTimer beq .elapsed lda INPT4 bmi .end .elapsed lda #state_ingame sta gameState .setDifficulty lda ballAccelDelay sta ballCounter .end rts ; ; deathTimer ; deathTimer subroutine jsr updateScore dec stateTimer bne .end lda gameFlags and #flag_lives bne .takeLife jsr startOptions rts .takeLife lda gameMode and #mode_twoscores bne .pregame IF !INFINITE_LIVES dec gameFlags ENDIF .pregame jsr startPregame .end rts ; ; clearTimer ; clearTimer subroutine lda gameMode ; Check for scoring type and #mode_twoscores ; If it's two scores... bne .skipScore ; Then don't update the score here lda #$00 ; Else add points to score sta temp lda #$06 ; 12 * 50 = 600pts jsr addScore ; THIS IS ONLY A SECOND LEVEL JSR jsr updateScore .skipScore dec stateTimer bne .end jsr clearBlocks jsr decompressLevel jsr resetPaddles jsr startPregame lda ballAccelDelay cmp #ball_minDelay bmi .end dec ballAccelDelay .end rts ; ; updateSounds ; updateSounds subroutine lda sfxEcho0 ; Load echo value beq .end ; If 0, then no sound bpl .doTimer ; If positive, then continue sound lda #$0F ; Else, initialize new sound sta sfxEcho0 ; Echo and volume begin at max (15) sta sfxVolume0 lda #sfxEchoDelay ; Echo timer is reset sta sfxTimer0 .doTimer dec sfxTimer0 ; Decrease echo timer bpl .doVolume ; If positive, then continue sound lda #sfxEchoDelay ; Else, echo timer is reset sta sfxTimer0 dec sfxEcho0 ; Reduce echo value by 3 dec sfxEcho0 dec sfxEcho0 lda sfxEcho0 sta sfxVolume0 ; Set volume to new echo value .doVolume lda sfxVolume0 ; Load volume beq .end ; If zero, then end dec sfxVolume0 ; Reduce volume by 3 dec sfxVolume0 dec sfxVolume0 lda sfxVolume0 sta AUDV0 ; Actually set volume register .end rts ; ; addScore ; addScore subroutine sed ; Decimal mode clc ; Clear carry, we're getting ready to add adc score+2 ; A + LSB (right 2 digits) sta score+2 lda score+1 ; Load middle 2 digits adc temp ; Add carry and high order byte sta score+1 lda score ; Load first 2 digits adc #0 ; Add carry sta score cld ; We're not in decimal mode anymore, Toto .end rts ; ; updateScore ; updateScore subroutine ldy #5 ; Set up score pointers (counts down 5 to 0) .scoreloop tya ; lsr ; Divide by 2 to get score byte offset (save carry) tax ; Move A to X for use as offset lda score,x ; Get 2 digits bcc .highnibble ; If carry clear, then use high nibble and #$0F ; else isolate low nibble asl ; Multiply by 8 (the height of our graphics) asl ; A will be used as the LSB in our pointer asl bcc .endnibble .highnibble and #$F0 ; Isolate high nibble lsr ; Divide by two to get our pointer's LSB .endnibble sta temp ; Store LSB tya ; Calculate offset for the pointer itself asl ; Y*2 because each pointer is a word tax ; Move A to X for use as an offset lda temp ; Get our pointer's LSB clc adc # ## (Ball wraps around if no obstructions) bmi .checkYunder jsr ballOut jmp .doneWithBally .checkYunder cmp #0 ; Check Y <= ## bpl .doneWithBally jsr ballOut .doneWithBally sta bally lda ballx+1 ; 16-bit addition (ballx=ballx+ballmx) clc adc ballmx+1 sta ballx+1 lda ballx adc ballmx cmp #147 ; Check X > ## bmi .checkXunder jsr ballOut jmp .doneWithBallx .checkXunder cmp #21 ; Check X <= ## bpl .doneWithBallx jsr ballOut .doneWithBallx sta ballx rts ; ; ballOut ; ballOut subroutine ldx #state_ballout stx gameState ldx #deathDelay stx stateTimer ldx #0 stx paddle0 stx paddle1 sta temp lda gameMode and #mode_twoscores beq .end ldx #1 lda #133 cmp temp bmi .skip inx .skip inc score,x lda score,x cmp #3 bne .end lda gameFlags and #(flag_lives^$FF) sta gameFlags .end lda temp rts ; ; optionsKernel ; optionsKernel subroutine jsr scoreArea ;+22 22 jsr optionsArea ;+20 42 jsr positionBall ;+2 44 jsr gameArea ;+130 174 IF !VU_METERS ldy #18 jsr skipLines ;+18 192 ELSE ldy #10 jsr skipLines ;+10 184 jsr vuMetersArea ;+8 192 ENDIF rts ; ; gameKernel ; gameKernel subroutine jsr scoreArea ;+22 22 ldy #6 jsr skipLines ;+6 28 jsr positionBall ;+2 30 jsr gameArea ;+130 160 ldy #11 jsr skipLines ;+11 161 jsr livesArea ;+5 166 ldy #16 jsr skipLines ;+16 192 rts ; ; optionsArea (20 scanlines) ; optionsArea subroutine sta WSYNC lda #0 sta GRP0 lda #$0c sta COLUP0 sleep 15 sta RESP0 ;lda #1 ;sta HMP0 sta WSYNC ;sta HMOVE lda gameMode ; One- or two-player controller options and #mode_twoplayers sta NUSIZ0 ; Double sprite accordingly lda gameMode ; Joystick or driving control icon and #mode_driving ; Isolate driving control bit clc adc <#optionJS ; Clean math, takes advantage of driving control bit position sta blockPtr ; Set LSB of sprite pointer lda >#optionJS sta blockPtr+1 ; Set MSB of sprite pointer ldy #15 ldx #$0F .loop sta WSYNC lda (blockPtr),y sta GRP0 dex dex stx COLUP0 inx dey sta WSYNC lda (blockPtr),y sta GRP0 stx COLUP0 dey bpl .loop sta WSYNC ; Blank row lda #0 ; Here, we reset the graphics registers sta GRP0 sta NUSIZ0 sta WSYNC rts ; ; vuMetersArea (8 scanlines) ; vuMetersArea subroutine IF VU_METERS SLEEP 7 sta RESP1 ldx #-1 .loop lda noteVolume+1,x lsr lsr tay iny lda livesTab,y sta WSYNC sta GRP1 sta WSYNC sta WSYNC lda #0 sta WSYNC sta GRP1 inx beq .loop rts ENDIF ; ; livesArea (5 scanlines) ; livesArea subroutine ldy #5 ; Init scanline counter lda gameMode ; Check game mode and #mode_twoscores ; If a competitive game, then don't show lives bne .skipLives bit gameFlags bmi .palColors lda #livesColor ; Set the PF color for lives bne .continue .palColors lda #PAL_livesColor ; Set the PF color for lives .continue sta COLUPF lda gameFlags and #flag_lives ; Isolate the life counter from gameFlags tax ; Transfer to X for indexing lda livesTab,x ; Lookup the bit pattern for x # of lives ldx #0 ; Set x to 0 for use in left half of non-mirrored PF .loop0 sta WSYNC ; Wait for next scanline stx PF1 ; Zeroize left half of PF1 sleep 40 ; Wait until left PF1 drawn before setting right PF1 sta PF1 ; Store lives bit pattern dey ; Decrease scanline counter bne .loop0 ; And loop stx PF1 ; After loop ends, zeroize PF1 again rts .skipLives sta WSYNC dey bne .skipLives rts ; ; skipLines (Y scanlines) ; skipLines subroutine .loop sta WSYNC dey bne .loop rts ; ; startOverscan ; startOverscan subroutine lda #2 bit gameFlags ; Let's see if the 50Hz flag is set bvs .pal50 ; It's stored in the overflow bit, so check that ldx #35 ; 60Hz sta WSYNC stx TIM64T sta VBLANK rts .pal50 ldx #65 ; 50Hz sta WSYNC stx TIM64T sta VBLANK rts ; ; endOverscan ; endOverscan subroutine .wait lda INTIM bne .wait rts ; ; getBallLocation ; getBallLocation subroutine lda ballx ; Calculate ball X position in blocks sec ; blockX = (ballx-19)/4 sbc #19 sta temp and #%00000011 ; offsetX = (ballx-19) mod 4 sta offsetX lda temp lsr lsr sta blockX sec ; Calculate ball Y position in blocks lda #[rowHeight * 16]+1 ; blockY = ((rowHeight*16+1)-bally)/8 sbc bally sta temp and #%00000111 ; offsetY = ((rowHeight*16+1)-bally) mod 8 sta offsetY lda temp lsr lsr lsr sta blockY rts ; ; getBrickValue ; inputs: a,y (block deltas) ; outputs: a (bit position of collision; 0 if none) ; getBrickValue subroutine tya ; Add Y to blockY clc adc blockY sta tryY ; Store result in tryY txa ; Add X to blockX clc adc blockX sta tryX ; Store result in tryX sta tryX_store beq .isPaddle ; Determine if tryX,tryY is a paddle cmp #31 beq .isPaddle lda tryY beq .isPaddle cmp #15 ; beq .isPaddle bne .notPaddle .isPaddle lda #-1 bne .continue .notPaddle lda #0 .continue sta temp lda tryX lsr ; Divide tryX by 8 to get PF byte 0,1,2, or 3 lsr lsr sta blockSection ; Store result in blockSection lda <#blockData ; Load LSB of blockData sta blockPtr ; Store it as LSB of blockPtr ldy blockSection ; Load X for counting beq .skipOffset ; If blockSection is 0, then no offset calculation .loop clc ; Else loop to find blockPtr offset adc #playfieldHeight ; Add playfieldHeight (16) to blockPtr sta blockPtr lda tryX ; And subtract 8 from tryX adc #-8 ; adc vs. sbc because I know carry is clear sta tryX ; Store result lda blockPtr ; Reload blockPtr and loop dey bne .loop .skipOffset lda blockSection lsr ; Determine if blockSection is even or odd bcc .dontFlip ; If even, then don't flip tryX lda tryX ; Else, flip tryX eor #%00000111 ; 0=7,1=6,2=5,etc... jmp .testBrick .dontFlip lda tryX .testBrick sta tryX tay lda singleBrickTab,y ldy tryY and (blockPtr),y ora temp sta temp rts ; ; reflect ; reflect subroutine ; Overview of inputs ; A non-zero value in temp indicates a collision ; x,y contain values between -1 and 3 ; -1 = Reflect ball movement if positive ; 0 = Reflect ball movement (+ to -, or - to +) ; 1 = Reflect ball movement if negative ; 2 = Do not reflect ; 3 = Special ball handling because ball is "buried" ; in a brick, not just touching ; Note: I tried very hard to keep the ball from behaving ; stupidly during "ambiguous" collisions usually caused ; by very high ballmx or ballmy values. This is my excuse ; for the following bit of spaghetti... lda temp ; Check temp PAUSEHERE bne .tryX ; If non-zero, then process horizontal movement rts ; Else return from sub .tryX txa ; Prep x (-1 to 3) for processing beq .reflectX ; If x=0, then reflect horizontal movement cmp #2 beq .tryY ; If x=2, then don't reflect horizontal movement (try vertical) cmp #3 bne .checkNegative ; If -1 or 1, then conditionally reflect ball in specified direction lda tryX_store ; If x=3, prepare for "buried" ball processing bne .checkRight ; If not buried in left side paddle, then check the right side ldx #1 ; Else force ball to move right (positive) bne .checkBottom ; Unconditional jump to check bottom side paddle .checkRight cmp #31 ; Check if ball buried in right side paddle bne .noHorizontal ; If not, then no horizontal reflection ldx #-1 ; Else force ball to move left (negative) bne .checkBottom ; Unconditional jump to check bottom side paddle .noHorizontal ldx #2 ; No horizontal reflection .checkBottom lda tryY ; Check top/bottom bounds bne .checkTop ; If not buried in bottom side paddle, then check the top side ldy #-1 ; Else force ball to move up (negative) bne .tryX ; Unconditional jump to reprocess reflection with new x,y values .checkTop cmp #15 ; Check if ball buried in top side paddle bne .noVertical ; If not, then no vertical reflection ldy #1 ; Else force ball to move down (positive) bne .tryX ; Unconditional jump to reprocess reflection with new x,y values .noVertical ldy #2 ; No vertical reflection cpx #2 ; See if there's horizontal reflection bne .tryX ; If so, then reprocess reflection with new x,y values ldy #2 ; Else, reflect X only... (This assumption is not 100% right) bne .reflectX ; Unconditional jump to reflect horizontal movement .checkNegative eor ballmx ; EOR with ballmx bpl .tryY .reflectX lda #15 sec sbc ballDirection sta ballDirection .tryY tya ; Load signed value in a beq .reflectY ; If 0, then reflect cmp #2 beq .english ; If 2, then skip reflect eor ballmy ; EOR with ballmy bpl .english .reflectY lda ballDirection and #%00001000 ldy temp sta temp lda #7 sec sbc ballDirection and #%00000111 ora temp sty temp sta ballDirection .english ; This paddle english routine was the bane of my ; existence for far too long. I owe much thanks ; to the Stella 2.0 development team for their hard ; work on building advanced debugging features into ; the Stella emulator. Without their regular alpha ; posts and unwavering commitment to the VCS ; community, this routine would have languished for ; God-knows-how-much longer. I especially want to ; thank B. Watson from [Stella] list for keeping me ; abreast of new features and for taking to heart my ; feature requests and feedback about each alpha. lda gameState cmp #state_options ; Check for "options" state beq .done ; If so, exit .bottom lda tryY ; Check for collision with bottom row bne .top ; If not 0, then check the top row lda gameMode ; Bottom paddle may be player 2, so load game mode bits... and #mode_twoplayers ; And isolate two-player bit (bit 0) tay ; Save two-player bit in Y for paddlePos/paddleSize offset lda tryX_store ; Load ball X position ldx #-4 bmi .horizontalMath ; Unconditional branch .top cmp #playfieldHeight-1 ; Row 15 = top bne .right ; If not top, then check right column ldy #0 ; Top paddle is Player 1, so use zero offset lda #playfieldWidth-1 ; A = 31-tryX_store (need inverse values for top) sbc tryX_store ; (Carry was set on the equal CMP above and remains set) ldx #4 .horizontalMath sec ; Prepare for subtraction (carry state unknown) sbc paddlePos,y ; Subtract paddle position bcs .calcDelta ; If positive, then continue to calculate delta adc #paddleBound ; Otherwise add 46 (carry bit is cleared from subtraction) bpl .calcDelta .right lda gameMode ; If two-player bit is set, then no side english and #mode_twoplayers bne .done ldy #0 ; It's one-player mode, so index should be 0 lda tryX_store ; Check to see if right column hit cmp #playfieldWidth-1 bne .left ; If not, check left column lda tryY ldx #-8 bmi .verticalMath ; Unconditional branch .left cmp #0 ; Check to see if left column hit bne .done ; If not, then it must have been a brick! lda #15 sbc tryY ; (Carry was set on the equal CMP above and remains set) ldx #0 .verticalMath clc ; OPTIMIZE USE OF CLC/SEC HERE adc #playfieldWidth-1 ; Add 31... sec sbc paddlePos ; Don't need index because sides have no english in two-player mode ; To get the delta: ; A = ball position on paddle ; A = A - 2 ; if A<0 then Delta = -2 or -1 (maybe -3 sometimes) ; else A = A + 4 - paddleSize ; if A>0 then Delta = 1 or 2 (maybe 3 sometimes) ; Depending on feedback, this whole thing may be ; simplified into constant reflect vectors depending on part of ; paddle hit... not deltas. .calcDelta ;sec ; (Carry should always be set at this point) sbc #2 ; Subtract 2 to get a negative offset bmi .applyDelta ;clc ; (Carry is always set at this point) adc #4 ; ... so adding 5 here with carry sec ; Add operation cleared carry, so we have to set it again sbc paddleSize,y ; Subtract paddleSize bmi .done .applyDelta stx offsetY ; This is the offset that converts ball direction to 0-7 sta offsetX ; This is the ball direction delta (-2,-1,1,2) calculated in .calcDelta lda ballDirection ; Get ball's current direction (complimentary reflection at this point) clc adc offsetY ; Normalize ball direction to a value between 0 and 7 and #7 sec sbc offsetX ; Subtract the ball direction delta .checkLowBound bpl .checkHighBound lda #0 ; If normalized ball direction < 0 then set it to 0 beq .inRange .checkHighBound cmp #8 bcc .inRange lda #7 ; If normalized ball direction > 7 then set it to 7 .inRange sec ; (Carry state unknown) sbc offsetY ; De-normalize ball direction and #15 sta ballDirection ; Save it .done rts ; This has been another over-engineered routine brought to you by yours truly. ; ; removeBrick ; removeBrick subroutine lda temp bne .zapBlock rts .zapBlock pla ; At this point, rts will exit two subroutines pla lda gameState cmp #state_options ; Check for "options" state beq .end lda #8 sta ballFlashTimer lda temp cmp #-1 ; Check for paddle bne .continue ; If not, don't exit lda #$0F cmp sfxEcho0 beq .end lda #-1 ; Ping! sta sfxEcho0 lda #sfxPaddleFreq sta AUDF0 .speedUp lda #ball_maxSpeed cmp ballSpeed bmi .speedUpDone dec ballCounter bpl .speedUpDone lda ballAccelDelay sta ballCounter inc ballSpeed .speedUpDone jmp .end .continue ldy tryX ; Remove brick... lda doubleBrickTab,y ldy tryY and (blockPtr),y eor #%11111111 and (blockPtr),y sta (blockPtr),y ; lda #flashDuration ; sta stateTimer lda #-1 ; Ping! sta sfxEcho0 lda #sfxBrickFreq sta AUDF0 dec blockCount ; Decrease the block counter bne .addScore ; If not zero, then add score lda #state_clear sta gameState lda #clearDelay sta stateTimer lda #0 sta paddle0 sta paddle1 .addScore lda gameMode ; Check for scoring type and #mode_twoscores ; If it's two scores... bne .end ; Then don't update the score here lda #>brickScore ; Else add points to score sta temp lda #