;------------------------------------ ;Super Toboggan Challenge - Game Code ;(C)2019 The New Dimension ;by Richard Bayliss ;------------------------------------ GameCode sei ;Kill all existing interrupts ldx #$31 ldy #$ea stx $0314 sty $0315 lda #$81 sta $dc0d sta $dd0d lda #$00 sta $d418 sta $d019 sta $d01a sta ANIMDELAY sta ANIMPOINTER ;We want to use KERNAL RAM, but with memory expansion ;(without REU) for data after $9000, and before $D000. ;($A000-$CFFF) lda #$36 sta $01 ;Prepare VIC2 registers (Colour data charset). lda #$00 sta $d020 lda #$1b sta $d011 lda #$0f ;Background Multicolour #1 - For main course, etc. sta $d022 lda #$06 ;Background Multicolour #2 - For water, and things like that sta $d023 lda #$12 sta $d018 lda #$18 sta $d016 lda #0 sta LIGHTMODE ;For every new game played. The player's score should be initialised. ldx #$00 ResetPlayerScore lda #$00 sta PlayerScore,x inx cpx #$06 bne ResetPlayerScore ;Draw starting map. ldx #$00 DrawTestMap1 lda GameMapStartPosition,x sta $0400,x lda GameMapStartPosition+$100,x sta $0500,x lda GameMapStartPosition+$200,x sta $0600,x lda GameMapStartPosition+$2e8,x sta $06e8,x ;Fill all multicolour as RED lda #$0a sta $d800,x sta $d900,x sta $da00,x sta $dae8,x inx bne DrawTestMap1 ;Now draw the status panel to the screen ldx #$00 DrawStatusPanel lda StatusPanel,x sta $0720,x lda #$09 ;Multi white! sta $db20,x inx cpx #4*40 bne DrawStatusPanel ;Clear any unneccessary screen junk ldx #$00 clearjunk lda #$00 sta $06f8,x sta $07c0,x inx cpx #$28 bne clearjunk ;Initialise sprite position ldx #$00 initpos lda #$00 sta ObjPos,x sta $d000,x lda #0 sta $d010 inx cpx #$10 bne initpos ;Setup sprite multi colours ldx #$00 ldy #$01 stx $d025 sty $d026 ;Setup sprites, position, colour, etc. lda #$02 sta $d027 ;Player 1 colour lda #$95;Grab still frame for player toboggan sta $07f8 ;Store to sprite 0 frame lda #$58 sta ObjPos lda #$98 sta ObjPos+1 ;Set up the frame for the 3 lights lda LightsFrame ldx LightsFrame+1 ldy LightsFrame+2 sta $07f9 stx $07fa sty $07fb ;Set up the colour for the 3 lights lda #2 ;Red ldx #8 ;Orange ldy #5 ;Green sta $d028 stx $d029 sty $d02a ;Set up the position for the 3 lights lda #$4a sta ObjPos+2 clc adc #$0c sta ObjPos+4 clc adc #$0c sta ObjPos+6 lda #$40 sta ObjPos+3 sta ObjPos+5 sta ObjPos+7 lda #$02 ldx #$08 ldy #$05 lda #$ff ;Enable all of the sprites for this sta $d015 ;game. sta $d01c ;Initialise the game map, for every new game started. lda #GameMapStartPosition sta MapPosition+2 ;Setup the IRQ interrupts, for setting up the main body of the game ;Initialise the Y-Pos of the scroller. This will initialise the ;vertical screen position of the scroll control. lda #$10 ;Screen on - position 0 sta ypos ldx #irq lda #$7f stx $0314 sty $0315 sta $dc0d sta $dd0d lda #$2e sta $d012 lda #$1b sta $d011 lda #$01 sta $d01a lda #GetReadyTune jsr MusicInit cli ;Lights loop - We use 3 counts, before we start the game. GetReadyLoop jsr SYNCLOOP jsr ExpandMSB ;Expand sprite MSB jsr ScoreConvert ;Convert value of scores to actual score digits on screen inc LIGHTDELAY lda LIGHTDELAY cmp #$30 beq NextLight jmp GetReadyLoop NextLight lda #$00 sta LIGHTDELAY inc LIGHTMODE lda LIGHTMODE cmp #$09 beq RedLight cmp #$0a beq AmberLight cmp #$0b beq GreenLight cmp #$0c beq StartTheGame jmp GetReadyLoop RedLight lda #1 jsr MusicInit lda #$0a sta $d028 jmp GetReadyLoop AmberLight lda #1 jsr MusicInit lda #$07 sta $d029 jmp GetReadyLoop GreenLight lda #2 jsr MusicInit lda #$0d sta $d02a jmp GetReadyLoop StartTheGame ;Initialise music lda #$00 jsr MusicInit ;The main game loop, which will allow control the main game, ;scrolling, etc. GameLoop jsr SYNCLOOP jsr ExpandMSB ;Expand the sprite area so more than 256 pixels can be used horizontally jsr Scroller ;Call the game map scroller subroutine. jsr PlayerControl ;Main player control jsr SPRBGRCollision ;Read sprite/background collision jsr AnimatePlayer ;Animate the player's toboggon jsr AnimateWaterChar ;Animate (Scroll down the water char) jsr RemoveLights ;When the game starts, remove the lights from the screen jsr ScoreConvert ;Convert value of scores to actual score digits on screen jmp GameLoop ;Expand the game's sprite area so sprites can be positioned further ;than just 256 pixels. Also make MSB automatic, before storing ;the virtual position of a sprite into an actual position. ExpandMSB ldx #$00 expdsprs lda ObjPos+1,x sta $d001,x lda ObjPos,x asl ror $d010 sta $d000,x inx inx cpx #$10 bne expdsprs rts ;Our main game map scroller. First of all calculate the number ;of bytes of the YPOS pointer (Which controls the vertical ;screen position of a raster split). Every 8 bytes can ;perform the hardscroll code (Which literally scrolls the ;screen one row below each previous row). Scroller lda ypos ;scroll, until after 8 bytes has been read. clc ScrollSpeed adc #2 ;The set speed of the scroll (Self-Modified) sta ypos lda ypos cmp #$18 ;Does the $D011 counter for YPOS reach past #$18? bcc ExitScrollControl ;No, allow the scroll to continue lda #$10 ;Reset y-pos control sta ypos jsr HardScroll ;Then call in the hard scroll. ExitScrollControl ;Nothing else need to be done here rts ;The hard scroll - Reset the YPOS, soft scroll control, then scroll ;the screen position. HardScroll lda #$10 sta ypos ;Perform the main hardscroll, through a loop. We ;basically grab a bottom, but one row, and ;then shift it to the next row. jsr ScrollRows ;Subroutine which takes a row and moves it to the next row. ;Store the map position to the very top row, after every iteration. ldx #$27 StoreMap MapPosition lda GameMapDefaultPosition,x ;Self-Mod position of the map ... sta row0,x ;Stored to the screen. dex bpl StoreMap ;Move from the last position of the map memory, to the ;next row. We are calculating the map position backwards. lda MapPosition+1 sec sbc #$28 sta MapPosition+1 lda MapPosition+2 sbc #$00 sta MapPosition+2 ;Check whether or not the finish has been reached in the ;self-mod routine. Simply check the value of the hi-byte ;to ensure it has not gone past the very first row of the ;game map. lda MapPosition+2 cmp #>MapStart bcs MapOkay ;The finish has been reached, so restart the map position ;from the default start position. The idea is to ;keep on playing the game, until the toboggan has ;finally crashed. Since the map is short. lda #GameMapStartPosition sta MapPosition+2 ;Award a bonus of 1000 points to the player ;for completing the course. ldx #14 lda #BonusSFX jsr MusicInit+6 ldy #$00 Give1000PTS jsr AddScore iny cpy #100 bne Give1000PTS MapOkay rts ;Fetch one row, and place it to the next row. A few ;loops have been made to perform this task in order ;to save cycles. ScrollRows DoHardScroll ;Start with the top half of the screen first. ;This subroutine calls a simple loop, which will take 40 ;chars of a screen row, and then place it into the next row ;Char rows 1 - 10 will go one step downwards. Char 10 also should ;be stored into a temporary memory location (set as rowtemp) fitrows ldx #$27 shiftrows1 lda row10,x ;Grab the middle screen row. sta RowTemp2,x ;Store it to a temporary row pointer lda row9,x ;Now, grab screen row 9 sta row10,x ;then move it one row down ... lda row8,x ;Grab screen row 8 sta row9,x ;then move it one row down ... lda row7,x ;Same as before, as each sta row8,x ;row gets moved down to another lda row6,x ;row position. Until we have sta row7,x ;completed the first loop. lda row5,x ;Always work from the bottom to sta row6,x ;the top, to store each row. lda row4,x sta row5,x lda row3,x sta row4,x lda row2,x sta row3,x lda row1,x sta row2,x lda row0,x sta row1,x dex ;Move to next screen char column ;until 40 chars per row read (loop = $27 to $00)? bpl shiftrows1 ;keep doing it until complete. ;Now do a similar row shift loop routine for the bottom half of the game screen ldx #$27 shiftrows2 lda row17,x ;Work our way from the sta row18,x ;bottom of the screen, all the lda row16,x ;way to the middle of the screen. sta row17,x ;(Row 11). lda row15,x sta row16,x lda row14,x sta row15,x lda row13,x sta row14,x lda row12,x sta row13,x lda row11,x sta row12,x lda RowTemp2,x ;Pick up variable 'rowtemp' sta row11,x ;place it to the middle row dex ;Next char position ;40 chars read? (Looping from $27 to $00) bpl shiftrows2 ;Loop to shiftrows 2 rts ;Complete ;Player properties. Basically, read joystick in ;port 2. Use left/right to move the player. ;We don't need UP/DOWN controls this time. As ;that would spoil the challenge :) PlayerControl CheckLeft lda #4 ;Remember 1,2,4,8,16 (Up, down, left, right, fire) bit $dc00 ;Read joystick port 2 bne CheckRight lda ObjPos ;Move player to the left like normal sec sbc #1 cmp #LeftStopPosition bcs ForceLeft lda #LeftStopPosition ForceLeft sta ObjPos rts ;Check for RIGHT on Joystick port 2 CheckRight lda #8 bit $dc00 bne NoControl lda ObjPos ;Move the player to the right like normal clc adc #1 cmp #RightStopPosition bcc ForceRight lda #RightStopPosition ForceRight sta ObjPos NoControl ;Finished rts ;Software based sprite/background collision. To be fair, ;we only want the central area of the player to have a ;complete collision. Otherwise the game will be too hard. SPRBGRCollision ldx #$10 ;Setup collision middle and ldy #$32 ;top area of the player (1 char) stx xcolpos+1 ;Store to self-mod X Collision range sty ycolpos+1 ;Store to self-mod Y Collision range jsr CollisionTest ;Register collision test ldx #$10 ;Setup collision in both central areas ldy #$2a ;of the player (1 char) stx xcolpos+1 ;As above, store to self-mod X Collision range sty ycolpos+1 ;As above, store to self-mod Y Collision range jsr CollisionTest ;Register collision test ldx #$10 ;Setup collision in middle, ldy #$22 stx xcolpos+1 ;and bottom area (1 char) sty ycolpos+1 jsr CollisionTest ;Register collision test rts CollisionTest lda $d000 ;Grab HW sprite X sec xcolpos sbc #$10 ;Self-mod X collision position sta zp lda $d010 ;Grab HW MSB for player sbc #$00 lsr lda zp ror lsr lsr sta zp+3 lda $d001 sec ycolpos sbc #$2a lsr lsr lsr sta zp+4 lda #<$0400 ;Low byte of Screen start area sta zp+1 lda #>$0400 ;Hi byte of screen start area sta zp+2 ldx zp+4 beq Check CollLoop lda zp+1 clc adc #$28 sta zp+1 lda zp+2 adc #$00 sta zp+2 dex bne CollLoop ;Check the characters hit by the player Check ldy zp+3 lda (zp+1),y cmp #Flag ;Is flag collected? beq PickUpFlag ;Yes, allow flag to be picked up and score points cmp #SafeArea ;Is area range of chars 0-20? bcc Safe ;All chars 0-19, are safe, therefore any higher jmp Hit ;the player is hit! Safe rts ;The player player picks up the flag (Char 179) ;replace char with blank course char (00) PickUpFlag lda #$00 sta (zp+1),y lda #FlagSFX jsr MusicInit+6 ;Award the player 150 points (15*10) ldy #$00 Give150PTS jsr AddScore iny cpy #15 bne Give150PTS rts ;The player has been hit. Therefore the player should now ;perform an explosion subroutine. Hit lda #Bang ;Prepare BANG (Track 3), for explosion effect jsr MusicInit lda #$00 ;Re-use animation pointer and delay sta ANIMDELAY ;For explosion sta ANIMPOINTER ;For pointer lda #$10 sta ypos ;Flush the scroller position ;Now a new sync loop to perform the explosion. ExplodeLoop jsr SYNCLOOP jsr ExpandMSB jsr AnimateWaterChar ;Animate (Scroll down the water char) ;Delay sprite animation before calling the main ;explosion routine lda ANIMDELAY cmp #$04 beq OkayToExplode inc ANIMDELAY jmp ExplodeLoop ;ANIMDELAY has reached its delay count, perform ;main explosion routine, and animate it over ;the player's sprite. OkayToExplode lda #$00 ;Reset delay sta ANIMDELAY ldx ANIMPOINTER ;Read anim pointer, 12 frames DoExploder lda ExplodeFrame,x sta $07f8 lda ExplodeColour,x sta $d027 inx cpx #12 beq FinishedExplosion inc ANIMPOINTER jmp ExplodeLoop FinishedExplosion ldx #$00 stx ANIMPOINTER ldx #$00 RemoveSpritePositionAgain lda #$00 sta ObjPos,x inx cpx #$10 bne RemoveSpritePositionAgain ldx #$00 DoGameOverSprites lda GameOverFrame,x sta $07f8,x lda #$0e sta $d027,x inx cpx #8 bne DoGameOverSprites lda #$40 sta ObjPos sta ObjPos+8 lda ObjPos clc adc #$10 sta ObjPos+2 sta ObjPos+10 clc adc #$10 sta ObjPos+4 sta ObjPos+12 clc adc #$10 sta ObjPos+6 sta ObjPos+14 lda #$60 sta ObjPos+1 sta ObjPos+3 sta ObjPos+5 sta ObjPos+7 lda #$80 sta ObjPos+9 sta ObjPos+11 sta ObjPos+13 sta ObjPos+15 ;Initialise the GAME OVER ditty lda #GameOverTune jsr MusicInit ;Check if score, overall is a new hi score. CheckHiScore lda PlayerScore sec lda HiScore+5 sbc PlayerScore+5 lda HiScore+4 sbc PlayerScore+4 lda HiScore+3 sbc PlayerScore+3 lda HiScore+2 sbc PlayerScore+2 lda HiScore+1 sbc PlayerScore+1 lda HiScore+0 sbc PlayerScore+0 bpl NoNewHiScore ;New hi score achieved .... Make player's current score a ;brand new hi score ldx #$00 MakeNewHiScore lda PlayerScore,x sta HiScore,x inx cpx #$06 bne MakeNewHiScore ;Display the WELL DONE message, since the ;player has achieved a great high score. ldx #$00 SetHiScoreDisplay lda WellDoneText1,x sta StatusMessage,x lda WellDoneText2,x sta StatusMessage+40,x inx cpx #WellDoneTextEnd-WellDoneText1 bne SetHiScoreDisplay jmp GameOverLoop ;No high score was achieved, so display the BAD LUCK ;message instead. Then jump straight over to the ;GAME OVER loop. NoNewHiScore ldx #$00 SetFailDisplay lda FailText1,x sta StatusMessage,x lda FailText2,x sta StatusMessage+40,x inx cpx #FailTextEnd-FailText1 bne SetFailDisplay GameOverLoop lda #$00 sta SYNCTIMER cmp SYNCTIMER beq *-3 jsr ExpandMSB jsr AnimateWaterChar ;Animate (Scroll down the water char) jsr ScoreConvert ;Convert value of scores to actual score digits on screen lda $dc00 lsr lsr lsr lsr lsr bit FIREBUTTON ror FIREBUTTON bmi GameOverLoop bvc GameOverLoop jmp TitleScreen ;Animate the toboggan, which is moving across the screen ;First read the delay - make sure it equals ;the correct delay speed, before processing new frames. AnimatePlayer lda ANIMDELAY cmp #6 beq OkayToAnimatePlayer inc ANIMDELAY rts ;Reset delay pointer, and perform main animation ;for the player's toboggan. OkayToAnimatePlayer lda #$00 sta ANIMDELAY ldx ANIMPOINTER lda TobogganFrame,x sta $07f8 inx cpx #2 beq ResetPlayerAnim inc ANIMPOINTER rts ;Reset animation pointer, so frame loops ResetPlayerAnim ldx #$00 stx ANIMPOINTER rts ;Animate the water charset AnimateWaterChar lda BGANIMDELAY cmp #$01 beq AnimWater inc BGANIMDELAY rts AnimWater lda #0 sta BGANIMDELAY lda 122*8+$0807 ;Character 112 is the water character sta $02 ;Store to temp zeropage ldx #$07 cyclechar lda 122*8+$0800-1,x sta 122*8+$0800,x dex bne cyclechar lda $02 sta 122*8+$0800 rts ;Simple removal of the lights from screen. All sprites ;that form the lights go to the right of the screen RemoveLights lda ObjPos+2 clc adc #$02 cmp #$ca bcc LightsNotOffset lda #$00 sta ObjPos+3 sta ObjPos+5 sta ObjPos+7 LightsNotOffset sta ObjPos+2 lda ObjPos+4 clc adc #2 sta ObjPos+4 lda ObjPos+6 clc adc #2 sta ObjPos+6 rts ;Main timer to synchronize the front end, game, etc. SYNCLOOP lda #$00 sta SYNCTIMER lda SYNCTIMER cmp SYNCTIMER beq *-3 rts ;Subroutine, which does the player's scoring AddScore inc PlayerScore+4 ldx #$05 ScoreLoop lda PlayerScore,x cmp #$0a ;Illegal value (Past 9) bne ScoreOK ;Reset digit to zero lda #$00 sta PlayerScore,x ;Then increment the next digit before one last used inc PlayerScore-1,x ScoreOK dex bne ScoreLoop rts ;Convert the player's score and hi score to actual score digits ScoreConvert !macro ConvertScoreToChars player_score, score_mask_left, score_mask_right { lda player_score jsr CheckScoreToStore sta score_mask_left lda score_mask_left clc adc #1 sta score_mask_right } +ConvertScoreToChars PlayerScore, ScoreMask, ScoreMask+1 +ConvertScoreToChars PlayerScore+1, ScoreMask+2, ScoreMask+3 +ConvertScoreToChars PlayerScore+2, ScoreMask+4, ScoreMask+5 +ConvertScoreToChars PlayerScore+3, ScoreMask+6, ScoreMask+7 +ConvertScoreToChars PlayerScore+4, ScoreMask+8, ScoreMask+9 +ConvertScoreToChars PlayerScore+5, ScoreMask+10, ScoreMask+11 +ConvertScoreToChars HiScore, HiScoreMask, HiScoreMask+1 +ConvertScoreToChars HiScore+1, HiScoreMask+2, HiScoreMask+3 +ConvertScoreToChars HiScore+2, HiScoreMask+4, HiScoreMask+5 +ConvertScoreToChars HiScore+3, HiScoreMask+6, HiScoreMask+7 +ConvertScoreToChars HiScore+4, HiScoreMask+8, HiScoreMask+9 +ConvertScoreToChars HiScore+5, HiScoreMask+10, HiScoreMask+11 rts CheckScoreToStore cmp #$00 bne notZero jsr SetZero rts notZero cmp #$01 bne notOne jsr setOne rts notOne cmp #$02 bne notTwo jsr setTwo rts notTwo cmp #$03 bne notThree jsr setThree rts notThree cmp #$04 bne notFour jsr setFour rts notFour cmp #$05 bne notFive jsr setFive rts notFive cmp #$06 bne notSix jsr setSix rts notSix cmp #$07 bne notSeven jsr setSeven rts notSeven cmp #$08 bne notEight jsr setEight rts notEight cmp #$09 bne notNine jsr setNine notNine rts SetZero lda #81 rts setOne lda #83 rts setTwo lda #85 rts setThree lda #87 rts setFour lda #89 rts setFive lda #91 rts setSix lda #93 rts setSeven lda #95 rts setEight lda #97 rts setNine lda #99 rts ;Raster split 1 - exclusive for playing music irq inc $d019 lda #split1 sta $d012 ldx #irq2 stx $0314 sty $0315 lda #1 sta SYNCTIMER jmp $ea7e ;Raster split 2 - This produces the game's scrolling irq2 inc $d019 lda #split2 sta $d012 lda #$ff sta $d015 lda ypos ;Smooth vertical scroller control, read from the Scroller subroutine sta $d011 lda #$12 ;Game graphics charset sta $d018 lda #1 ;White background sta $d021 lda #$0f ; Blue background colour scheme sta $d022 lda #$06 sta $d023 ldx #irq3 stx $0314 sty $0315 jmp $ea7e ;Raster split 3 - Produces a large black line after the scroll ;in order to stabilize the score panel below. irq3 inc $d019 lda #split3 sta $d012 lda #$7f ;Trick to force a black line over the screen sta $d011 sta $d015 lda #$12 sta $d018 ldx #irq4 stx $0314 sty $0315 jmp $ea7e irq4 inc $d019 lda #split4 sta $d012 lda #$1f ;Special stationary position for the screen sta $d011 lda #0 sta $d015 lda #$1a ;Charset for title screen and scoring panel sta $d018 lda #$00 sta $d021 lda #$0e ;Ice colour scheme for panel sta $d022 lda #$03 sta $d023 jsr PALNTSCPlayerTest ldx #irq stx $0314 sty $0315 jmp $ea7e ;PAL/NTSC music player. Check machine is PAL or NTSC PALNTSCPlayerTest lda SYSTEM cmp #1 beq PAL ;System is NTSC so use NTSC timer to sync the ;speed of music with PAL machines inc NTSCTimer lda NTSCTimer cmp #$06 beq ResetNTSCTimer PAL jsr MusicPlay rts ResetNTSCTimer lda #0 sta NTSCTimer rts BGANIMDELAY !byte 0 SYNCTIMER !byte 0 ;Game synchronized timer (Syncs the game code with the IRQ Rasters) ANIMDELAY !byte 0 ;Animation delay for player/explosion ANIMPOINTER !byte 0 ;Animation pointer for player/explosion SYSTEM !byte 0 ;PAL/NTSC system detect (Does this only once) NTSCTimer !byte 0 ;Timer that controls NTSC music playing LIGHTDELAY !byte 0 ;Delay counter, that controls the lights at the start of the game FIREBUTTON !byte 0 ;Fire button sensitivity control (Avoid sensitivity). LIGHTMODE !byte 0 ;XY Position for sprites when using expanded sprites ObjPos !byte 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ;Animation frame for the player (Toboggan) TobogganFrame !byte $80,$81 ;Animation for the player explosion ExplodeFrame !byte $82,$83,$84,$85,$86,$87,$88,$89,$89,$89,$89,$89 ExplodeColour !byte $09,$02,$08,$0a,$07,$0a,$08,$02,$09,$09,$09,$09,$09 ;LightsFrame LightsFrame !byte $8a,$8b,$8c ;Game Over Frame GameOverFrame !byte $8d,$8e,$8f,$90,$91,$92,$93,$94,$95 ;Sound effects for collect flag BonusSFX !byte $0C,$00,$00,$B1,$21,$B2,$B3,$B4,$B5,$B6,$B7,$B8,$B9,$BA,$BB,$BC !byte $BD,$BE,$BF,$C0,$C1,$C2,$C3,$C4,$C5,$C6,$C7,$90,$11,$00 FlagSFX !byte $0C,$00,$00,$B0,$21,$C0,$C8,$11,$D0,$D8,$E0,$E8,$00 PlayerScore !byte $00,$00,$00,$00,$00,$00 HiScore !byte $00,$00,$00,$00,$00,$00 ;Temporary row of bytes for storing the screen rows in place RowTemp2 !fill $28,$00 !ct scr WellDoneText1 !text "new hi score" WellDoneTextEnd WellDoneText2 !text " well done! " FailText1 !text "no hi score " FailTextEnd FailText2 !text " bad luck! "