.INCLUDE "header.asm" .INCLUDE "init.asm" .INCLUDE "registers.asm" .INCLUDE "memory.asm" ; TODO: define screen / ship / shot dimensions as constants. ; Sets A to 8-bit (& enables 8-bit "B" register). .MACRO SetA8Bit sep #%00100000 ; 8-bit A/B. .ENDM ; Sets A to 16-bit. .MACRO SetA16Bit rep #%00100000 ; 16-bit A. .ENDM ; Sets X/Y to 16-bit. .MACRO SetXY16Bit rep #%00010000 ; 16-bit X/Y. .ENDM ; Stores result to A. ; Assumes 16-bit X & 8-bit A. ; Modifies X. ; Updates randomBytePtr. .MACRO GetRandomByte ldx randomBytePtr lda $028000, X ; $028000: beginning of ROM bank 2. inx cpx #$8000 ; This is the size of the entire ROM bank. bne +++ ldx #0 +++ stx randomBytePtr .ENDM ; Modifies X and Y to move to the next elements in the sprite tables. .MACRO AdvanceSpritePointers .rept 4 inx .endr iny .ENDM .BANK 0 SLOT 0 .ORG 0 .SECTION "MainCode" Start: InitSNES ; By default we assume 16-bit X/Y and 8-bit A. ; If any code wants to change this, it's expected to do so itself, ; and to change them back to the defaults before returning. SetXY16Bit SetA8Bit jsr LoadPaletteAndTileData jsr InitWorld ; Set screen mode: 16x16 tiles for backgrounds, mode 1. lda #%11000001 sta BGMODE ; Set sprite size to 8x8 (small) and 32x32 (large). lda #%00100000 sta OAMSIZE ; Main screen: enable sprites & BG3. lda #%00010100 sta MSENABLE ; Turn on the screen. ; Format: x000bbbb ; x: 0 = screen on, 1 = screen off, bbbb: Brightness ($0-$F) lda #%00001111 sta INIDISP jmp MainLoop LoadPaletteAndTileData: ; For more details on DMA, see: ; http://wiki.superfamicom.org/snes/show/Grog%27s+Guide+to+DMA+and+HDMA+on+the+SNES ; http://wiki.superfamicom.org/snes/show/Making+a+Small+Game+-+Tic-Tac-Toe ; ; A lot of the graphics-related registers are explained in Qwertie's doc: ; http://emu-docs.org/Super%20NES/General/snesdoc.html ; ... but be careful, because there are some errors in this doc. ; ; bazz's tutorial (available from http://wiki.superfamicom.org/snes/) is ; quite helpful with palette / sprites / DMA, especially starting at ; http://wiki.superfamicom.org/snes/show/Working+with+VRAM+-+Loading+the+Palette ; Initialize the palette memory in a loop. ; We could also do this with a DMA transfer (like we do with the tile data ; below), but it seems overkill for just a few bytes. :) ; TODO: do it with a DMA transfer. ; First, sprite palette data: ldx #0 lda #128 ; Palette entries for sprites start at 128. sta CGADDR - lda.l SpritePalette, X sta CGDATA inx cpx #32 ; 32 bytes of palette data. bne - ; Now, BG3 palette data. ; Palette entries for BG3 start at 0. ldx #0 lda #0 sta CGADDR - lda.l TilePalette, X sta CGDATA inx cpx #8 ; 8 bytes of palette data. bne - ; TODO: make the "DMA stuff into VRAM" a macro or function. ; Set VMADDR to where we want the DMA to start. We'll store sprite data ; at the beginning of VRAM. ldx #$0000 stx VMADDR ; DMA 0 source address & bank. ldx #SpriteData stx DMA0SRC lda #:SpriteData sta DMA0SRCBANK ; DMA 0 transfer size. Equal to the size of sprites32.pic. ldx #4096 stx DMA0SIZE ; DMA 0 control register. ; Transfer type 001 = 2 addresses, LH. lda #%00000001 sta DMA0CTRL ; DMA 0 destination. lda #$18 ; The upper byte is assumed to be $21, so this is $2118 & $2119. sta DMA0DST ; Enable DMA channel 0. lda #%00000001 sta DMAENABLE ; Store background tile data at byte $2000 of VRAM. ; (VMADDR is a word address, so multiply by 2 to get the byte address.) ldx #$1000 stx VMADDR ; DMA 0 source address & bank. ldx #TileData stx DMA0SRC lda #:TileData sta DMA0SRCBANK ; DMA 0 transfer size. Equal to the size of tiles.pic. ldx #512 stx DMA0SIZE ; DMA 0 control register. ; Transfer type 001 = 2 addresses, LH. lda #%00000001 sta DMA0CTRL ; DMA 0 destination. lda #$18 ; The upper byte is assumed to be $21, so this is $2118 & $2119. sta DMA0DST ; Enable DMA channel 0. lda #%00000001 sta DMAENABLE ; Tell the system that the BG3 tilemap starts at $4000. lda #%00100000 sta BG3TILEMAP ; ... and that the background tile data for BG3 starts at $2000. lda #%00000001 sta BG34NBA ; Set up the BG3 tilemap. ; VRAM write mode: increments the address every time we write a word. lda #%10000000 sta VMAIN ; Set word address for accessing VRAM. ldx #$2000 ; BG 3 tilemap starts here. (Byte address $4000.) stx VMADDR ; Now write entries into the tile map. ldy #0 - GetRandomByte sta $00 ldx #$0000 ; This is a blank tile. ; 1 in 8 chance that we choose a non-blank tile. bit #%00000111 bne + ldx #$0002 bit #%10000000 bne + ldx #$8002 ; Flip vertically. + stx VMDATA iny ; The tile map is 32x32 (1024 entries). cpy #1024 bne - rts InitWorld: ; Clear the memory that's used to keep track of normal game state. SetA16Bit lda #0 ldx #$20 - sta 0, X inx inx cpx #$1000 bne - SetA8Bit jsr SetBackgroundColor ; Initial enemy ship-spawn cooldown. lda #30 sta enemyShipSpawnCooldown ; Player's initial starting location and health. lda #(256 / 4) sta playerX lda #((224 - 32) / 2) sta playerY lda #10 sta playerHealth ; (x-velocity, y-velocity) of 4 different player shot patterns. lda #6 sta shotVelocityTable lda #0 sta shotVelocityTable + 1 lda #3 sta shotVelocityTable + 2 lda #3 sta shotVelocityTable + 3 lda #0 sta shotVelocityTable + 4 lda #6 sta shotVelocityTable + 5 lda #-3 sta shotVelocityTable + 6 lda #3 sta shotVelocityTable + 7 lda #-6 sta shotVelocityTable + 8 lda #0 sta shotVelocityTable + 9 lda #-3 sta shotVelocityTable + 10 lda #-3 sta shotVelocityTable + 11 lda #0 sta shotVelocityTable + 12 lda #-6 sta shotVelocityTable + 13 lda #3 sta shotVelocityTable + 14 lda #-3 sta shotVelocityTable + 15 rts MainLoop: lda #%10000001 ; Enable NMI interrupt & auto joypad read. sta NMITIMEN wai ; Wait for interrupt. lda #%00000001 ; Disable NMI interrupt while processing. sta NMITIMEN jsr JoypadRead lda playerHealth cmp #0 beq + jsr UpdateWorld bra ++ + jsr GameOver ++ jsr UpdateSprites jsr FillSecondarySpriteTable bra MainLoop GameOver: ; Wait until the player hits Start. lda joy1 + 1 bit #$10 ; Start beq + jsr InitWorld + rts JoypadRead: ; Load joypad registers into RAM for easy inspection & manipulation. - lda HVBJOY bit #$01 ; If auto-joypad read is happening, loop. bne - ldx JOY1L stx joy1 ldx JOY2L stx joy2 rts ; TODO: move functions around to be in a more sensible order. JoypadHandler: JoypadUp: lda joy1 + 1 bit #$08 ; Up beq JoypadDown ; Button not pressed. lda playerY cmp #0 beq JoypadDown ; Value saturated. dec playerY dec playerY JoypadDown: lda joy1 + 1 bit #$04 ; Down beq JoypadLeft ; Button not pressed. lda playerY cmp #(224 - 32 - 8 - 4) ; player height, bottom status bar, bottom padding beq JoypadLeft ; Value saturated. inc playerY inc playerY JoypadLeft: lda joy1 + 1 bit #$02 ; Left beq JoypadRight ; Button not pressed. lda playerX cmp #0 beq JoypadRight ; Value saturated. dec playerX dec playerX JoypadRight: lda joy1 + 1 bit #$01 ; Right beq JoypadY ; Button not pressed. lda playerX cmp #(256 - 32) beq JoypadY ; Value saturated. inc playerX inc playerX JoypadY: lda joy1 + 1 bit #$40 ; Y beq JoypadX ; Button not pressed. lda backgroundRed cmp #0 beq JoypadX ; Value saturated. dec backgroundRed JoypadX: lda joy1 bit #$40 ; X beq JoypadL ; Button not pressed. lda backgroundRed cmp #31 beq JoypadL ; Value saturated. inc backgroundRed JoypadL: lda joy1 bit #$20 ; L beq JoypadR ; Button not pressed. lda backgroundBlue cmp #0 beq JoypadR ; Value saturated. dec backgroundBlue JoypadR: lda joy1 bit #$10 ; R beq JoypadB ; Button not pressed. lda backgroundBlue cmp #31 beq JoypadB ; Value saturated. inc backgroundBlue JoypadB: lda joy1 + 1 bit #$80 ; B beq JoypadDone jsr MaybeShoot JoypadDone: rts MaybeShoot: ; If the cooldown timer is non-zero, don't shoot. lda shotCooldown cmp #0 bne MaybeShootDone ; Find the first empty spot in the shots array. ldx #playerShotArray - lda 0, X cmp #0 beq + .rept shotSize inx .endr ; If we went all the way to the end, bail out. cpx #(playerShotArray + playerShotArrayLength * shotSize) bne - rts + ; Enable shot; set its position based on player position. lda #8 ; Sprite number. sta 0, X lda playerX clc adc #28 sta 1, X lda playerY clc adc #14 sta 2, X ; Get x- and y-velocity out of shotVelocityTable. lda nextShotState and #%00000000 ; 8 possibilities if we use #%00000111. ldy #0 - cmp #0 beq + .rept 2 iny .endr dec A bra - + inc nextShotState ; x-velocity. lda shotVelocityTable, Y sta 3, X ; y-velocity. lda shotVelocityTable + 1, Y sta 4, X ; Set cooldown timer. lda #8 sta shotCooldown MaybeShootDone: rts UpdateWorld: jsr UpdateShotCooldown jsr UpdateShotPositions jsr JoypadHandler jsr SpawnEnemyShips jsr UpdateEnemyShips jsr CheckCollisionsWithPlayer jsr CheckCollisionsWithEnemies jsr UpdateBackgroundScroll jsr UpdateHighScore rts UpdateShotCooldown: ; Update shot cooldown. lda shotCooldown cmp #0 beq + dec A sta shotCooldown + rts SpawnEnemyShips: lda enemyShipSpawnCooldown cmp #0 beq + dec A sta enemyShipSpawnCooldown rts + GetRandomByte and #%00011111 clc adc #16 sta enemyShipSpawnCooldown ; Find an empty spot in the array. ldy #0 - lda enemyShipArray, Y cmp #0 beq + .rept enemyShipSize iny .endr cpy #(enemyShipArrayLength * enemyShipSize) bne - rts ; Too many ships; bail. + lda #4 ; Sprite number. sta enemyShipArray, Y lda #254 sta enemyShipArray + 1, Y ; x. - GetRandomByte cmp #(224 - 32 - 8 - 4) bcs - ; Keep trying. sta enemyShipArray + 2, Y ; y. lda #0 sta enemyShipArray + 3, Y ; move AI type. GetRandomByte and #%00000011 cmp #1 beq + lda #0 + sta enemyShipArray + 4, Y ; shoot AI type. lda #12 sta enemyShipArray + 5, Y ; shot cooldown. rts ; TODO: reap ships if they move off the top, bottom, or right too. UpdateEnemyShips: ldy #0 ; Index into enemyShipArray. sty $00 ; Index into enemyShotArray. -- lda enemyShipArray, Y cmp #0 ; If it's not enabled, skip it. beq ++ ; Move the ship. ; TODO: implement different movement based on AI-type. lda enemyShipArray + 1, Y ; x clc adc #-3 ; x-velocity. bcs + lda #0 sta enemyShipArray, Y ; reap it. bra ++ + sta enemyShipArray + 1, Y ; move it. lda enemyShipArray + 5, Y ; shot cooldown cmp #0 beq + dec A sta enemyShipArray + 5, Y ; new shot cooldown bra ++ + ; Add a shot. ; TODO: implement different shooting based on shoot-type. lda #12 sta enemyShipArray + 5, Y ; new shot cooldown lda enemyShipArray + 1, Y sta $02 ; Enemy ship x. lda enemyShipArray + 2, Y sta $03 ; Enemy ship y. lda enemyShipArray + 4, Y sta $04 ; Enemy ship shoot-AI type. phy ldy $00 jsr SpawnEnemyShot sty $00 ply ++ ; Done processing this ship. .rept enemyShipSize iny .endr cpy #(enemyShipArrayLength * enemyShipSize) bne -- rts ; Expects: ; Y: index into enemyShotArray (bytes). ; $02: enemy ship x-position. ; $03: enemy ship y-position. ; $04: enemy ship shoot-AI type. ; ; Modifies: ; A. ; Y: to point at the next possible free index into enemyShotArray. SpawnEnemyShot: - ; Bail if at end of array. cpy #(enemyShotArrayLength * shotSize) bne + rts + lda enemyShotArray, Y cmp #0 beq + ; Try next slot. .rept shotSize iny .endr bra - + ; OK, found a spot. lda #9 ; Sprite number. sta enemyShotArray, Y lda $02 ; Get enemy x. sta enemyShotArray + 1, Y ; Save as shot x. lda $03 ; Get enemy y. clc adc #((32 - 4) / 2) ; Center it with enemy ship. sta enemyShotArray + 2, Y ; Save as shot y. lda #-6 sta enemyShotArray + 3, Y ; x-velocity. ; Choose velocities based on shoot AI type. lda $04 cmp #1 beq + ; Normal shot. lda #0 sta enemyShotArray + 4, Y ; y-velocity. rts ; Shot aimed toward player. + lda playerY cmp $03 bcs + lda #-2 sta enemyShotArray + 4, Y ; y-velocity. rts + lda #2 sta enemyShotArray + 4, Y ; y-velocity. rts UpdateShotPositions: ldx #0 UpdateShot: ; Updates position of one shot. lda playerShotArray, X cmp #0 beq ShotDone ; Add to the x-coordinate. If the carry bit is set, we went off the edge ; of the screen, so disable the shot. lda playerShotArray + 3, X ; x-velocity. sta $00 bit #%10000000 ; Check whether the velocity is negative. bne UpdateShotWithNegativeXVelocity lda playerShotArray + 1, X clc adc $00 bcs DisableShot sta playerShotArray + 1, X ; Store new x-coord. bra UpdateShotY UpdateShotWithNegativeXVelocity: ; TODO: wrap sprites when they go negative here, like we do with ; y-velocities. lda playerShotArray + 1, X ; Current x. clc adc $00 bcc DisableShot sta playerShotArray + 1, X UpdateShotY: ; Add to the y-coordinate. lda playerShotArray + 4, X ; y-velocity. sta $00 bit #%10000000 ; Check whether the velocity is negative. bne UpdateShotWithNegativeYVelocity lda playerShotArray + 2, X clc adc $00 cmp #224 bcs DisableShot sta playerShotArray + 2, X ; Store new y-coord. bra ShotDone UpdateShotWithNegativeYVelocity: lda playerShotArray + 2, X ; Current y. cmp #224 bcs + ; If the shot was "off the top" before moving, maybe we'll reap it. adc $00 ; Otherwise, just update it, sta playerShotArray + 2, X ; save the result, bra ShotDone ; and we know it shouldn't be reaped. + clc adc $00 cmp #224 bcc DisableShot ; If it's now wrapped around, reap it. sta playerShotArray + 2, X bra ShotDone DisableShot: stz playerShotArray, X ShotDone: ; TODO: in places where we .rept inx (etc), is it faster to use actual ; addition? .rept shotSize inx .endr cpx #((playerShotArrayLength + enemyShotArrayLength) * shotSize) bne UpdateShot rts ; Expects: ; $00: x-coordinate of ship's center. ; $01: y-coordinate of ship's center. ; $02: half of the shot's size. ; $03: x-coordinate of shot's upper-left. ; $04: y-coordinate of shot's upper-left. ; ; Modifies: ; $05 ; A: set to non-zero if there was a collision, zero otherwise. CheckCollision: lda $03 clc adc $02 ; Get the center of the shot. sbc $00 bpl + ; If the result is positive, great! eor #$ff ; Otherwise, negate it. inc A + ; A now contains dx, guaranteed to be positive. cmp #18 ; Threshold for "successful hit". bcc + lda #0 ; Already too far to be a hit; bail. rts + sta $05 ; Save dx for later. ; Find dy. lda $04 clc adc $02 ; Get the center of the shot. sbc $01 bpl + ; If the result is positive, great! eor #$ff ; Otherwise, negate it. inc A + ; A now contains dy, guaranteed to be positive. clc adc $05 ; Add dx. cmp #18 ; Threshold for "successful hit". lda #0 bcs + lda #1 ; Got a hit. + rts CheckCollisionsWithPlayer: lda #2 ; Half of shot's size. sta $02 ; Store player position statically. clc lda playerX adc #16 ; Can't overflow. sta $00 ; Store the center. lda playerY ; Store the center. Our ship is actually 31 pixels tall, so offsetting by ; 15 feels more "fair": a shot that hits the invisible bottom edge of the ; ship won't count as a hit. adc #15 sta $01 ldx #0 - lda enemyShotArray, X cmp #0 ; Check whether it's active. beq + lda enemyShotArray + 1, X ; x. sta $03 lda enemyShotArray + 2, X ; y. sta $04 jsr CheckCollision cmp #0 beq + ; OK, we got a hit! Disable the shot. lda #0 sta enemyShotArray, X ; ... and decrement the player's life. lda playerHealth cmp #0 beq + dec playerHealth + .rept shotSize inx .endr cpx #(enemyShotArrayLength * shotSize) bne - rts CheckCollisionsWithEnemies: lda #2 ; Half of shot's size. sta $02 ldy #0 ; Index into enemyShipArray. -- lda enemyShipArray, Y cmp #0 ; Check whether it's active. beq ++ ; Store enemy position statically. clc lda enemyShipArray + 1, Y ; x. adc #16 ; Can't overflow. sta $00 ; Store the center. lda enemyShipArray + 2, Y ; y. adc #15 sta $01 ; Store the center. ldx #0 ; Index into playerShotArray. - lda playerShotArray, X cmp #0 beq + lda playerShotArray + 1, X ; x. sta $03 lda playerShotArray + 2, X ; y. sta $04 jsr CheckCollision cmp #0 beq + ; OK, we got a hit! Disable the shot. lda #0 sta playerShotArray, X ; ... and also the enemy ship. sta enemyShipArray, Y ; Give that player some points. Players love points. SetA16Bit sed ; Set decimal mode. lda playerScore clc adc #1 ; We can't just "inc"; it doesn't know about decimal mode. sta playerScore cld ; Clear decimal mode. SetA8Bit bra ++ ; ... we're done with this ship; no need to check more shots. + .rept shotSize inx .endr cpx #(playerShotArrayLength * shotSize) bne - ++ .rept enemyShipSize iny .endr cpy #(enemyShipArrayLength * enemyShipSize) bne -- rts UpdateBackgroundScroll: ; Make the background scroll. Horizontal over time; vertical depending on ; player's y-coordinate. lda vBlankCounter sta BG3HOFS lda vBlankCounter + 1 sta BG3HOFS lda playerY .rept 3 lsr .endr sta BG3VOFS stz BG3VOFS rts UpdateHighScore: SetA16Bit lda playerScore cmp highScore bcc + sta highScore + SetA8Bit rts UpdateSprites: ; TODO: refactor into smaller pieces. ; This page is a good reference on SNES sprite formats: ; http://wiki.superfamicom.org/snes/show/SNES+Sprites ; It uses the same approach we're using, in which we keep a buffer of the ; sprite tables in RAM, and DMA the sprite tables to the system's OAM ; during VBlank. ; Sprite table 1 has 4 bytes per sprite, laid out as follows: ; Byte 1: xxxxxxxx x: X coordinate ; Byte 2: yyyyyyyy y: Y coordinate ; Byte 3: cccccccc c: Starting tile # ; Byte 4: vhoopppc v: vertical flip h: horizontal flip o: priority bits ; p: palette # ; Sprite table 2 has 2 bits per sprite, like so: ; bits 0,2,4,6 - High bit of the sprite's x-coordinate. ; bits 1,3,5,7 - Toggle Sprite size: 0 - small size 1 - large size ; Setting all the high bits keeps the sprites offscreen. ; Zero out the scratch space for the secondary sprite table. ldx #0 - stz spriteTableScratchStart, X inx cpx #numSprites bne - ldx #0 ; Index into sprite table 1. ldy #0 ; Index into sprite table 2. jsr MaybeGameOver ; Copy player coords into sprite table. lda playerX sta spriteTableStart, X lda playerY sta spriteTableStart + 1, X lda #0 sta spriteTableStart + 2, X ; Set priority bits so that the sprite is drawn in front. lda #%00010000 sta spriteTableStart + 3, X lda #%11000000 ; Enable large sprite. sta spriteTableScratchStart, Y AdvanceSpritePointers ; Now add enemy ships. sty $00 ; Save sprite table 2 index. ldy #0 ; Index into enemyShipArray. - lda enemyShipArray, Y cmp #0 ; If not enabled, skip to next ship. beq + ; Update sprite table 1. sta spriteTableStart + 2, X ; sprite number lda enemyShipArray + 1, Y sta spriteTableStart, X ; x lda enemyShipArray + 2, Y sta spriteTableStart + 1, X ; y lda #%01000000 ; flip horizontally. sta spriteTableStart + 3, X ; Update secondary sprite table. phy ; Save enemyShipArray index. ldy $00 lda #%11000000 ; Enable large sprite. sta spriteTableScratchStart, Y AdvanceSpritePointers sty $00 ply ; Restore enemyShipArray index. + .rept enemyShipSize iny .endr cpy #(enemyShipArrayLength * enemyShipSize) bne - ldy $00 ; Restore Y to its rightful self. ; Now add shots. sty $00 ; Save sprite table 2 index. ldy #0 ; Index into playerShotArray. - lda playerShotArray, Y cmp #0 beq + ; If not enabled, skip to next shot. ; Update sprite table 1. sta spriteTableStart + 2, X ; sprite number lda playerShotArray + 1, Y sta spriteTableStart, X ; x lda playerShotArray + 2, Y sta spriteTableStart + 1, X ; y ; Update secondary sprite table. phy ; Save playerShotArray index. ldy $00 lda #%01000000 ; Enable small sprite. sta spriteTableScratchStart, Y AdvanceSpritePointers sty $00 ply ; Restore playerShotArray index. + .rept shotSize iny .endr cpy #((playerShotArrayLength + enemyShotArrayLength) * shotSize) bne - ldy $00 ; Restore Y to its rightful self. ; Now add sprites to show player health. stz $01 lda #4 sta $02 - lda $01 cmp playerHealth beq + ; All done? lda #10 sta spriteTableStart + 2, X ; sprite number lda $02 sta spriteTableStart, X ; x clc adc #7 sta $02 lda #212 sta spriteTableStart + 1, X ; y ; Set priority bits so that the sprite is drawn in front. lda #%00110000 sta spriteTableStart + 3, X lda #%01000000 ; Enable small sprite. sta spriteTableScratchStart, Y AdvanceSpritePointers inc $01 bra - + ; Sprites to show player score. lda #76 sta $00 ; x-position lda #212 sta $01 ; y-position stz $02 ; Don't render leading zeroes. stz $03 ; ... not even for the second digit. lda playerScore + 1 jsr RenderTwoDigits lda playerScore jsr RenderTwoDigits inc $03 ; Render rightmost zero always. lda #0 jsr RenderTwoDigits ; Sprites to show high score. lda #(252 - 7 * 6) sta $00 lda #212 sta $01 stz $02 stz $03 lda highScore + 1 jsr RenderTwoDigits lda highScore jsr RenderTwoDigits inc $03 ; Render rightmost zero always. lda #0 jsr RenderTwoDigits ; The little "HI" sprite next to high-score. lda #(252 - 7 * 7 - 2) sta spriteTableStart, X lda #212 sta spriteTableStart + 1, X lda #74 sta spriteTableStart + 2, X ; Set priority bits so that the sprite is drawn in front. lda #%00110000 sta spriteTableStart + 3, X lda #%01000000 ; Enable small sprite. sta spriteTableScratchStart, Y AdvanceSpritePointers ; Now clear out the unused entries in the sprite table. - cpx #spriteTable1Size beq + lda #1 sta spriteTableStart, X AdvanceSpritePointers bra - + rts MaybeGameOver: lda playerHealth cmp #0 beq + rts + ; Sprites to show "Game Over" text. lda #80 ; G sta spriteTableStart + 2, X lda #112 sta spriteTableStart, X lda #104 sta spriteTableStart + 1, X lda #%00110000 sta spriteTableStart + 3, X lda #%01000000 ; Enable small sprite. sta spriteTableScratchStart, Y AdvanceSpritePointers lda #81 ; A sta spriteTableStart + 2, X lda #120 sta spriteTableStart, X lda #104 sta spriteTableStart + 1, X lda #%00110000 sta spriteTableStart + 3, X lda #%01000000 ; Enable small sprite. sta spriteTableScratchStart, Y AdvanceSpritePointers lda #82 ; M sta spriteTableStart + 2, X lda #128 sta spriteTableStart, X lda #104 sta spriteTableStart + 1, X lda #%00110000 sta spriteTableStart + 3, X lda #%01000000 ; Enable small sprite. sta spriteTableScratchStart, Y AdvanceSpritePointers lda #83 ; E sta spriteTableStart + 2, X lda #136 sta spriteTableStart, X lda #104 sta spriteTableStart + 1, X lda #%00110000 sta spriteTableStart + 3, X lda #%01000000 ; Enable small sprite. sta spriteTableScratchStart, Y AdvanceSpritePointers lda #84 ; O sta spriteTableStart + 2, X lda #112 sta spriteTableStart, X lda #114 sta spriteTableStart + 1, X lda #%00110000 sta spriteTableStart + 3, X lda #%01000000 ; Enable small sprite. sta spriteTableScratchStart, Y AdvanceSpritePointers lda #85 ; V sta spriteTableStart + 2, X lda #120 sta spriteTableStart, X lda #114 sta spriteTableStart + 1, X lda #%00110000 sta spriteTableStart + 3, X lda #%01000000 ; Enable small sprite. sta spriteTableScratchStart, Y AdvanceSpritePointers lda #83 ; E sta spriteTableStart + 2, X lda #128 sta spriteTableStart, X lda #114 sta spriteTableStart + 1, X lda #%00110000 sta spriteTableStart + 3, X lda #%01000000 ; Enable small sprite. sta spriteTableScratchStart, Y AdvanceSpritePointers lda #86 ; R sta spriteTableStart + 2, X lda #136 sta spriteTableStart, X lda #114 sta spriteTableStart + 1, X lda #%00110000 sta spriteTableStart + 3, X lda #%01000000 ; Enable small sprite. sta spriteTableScratchStart, Y AdvanceSpritePointers rts ; Expects: ; A: number to display (a byte where each nybble is from 0-9). ; X/Y: pointing at appropriate locations into the sprite tables. ; $00: x-position to render the leftmost digit into. ; $01: y-position to render the leftmost digit into. ; $02: if set, render leading zeroes. ; $03: if set, always render the zero for the low-order digit. ; ; Updates: ; X & Y to point at the next locations in the sprite tables. ; The sprite tables, to add (up to) 2 sprites for digits. ; $00: x-position to render additional digits into. ; $02: whether to render leading zeroes. ; $04-$06 (scratch). RenderTwoDigits: ; Store the high digit in $05 and the low digit in $06. sta $06 .rept 4 lsr .endr sta $05 lda $06 and #$0F sta $06 ; Render the first digit. lda $05 jsr RenderDigit lda $00 clc adc #7 sta $00 ; Set "render zero" for rightmost digit to true if requested. lda $02 ora $03 sta $02 ; Render the second digit. lda $06 jsr RenderDigit lda $00 clc adc #7 sta $00 rts ; Expects: ; A: number to display (from 0-9). ; X/Y: pointing at appropriate locations into the sprite tables. ; $00: x-position to render the digit into. ; $01: y-position to render the digit into. ; $02: whether to render if the number is zero. ; ; Updates: ; X & Y to point at the next locations in the sprite tables. ; The sprite tables, to add (up to) 1 sprite for digits. ; $02: whether to render further leading zeroes. ; $04 (scratch). RenderDigit: sta $04 cmp #0 bne + ; Non-zero: render it regardless. lda $02 cmp #0 bne + ; Render because "render zeroes" is set. rts ; Nothing to render. + lda #1 sta $02 ; Render leading zeroes from here on out. lda $04 clc adc #64 ; Base index of digit sprites. sta spriteTableStart + 2, X ; sprite number lda $00 sta spriteTableStart, X ; x lda $01 sta spriteTableStart + 1, X ; y ; Set priority bits so that the sprite is drawn in front. lda #%00110000 sta spriteTableStart + 3, X lda #%01000000 ; Enable small sprite. sta spriteTableScratchStart, Y AdvanceSpritePointers rts FillSecondarySpriteTable: ; The secondary sprite table wants 2 bits for each sprite: one to set the ; sprite's size, and one that's the high bit of the sprite's x-coordinate. ; It's annoying to deal with bitfields when thinking about business logic, ; so the spriteTableScratch array contains one byte for each sprite, in ; which the two most significant bits are the "size" and "upper x" bits. ; This function is meant to be called after UpdateWorld, and packs those ; bytes into the actual bitfield that the OAM wants for the secondary ; sprite table. ; ; The expected format of every byte in the scratch sprite table is: ; sx------ s = size (0 = small, 1 = large) ; x = flipped high x-coordinate (so 1 behaves like "enable"). ldx #0 ; Index into input table. ldy #0 ; Index into output table. - stz $00 ; Current byte; filled out by a set of 4 input table entries. .rept 4 ; For each byte, the lower-order bits correspond to the lower-numbered ; sprites; therefore we insert the current sprite's bits "at the top" ; and shift them right for each successive sprite. lsr $00 lsr $00 lda spriteTableScratchStart, X ora $00 sta $00 inx .endr lda $00 eor #%01010101 sta spriteTable2Start, Y iny cpx #numSprites bne - rts SetBackgroundColor: ; The background-color bytes are (R, G, B), each ranging from [0-31]. ; The palette color format is 15-bit: [0bbbbbgg][gggrrrrr] ; Set the background color. ; Entry 0 corresponds to the SNES background color. stz CGADDR ; Compute and the low-order byte and store it in CGDATA. lda backgroundGreen .rept 5 asl .endr ora backgroundRed sta CGDATA ; Compute the high-order byte and store it in CGDATA. lda backgroundBlue .rept 2 asl .endr sta $00 lda backgroundGreen .rept 3 lsr .endr ora $00 sta CGDATA rts VBlankHandler: jsr VBlankCounter jsr DMASpriteTables rti VBlankCounter: ; Increment a counter of how many VBlanks we've done. ; This is a 24-bit counter. At 60 vblanks/second, this will take ; 77 hours to wrap around; that's good enough for me :) inc vBlankCounter bne + inc vBlankCounter + 1 bne + inc vBlankCounter + 2 + rts DMASpriteTables: ; Store at the base OAM address. ldx #$0000 stx OAMADDR ; Default DMA control; destination $2104 (OAM data register). stz DMA0CTRL lda #$04 sta DMA0DST ; Our sprites start at $0100 in bank 0 and are #$220 bytes long. ldx #spriteTableStart stx DMA0SRC stz DMA0SRCBANK ldx #spriteTableSize stx DMA0SIZE ; Kick off the DMA transfer. lda #%00000001 sta DMAENABLE rts .ENDS ; Bank 1 is used for our graphics assets. .BANK 1 SLOT 0 .ORG 0 .SECTION "GraphicsData" SpriteData: .INCBIN "sprites32.pic" SpritePalette: .INCBIN "sprites32.clr" TileData: .INCBIN "tiles.pic" TilePalette: .INCBIN "tiles.clr" .ENDS ; Fill an entire bank with random numbers. .SEED 1 .BANK 2 SLOT 0 .ORG 0 .SECTION "RandomBytes" .DBRND 32 * 1024, 0, 255 .ENDS