Game 1 Unit 10 of 64 1 hr learning time
Lives and Death
Add a lives system with death and respawn.
16% of Neon Nexus
What You’re Building
Lives. Death. Stakes.

Now you can lose.
Lives System
Track lives in zero page:
STARTING_LIVES = 3
lives: .res 1
invuln_timer: .res 1 ; Invulnerability after respawn
game_over: .res 1
Death and Respawn
When collision is detected:
player_hit:
dec lives
lda lives
beq @game_over
; Respawn at centre
lda #PLAYER_START_X
sta player_x
lda #PLAYER_START_Y
sta player_y
; Grant invulnerability
lda #INVULN_TIME
sta invuln_timer
rts
@game_over:
lda #1
sta game_over
rts
Invulnerability
After respawning, the player is briefly invulnerable. This prevents instant re-death:
INVULN_TIME = 90 ; 1.5 seconds at 60fps
check_collisions:
; Skip collision check if invulnerable
lda invuln_timer
beq @check
dec invuln_timer
rts
@check:
; Normal collision detection...
During invulnerability, the player flashes (same flash effect as Unit 9).
Lives Display
Show remaining lives as sprites:
update_lives_display:
ldy #20 ; OAM offset for lives
ldx lives
beq @hide_all
; Show life 1
lda #8 ; Y position
sta oam_buffer, y
iny
lda #SPRITE_LIFE
sta oam_buffer, y
; ...repeat for each life
Game Over
When lives reach zero:
main_loop:
lda game_over
bne @game_over_loop
; Normal game logic...
jmp main_loop
@game_over_loop:
; Freeze - game is over
jmp @game_over_loop
The game stops. The player sees their final position.
The Code
; =============================================================================
; NEON NEXUS - Unit 10: Lives and Death
; =============================================================================
; Add lives system with death and respawn.
; =============================================================================
; -----------------------------------------------------------------------------
; NES Hardware Addresses
; -----------------------------------------------------------------------------
PPUCTRL = $2000
PPUMASK = $2001
PPUSTATUS = $2002
OAMADDR = $2003
PPUSCROLL = $2005
PPUADDR = $2006
PPUDATA = $2007
OAMDMA = $4014
JOYPAD1 = $4016
JOYPAD2 = $4017
; Controller buttons
BTN_A = %10000000
BTN_B = %01000000
BTN_SELECT = %00100000
BTN_START = %00010000
BTN_UP = %00001000
BTN_DOWN = %00000100
BTN_LEFT = %00000010
BTN_RIGHT = %00000001
; -----------------------------------------------------------------------------
; Game Constants
; -----------------------------------------------------------------------------
PLAYER_START_X = 124
PLAYER_START_Y = 116
PLAYER_SPEED = 2
ENEMY_SPEED = 1
NUM_ENEMIES = 4
STARTING_LIVES = 3
COLLISION_DIST = 6
INVULN_TIME = 90 ; 1.5 seconds of invulnerability
; Tile indices
TILE_EMPTY = 0
TILE_BORDER = 1
TILE_FLOOR = 2
TILE_CORNER_TL = 3
TILE_CORNER_TR = 4
TILE_CORNER_BL = 5
TILE_CORNER_BR = 6
SPRITE_PLAYER = 7
SPRITE_ENEMY = 8
SPRITE_LIFE = 9 ; Life icon
; Arena boundaries
ARENA_LEFT = 16
ARENA_RIGHT = 232
ARENA_TOP = 24 ; Leave room for HUD
ARENA_BOTTOM = 208
DIR_RIGHT = 1
DIR_LEFT = $FF
DIR_DOWN = 1
DIR_UP = $FF
BG_COLOUR = $0F
; -----------------------------------------------------------------------------
; Memory Layout
; -----------------------------------------------------------------------------
.segment "ZEROPAGE"
player_x: .res 1
player_y: .res 1
buttons: .res 1
temp: .res 1
row_counter: .res 1
frame_count: .res 1
lives: .res 1
invuln_timer: .res 1 ; Invulnerability countdown
game_over: .res 1 ; Non-zero when game is over
enemy_x: .res NUM_ENEMIES
enemy_y: .res NUM_ENEMIES
enemy_dir_x: .res NUM_ENEMIES
enemy_dir_y: .res NUM_ENEMIES
.segment "OAM"
oam_buffer: .res 256
.segment "BSS"
; -----------------------------------------------------------------------------
; iNES Header
; -----------------------------------------------------------------------------
.segment "HEADER"
.byte "NES", $1A
.byte 2
.byte 1
.byte $01
.byte $00
.byte 0,0,0,0,0,0,0,0
; -----------------------------------------------------------------------------
; Code
; -----------------------------------------------------------------------------
.segment "CODE"
reset:
sei
cld
ldx #$40
stx $4017
ldx #$FF
txs
inx
stx PPUCTRL
stx PPUMASK
stx $4010
@vblank1:
bit PPUSTATUS
bpl @vblank1
lda #0
@clear_ram:
sta $0000, x
sta $0100, x
sta $0200, x
sta $0300, x
sta $0400, x
sta $0500, x
sta $0600, x
sta $0700, x
inx
bne @clear_ram
@vblank2:
bit PPUSTATUS
bpl @vblank2
jsr load_palette
jsr draw_arena
jsr set_attributes
jsr init_game
; Hide all sprites initially
lda #$FF
ldx #0
@hide_all:
sta oam_buffer, x
inx
bne @hide_all
jsr update_all_sprites
lda #0
sta PPUSCROLL
sta PPUSCROLL
sta frame_count
lda #%10000000
sta PPUCTRL
lda #%00011110
sta PPUMASK
main_loop:
lda game_over
bne @game_over_loop
jsr read_controller
jsr move_player
jsr move_enemies
jsr check_collisions
jsr update_all_sprites
jmp main_loop
@game_over_loop:
; Game over - just loop (player can see final state)
jmp @game_over_loop
; -----------------------------------------------------------------------------
; Initialise Game
; -----------------------------------------------------------------------------
init_game:
lda #STARTING_LIVES
sta lives
lda #0
sta game_over
sta invuln_timer
lda #PLAYER_START_X
sta player_x
lda #PLAYER_START_Y
sta player_y
jsr init_enemies
rts
; -----------------------------------------------------------------------------
; Initialise Enemies
; -----------------------------------------------------------------------------
init_enemies:
lda #48
sta enemy_x+0
lda #48
sta enemy_y+0
lda #DIR_RIGHT
sta enemy_dir_x+0
lda #DIR_DOWN
sta enemy_dir_y+0
lda #200
sta enemy_x+1
lda #48
sta enemy_y+1
lda #DIR_LEFT
sta enemy_dir_x+1
lda #DIR_DOWN
sta enemy_dir_y+1
lda #48
sta enemy_x+2
lda #176
sta enemy_y+2
lda #DIR_RIGHT
sta enemy_dir_x+2
lda #DIR_UP
sta enemy_dir_y+2
lda #200
sta enemy_x+3
lda #176
sta enemy_y+3
lda #DIR_LEFT
sta enemy_dir_x+3
lda #DIR_UP
sta enemy_dir_y+3
rts
; -----------------------------------------------------------------------------
; Check Collisions
; -----------------------------------------------------------------------------
check_collisions:
; Skip if invulnerable
lda invuln_timer
beq @check
dec invuln_timer
rts
@check:
ldx #0
@check_enemy:
lda player_x
sec
sbc enemy_x, x
bpl @check_x_pos
eor #$FF
clc
adc #1
@check_x_pos:
cmp #COLLISION_DIST
bcs @next_enemy
lda player_y
sec
sbc enemy_y, x
bpl @check_y_pos
eor #$FF
clc
adc #1
@check_y_pos:
cmp #COLLISION_DIST
bcs @next_enemy
; Collision! Lose a life
jsr player_hit
rts
@next_enemy:
inx
cpx #NUM_ENEMIES
bne @check_enemy
rts
; -----------------------------------------------------------------------------
; Player Hit - Lose life and respawn
; -----------------------------------------------------------------------------
player_hit:
dec lives
lda lives
beq @game_over
; Respawn at start position
lda #PLAYER_START_X
sta player_x
lda #PLAYER_START_Y
sta player_y
; Start invulnerability
lda #INVULN_TIME
sta invuln_timer
rts
@game_over:
lda #1
sta game_over
rts
; -----------------------------------------------------------------------------
; Update All Sprites
; -----------------------------------------------------------------------------
update_all_sprites:
jsr update_player_sprite
jsr update_enemy_sprites
jsr update_lives_display
rts
; -----------------------------------------------------------------------------
; Update Player Sprite
; -----------------------------------------------------------------------------
update_player_sprite:
; Check if game over
lda game_over
beq @alive
; Hide player
lda #$FF
sta oam_buffer+0
rts
@alive:
; Check invulnerability flash
lda invuln_timer
beq @no_flash
and #%00000100
beq @show
; Hide during flash
lda #$FF
sta oam_buffer+0
rts
@show:
@no_flash:
lda player_y
sta oam_buffer+0
lda #SPRITE_PLAYER
sta oam_buffer+1
lda #0
sta oam_buffer+2
lda player_x
sta oam_buffer+3
rts
; -----------------------------------------------------------------------------
; Update Enemy Sprites
; -----------------------------------------------------------------------------
update_enemy_sprites:
ldx #0
ldy #4 ; Start after player sprite
@loop:
lda enemy_y, x
sta oam_buffer, y
iny
lda #SPRITE_ENEMY
sta oam_buffer, y
iny
lda #%00000001
sta oam_buffer, y
iny
lda enemy_x, x
sta oam_buffer, y
iny
inx
cpx #NUM_ENEMIES
bne @loop
rts
; -----------------------------------------------------------------------------
; Update Lives Display - Show life icons at top of screen
; -----------------------------------------------------------------------------
update_lives_display:
; OAM slots 20-31 for lives display (up to 3 lives)
ldy #20 ; OAM offset
ldx lives
beq @hide_all_lives
; Show life 1
lda #8 ; Y position (top of screen)
sta oam_buffer, y
iny
lda #SPRITE_LIFE
sta oam_buffer, y
iny
lda #0
sta oam_buffer, y
iny
lda #16 ; X position
sta oam_buffer, y
iny
cpx #1
beq @hide_rest
; Show life 2
lda #8
sta oam_buffer, y
iny
lda #SPRITE_LIFE
sta oam_buffer, y
iny
lda #0
sta oam_buffer, y
iny
lda #26
sta oam_buffer, y
iny
cpx #2
beq @hide_rest
; Show life 3
lda #8
sta oam_buffer, y
iny
lda #SPRITE_LIFE
sta oam_buffer, y
iny
lda #0
sta oam_buffer, y
iny
lda #36
sta oam_buffer, y
iny
rts
@hide_rest:
; Hide remaining life slots
@hide_loop:
cpy #32
bcs @done
lda #$FF
sta oam_buffer, y
iny
iny
iny
iny
jmp @hide_loop
@hide_all_lives:
ldy #20
jmp @hide_loop
@done:
rts
; -----------------------------------------------------------------------------
; Move Enemies
; -----------------------------------------------------------------------------
move_enemies:
ldx #0
@enemy_loop:
lda enemy_x, x
clc
adc enemy_dir_x, x
sta enemy_x, x
cmp #ARENA_LEFT
bcs @check_right
lda #DIR_RIGHT
sta enemy_dir_x, x
lda #ARENA_LEFT
sta enemy_x, x
jmp @move_y
@check_right:
cmp #ARENA_RIGHT
bcc @move_y
lda #DIR_LEFT
sta enemy_dir_x, x
lda #ARENA_RIGHT
sec
sbc #1
sta enemy_x, x
@move_y:
lda enemy_y, x
clc
adc enemy_dir_y, x
sta enemy_y, x
cmp #ARENA_TOP
bcs @check_bottom
lda #DIR_DOWN
sta enemy_dir_y, x
lda #ARENA_TOP
sta enemy_y, x
jmp @next_enemy
@check_bottom:
cmp #ARENA_BOTTOM
bcc @next_enemy
lda #DIR_UP
sta enemy_dir_y, x
lda #ARENA_BOTTOM
sec
sbc #1
sta enemy_y, x
@next_enemy:
inx
cpx #NUM_ENEMIES
bne @enemy_loop
rts
; -----------------------------------------------------------------------------
; Load Palette
; -----------------------------------------------------------------------------
load_palette:
bit PPUSTATUS
lda #$3F
sta PPUADDR
lda #$00
sta PPUADDR
ldx #0
@loop:
lda palette_data, x
sta PPUDATA
inx
cpx #32
bne @loop
rts
; -----------------------------------------------------------------------------
; Draw Arena
; -----------------------------------------------------------------------------
draw_arena:
bit PPUSTATUS
lda #$20
sta PPUADDR
lda #$00
sta PPUADDR
lda #0
sta row_counter
@draw_row:
lda row_counter
cmp #0
beq @top_row
cmp #1
beq @top_row
cmp #28
beq @bottom_row
cmp #29
beq @bottom_row
jmp @middle_row
@top_row:
lda row_counter
cmp #0
bne @top_row_inner
lda #TILE_CORNER_TL
sta PPUDATA
lda #TILE_BORDER
ldx #30
@top_fill:
sta PPUDATA
dex
bne @top_fill
lda #TILE_CORNER_TR
sta PPUDATA
jmp @next_row
@top_row_inner:
lda #TILE_BORDER
ldx #32
@top_inner_fill:
sta PPUDATA
dex
bne @top_inner_fill
jmp @next_row
@bottom_row:
lda row_counter
cmp #29
bne @bottom_row_inner
lda #TILE_CORNER_BL
sta PPUDATA
lda #TILE_BORDER
ldx #30
@bottom_fill:
sta PPUDATA
dex
bne @bottom_fill
lda #TILE_CORNER_BR
sta PPUDATA
jmp @next_row
@bottom_row_inner:
lda #TILE_BORDER
ldx #32
@bottom_inner_fill:
sta PPUDATA
dex
bne @bottom_inner_fill
jmp @next_row
@middle_row:
lda #TILE_BORDER
sta PPUDATA
sta PPUDATA
lda #TILE_FLOOR
ldx #28
@floor_fill:
sta PPUDATA
dex
bne @floor_fill
lda #TILE_BORDER
sta PPUDATA
sta PPUDATA
@next_row:
inc row_counter
lda row_counter
cmp #30
beq @done_drawing
jmp @draw_row
@done_drawing:
rts
; -----------------------------------------------------------------------------
; Set Attribute Table
; -----------------------------------------------------------------------------
set_attributes:
bit PPUSTATUS
lda #$23
sta PPUADDR
lda #$C0
sta PPUADDR
ldx #8
lda #$00
@attr_top:
sta PPUDATA
dex
bne @attr_top
ldx #6
@attr_floor:
lda #$00
sta PPUDATA
lda #%01010101
sta PPUDATA
sta PPUDATA
sta PPUDATA
sta PPUDATA
sta PPUDATA
sta PPUDATA
lda #$00
sta PPUDATA
dex
bne @attr_floor
ldx #8
lda #$00
@attr_bottom:
sta PPUDATA
dex
bne @attr_bottom
rts
; -----------------------------------------------------------------------------
; Read Controller
; -----------------------------------------------------------------------------
read_controller:
lda #1
sta JOYPAD1
lda #0
sta JOYPAD1
ldx #8
@read_loop:
lda JOYPAD1
lsr a
rol buttons
dex
bne @read_loop
rts
; -----------------------------------------------------------------------------
; Move Player
; -----------------------------------------------------------------------------
move_player:
lda buttons
and #BTN_UP
beq @check_down
lda player_y
sec
sbc #PLAYER_SPEED
cmp #ARENA_TOP
bcc @check_down
sta player_y
@check_down:
lda buttons
and #BTN_DOWN
beq @check_left
lda player_y
clc
adc #PLAYER_SPEED
cmp #ARENA_BOTTOM
bcs @check_left
sta player_y
@check_left:
lda buttons
and #BTN_LEFT
beq @check_right
lda player_x
sec
sbc #PLAYER_SPEED
cmp #ARENA_LEFT
bcc @check_right
sta player_x
@check_right:
lda buttons
and #BTN_RIGHT
beq @done
lda player_x
clc
adc #PLAYER_SPEED
cmp #ARENA_RIGHT
bcs @done
sta player_x
@done:
rts
; === NMI ===
nmi:
pha
txa
pha
tya
pha
lda #0
sta OAMADDR
lda #>oam_buffer
sta OAMDMA
inc frame_count
lda #0
sta PPUSCROLL
sta PPUSCROLL
pla
tay
pla
tax
pla
rti
irq:
rti
; -----------------------------------------------------------------------------
; Data
; -----------------------------------------------------------------------------
palette_data:
.byte BG_COLOUR, $11, $21, $31
.byte BG_COLOUR, $13, $23, $33
.byte BG_COLOUR, $19, $29, $39
.byte BG_COLOUR, $16, $26, $36
.byte BG_COLOUR, $30, $27, $17 ; Player + lives
.byte BG_COLOUR, $16, $26, $36 ; Enemies
.byte BG_COLOUR, $30, $27, $17
.byte BG_COLOUR, $30, $27, $17
; -----------------------------------------------------------------------------
; Vectors
; -----------------------------------------------------------------------------
.segment "VECTORS"
.word nmi
.word reset
.word irq
; -----------------------------------------------------------------------------
; CHR-ROM
; -----------------------------------------------------------------------------
.segment "CHARS"
; Tile 0: Empty
.byte $00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00
; Tile 1: Border
.byte %11111111,%10000001,%10000001,%11111111
.byte %11111111,%00010001,%00010001,%11111111
.byte %00000000,%01111110,%01111110,%00000000
.byte %00000000,%11101110,%11101110,%00000000
; Tile 2: Floor
.byte %00000000,%00000000,%00000000,%00000000
.byte %00000000,%00000000,%00000000,%10000001
.byte %00000000,%00000000,%00000000,%00000000
.byte %00000000,%00000000,%00000000,%00000000
; Tile 3: Corner TL
.byte %11111111,%11000000,%10100000,%10010000
.byte %10001000,%10000100,%10000010,%10000001
.byte %00000000,%00111111,%01011111,%01101111
.byte %01110111,%01111011,%01111101,%01111110
; Tile 4: Corner TR
.byte %11111111,%00000011,%00000101,%00001001
.byte %00010001,%00100001,%01000001,%10000001
.byte %00000000,%11111100,%11111010,%11110110
.byte %11101110,%11011110,%10111110,%01111110
; Tile 5: Corner BL
.byte %10000001,%10000010,%10000100,%10001000
.byte %10010000,%10100000,%11000000,%11111111
.byte %01111110,%01111101,%01111011,%01110111
.byte %01101111,%01011111,%00111111,%00000000
; Tile 6: Corner BR
.byte %10000001,%01000001,%00100001,%00010001
.byte %00001001,%00000101,%00000011,%11111111
.byte %01111110,%10111110,%11011110,%11101110
.byte %11110110,%11111010,%11111100,%00000000
; Tile 7: Player
.byte %00011000,%00011000,%00111100,%01111110
.byte %11111111,%10111101,%00100100,%00100100
.byte %00000000,%00011000,%00011000,%00111100
.byte %01000010,%01000010,%00011000,%00000000
; Tile 8: Enemy
.byte %00011000,%00111100,%01111110,%11111111
.byte %11111111,%01111110,%00111100,%00011000
.byte %00000000,%00011000,%00100100,%01000010
.byte %01000010,%00100100,%00011000,%00000000
; Tile 9: Life icon (small heart/ship)
.byte %00000000,%00100100,%01111110,%01111110
.byte %00111100,%00011000,%00000000,%00000000
.byte %00000000,%00000000,%00000000,%00100100
.byte %00011000,%00000000,%00000000,%00000000
; Fill rest
.res 8192 - 160, $00
Build It
ca65 nexus.asm -o nexus.o
ld65 -C nes.cfg nexus.o -o nexus.nes
Die three times and it’s over. The game now has stakes.
Next
Death is the stick. Unit 11 adds the carrot: collectibles.
What Changed
Unit 9 → Unit 10
+274-151
| 1 | 1 | ; ============================================================================= | |
| 2 | - | ; NEON NEXUS - Unit 9: Collision Detection | |
| 2 | + | ; NEON NEXUS - Unit 10: Lives and Death | |
| 3 | 3 | ; ============================================================================= | |
| 4 | - | ; Detect when player touches enemies. Visual feedback on collision. | |
| 4 | + | ; Add lives system with death and respawn. | |
| 5 | 5 | ; ============================================================================= | |
| 6 | 6 | | |
| 7 | 7 | ; ----------------------------------------------------------------------------- | |
| ... | |||
| 32 | 32 | ; ----------------------------------------------------------------------------- | |
| 33 | 33 | ; Game Constants | |
| 34 | 34 | ; ----------------------------------------------------------------------------- | |
| 35 | - | PLAYER_START_X = 124 | |
| 36 | - | PLAYER_START_Y = 116 | |
| 37 | - | PLAYER_SPEED = 2 | |
| 38 | - | ENEMY_SPEED = 1 | |
| 35 | + | PLAYER_START_X = 124 | |
| 36 | + | PLAYER_START_Y = 116 | |
| 37 | + | PLAYER_SPEED = 2 | |
| 38 | + | ENEMY_SPEED = 1 | |
| 39 | 39 | | |
| 40 | - | NUM_ENEMIES = 4 | |
| 41 | - | SPRITE_SIZE = 8 ; 8x8 pixel sprites | |
| 42 | - | COLLISION_DIST = 6 ; Overlap distance for collision | |
| 40 | + | NUM_ENEMIES = 4 | |
| 41 | + | STARTING_LIVES = 3 | |
| 42 | + | COLLISION_DIST = 6 | |
| 43 | + | INVULN_TIME = 90 ; 1.5 seconds of invulnerability | |
| 43 | 44 | | |
| 44 | 45 | ; Tile indices | |
| 45 | 46 | TILE_EMPTY = 0 | |
| ... | |||
| 52 | 53 | | |
| 53 | 54 | SPRITE_PLAYER = 7 | |
| 54 | 55 | SPRITE_ENEMY = 8 | |
| 56 | + | SPRITE_LIFE = 9 ; Life icon | |
| 55 | 57 | | |
| 56 | 58 | ; Arena boundaries | |
| 57 | 59 | ARENA_LEFT = 16 | |
| 58 | 60 | ARENA_RIGHT = 232 | |
| 59 | - | ARENA_TOP = 16 | |
| 61 | + | ARENA_TOP = 24 ; Leave room for HUD | |
| 60 | 62 | ARENA_BOTTOM = 208 | |
| 61 | 63 | | |
| 62 | 64 | DIR_RIGHT = 1 | |
| ... | |||
| 70 | 72 | ; Memory Layout | |
| 71 | 73 | ; ----------------------------------------------------------------------------- | |
| 72 | 74 | .segment "ZEROPAGE" | |
| 73 | - | player_x: .res 1 | |
| 74 | - | player_y: .res 1 | |
| 75 | - | buttons: .res 1 | |
| 76 | - | temp: .res 1 | |
| 77 | - | temp2: .res 1 | |
| 78 | - | row_counter: .res 1 | |
| 79 | - | frame_count: .res 1 | |
| 80 | - | collision_flag: .res 1 ; Non-zero if collision detected | |
| 81 | - | flash_timer: .res 1 ; Timer for visual feedback | |
| 75 | + | player_x: .res 1 | |
| 76 | + | player_y: .res 1 | |
| 77 | + | buttons: .res 1 | |
| 78 | + | temp: .res 1 | |
| 79 | + | row_counter: .res 1 | |
| 80 | + | frame_count: .res 1 | |
| 81 | + | lives: .res 1 | |
| 82 | + | invuln_timer: .res 1 ; Invulnerability countdown | |
| 83 | + | game_over: .res 1 ; Non-zero when game is over | |
| 82 | 84 | | |
| 83 | - | enemy_x: .res NUM_ENEMIES | |
| 84 | - | enemy_y: .res NUM_ENEMIES | |
| 85 | - | enemy_dir_x: .res NUM_ENEMIES | |
| 86 | - | enemy_dir_y: .res NUM_ENEMIES | |
| 85 | + | enemy_x: .res NUM_ENEMIES | |
| 86 | + | enemy_y: .res NUM_ENEMIES | |
| 87 | + | enemy_dir_x: .res NUM_ENEMIES | |
| 88 | + | enemy_dir_y: .res NUM_ENEMIES | |
| 87 | 89 | | |
| 88 | 90 | .segment "OAM" | |
| 89 | - | oam_buffer: .res 256 | |
| 91 | + | oam_buffer: .res 256 | |
| 90 | 92 | | |
| 91 | 93 | .segment "BSS" | |
| 92 | 94 | | |
| ... | |||
| 142 | 144 | jsr load_palette | |
| 143 | 145 | jsr draw_arena | |
| 144 | 146 | jsr set_attributes | |
| 145 | - | jsr init_enemies | |
| 146 | - | | |
| 147 | - | lda #PLAYER_START_X | |
| 148 | - | sta player_x | |
| 149 | - | lda #PLAYER_START_Y | |
| 150 | - | sta player_y | |
| 151 | - | | |
| 152 | - | lda #0 | |
| 153 | - | sta collision_flag | |
| 154 | - | sta flash_timer | |
| 155 | - | | |
| 156 | - | ; Player sprite | |
| 157 | - | lda player_y | |
| 158 | - | sta oam_buffer+0 | |
| 159 | - | lda #SPRITE_PLAYER | |
| 160 | - | sta oam_buffer+1 | |
| 161 | - | lda #0 | |
| 162 | - | sta oam_buffer+2 | |
| 163 | - | lda player_x | |
| 164 | - | sta oam_buffer+3 | |
| 165 | - | | |
| 166 | - | jsr update_enemy_sprites | |
| 147 | + | jsr init_game | |
| 167 | 148 | | |
| 168 | - | ; Hide remaining sprites | |
| 149 | + | ; Hide all sprites initially | |
| 169 | 150 | lda #$FF | |
| 170 | - | ldx #20 | |
| 171 | - | @hide_sprites: | |
| 151 | + | ldx #0 | |
| 152 | + | @hide_all: | |
| 172 | 153 | sta oam_buffer, x | |
| 173 | 154 | inx | |
| 174 | - | bne @hide_sprites | |
| 155 | + | bne @hide_all | |
| 156 | + | | |
| 157 | + | jsr update_all_sprites | |
| 175 | 158 | | |
| 176 | 159 | lda #0 | |
| 177 | 160 | sta PPUSCROLL | |
| ... | |||
| 184 | 167 | sta PPUMASK | |
| 185 | 168 | | |
| 186 | 169 | main_loop: | |
| 170 | + | lda game_over | |
| 171 | + | bne @game_over_loop | |
| 172 | + | | |
| 187 | 173 | jsr read_controller | |
| 188 | 174 | jsr move_player | |
| 189 | 175 | jsr move_enemies | |
| 190 | 176 | jsr check_collisions | |
| 191 | - | jsr update_player_sprite | |
| 192 | - | jsr update_enemy_sprites | |
| 177 | + | jsr update_all_sprites | |
| 193 | 178 | jmp main_loop | |
| 179 | + | | |
| 180 | + | @game_over_loop: | |
| 181 | + | ; Game over - just loop (player can see final state) | |
| 182 | + | jmp @game_over_loop | |
| 194 | 183 | | |
| 195 | 184 | ; ----------------------------------------------------------------------------- | |
| 196 | - | ; Check Collisions - Player vs all enemies | |
| 185 | + | ; Initialise Game | |
| 197 | 186 | ; ----------------------------------------------------------------------------- | |
| 198 | - | check_collisions: | |
| 187 | + | init_game: | |
| 188 | + | lda #STARTING_LIVES | |
| 189 | + | sta lives | |
| 199 | 190 | lda #0 | |
| 200 | - | sta collision_flag | |
| 191 | + | sta game_over | |
| 192 | + | sta invuln_timer | |
| 193 | + | | |
| 194 | + | lda #PLAYER_START_X | |
| 195 | + | sta player_x | |
| 196 | + | lda #PLAYER_START_Y | |
| 197 | + | sta player_y | |
| 198 | + | | |
| 199 | + | jsr init_enemies | |
| 200 | + | rts | |
| 201 | + | | |
| 202 | + | ; ----------------------------------------------------------------------------- | |
| 203 | + | ; Initialise Enemies | |
| 204 | + | ; ----------------------------------------------------------------------------- | |
| 205 | + | init_enemies: | |
| 206 | + | lda #48 | |
| 207 | + | sta enemy_x+0 | |
| 208 | + | lda #48 | |
| 209 | + | sta enemy_y+0 | |
| 210 | + | lda #DIR_RIGHT | |
| 211 | + | sta enemy_dir_x+0 | |
| 212 | + | lda #DIR_DOWN | |
| 213 | + | sta enemy_dir_y+0 | |
| 214 | + | | |
| 215 | + | lda #200 | |
| 216 | + | sta enemy_x+1 | |
| 217 | + | lda #48 | |
| 218 | + | sta enemy_y+1 | |
| 219 | + | lda #DIR_LEFT | |
| 220 | + | sta enemy_dir_x+1 | |
| 221 | + | lda #DIR_DOWN | |
| 222 | + | sta enemy_dir_y+1 | |
| 223 | + | | |
| 224 | + | lda #48 | |
| 225 | + | sta enemy_x+2 | |
| 226 | + | lda #176 | |
| 227 | + | sta enemy_y+2 | |
| 228 | + | lda #DIR_RIGHT | |
| 229 | + | sta enemy_dir_x+2 | |
| 230 | + | lda #DIR_UP | |
| 231 | + | sta enemy_dir_y+2 | |
| 232 | + | | |
| 233 | + | lda #200 | |
| 234 | + | sta enemy_x+3 | |
| 235 | + | lda #176 | |
| 236 | + | sta enemy_y+3 | |
| 237 | + | lda #DIR_LEFT | |
| 238 | + | sta enemy_dir_x+3 | |
| 239 | + | lda #DIR_UP | |
| 240 | + | sta enemy_dir_y+3 | |
| 241 | + | rts | |
| 242 | + | | |
| 243 | + | ; ----------------------------------------------------------------------------- | |
| 244 | + | ; Check Collisions | |
| 245 | + | ; ----------------------------------------------------------------------------- | |
| 246 | + | check_collisions: | |
| 247 | + | ; Skip if invulnerable | |
| 248 | + | lda invuln_timer | |
| 249 | + | beq @check | |
| 250 | + | dec invuln_timer | |
| 251 | + | rts | |
| 201 | 252 | | |
| 253 | + | @check: | |
| 202 | 254 | ldx #0 | |
| 203 | 255 | @check_enemy: | |
| 204 | - | ; Check X overlap | |
| 205 | - | ; |player_x - enemy_x| < COLLISION_DIST | |
| 206 | 256 | lda player_x | |
| 207 | 257 | sec | |
| 208 | 258 | sbc enemy_x, x | |
| 209 | - | bpl @check_x_positive | |
| 210 | - | ; Negative - negate it | |
| 259 | + | bpl @check_x_pos | |
| 211 | 260 | eor #$FF | |
| 212 | 261 | clc | |
| 213 | 262 | adc #1 | |
| 214 | - | @check_x_positive: | |
| 263 | + | @check_x_pos: | |
| 215 | 264 | cmp #COLLISION_DIST | |
| 216 | - | bcs @next_enemy ; No X overlap | |
| 265 | + | bcs @next_enemy | |
| 217 | 266 | | |
| 218 | - | ; Check Y overlap | |
| 219 | 267 | lda player_y | |
| 220 | 268 | sec | |
| 221 | 269 | sbc enemy_y, x | |
| 222 | - | bpl @check_y_positive | |
| 270 | + | bpl @check_y_pos | |
| 223 | 271 | eor #$FF | |
| 224 | 272 | clc | |
| 225 | 273 | adc #1 | |
| 226 | - | @check_y_positive: | |
| 274 | + | @check_y_pos: | |
| 227 | 275 | cmp #COLLISION_DIST | |
| 228 | - | bcs @next_enemy ; No Y overlap | |
| 276 | + | bcs @next_enemy | |
| 229 | 277 | | |
| 230 | - | ; Collision detected! | |
| 231 | - | lda #1 | |
| 232 | - | sta collision_flag | |
| 233 | - | lda #30 ; Flash for 30 frames | |
| 234 | - | sta flash_timer | |
| 235 | - | rts ; Exit early on first collision | |
| 278 | + | ; Collision! Lose a life | |
| 279 | + | jsr player_hit | |
| 280 | + | rts | |
| 236 | 281 | | |
| 237 | 282 | @next_enemy: | |
| 238 | 283 | inx | |
| ... | |||
| 241 | 286 | rts | |
| 242 | 287 | | |
| 243 | 288 | ; ----------------------------------------------------------------------------- | |
| 244 | - | ; Update Player Sprite - Flash when hit | |
| 289 | + | ; Player Hit - Lose life and respawn | |
| 290 | + | ; ----------------------------------------------------------------------------- | |
| 291 | + | player_hit: | |
| 292 | + | dec lives | |
| 293 | + | lda lives | |
| 294 | + | beq @game_over | |
| 295 | + | | |
| 296 | + | ; Respawn at start position | |
| 297 | + | lda #PLAYER_START_X | |
| 298 | + | sta player_x | |
| 299 | + | lda #PLAYER_START_Y | |
| 300 | + | sta player_y | |
| 301 | + | | |
| 302 | + | ; Start invulnerability | |
| 303 | + | lda #INVULN_TIME | |
| 304 | + | sta invuln_timer | |
| 305 | + | rts | |
| 306 | + | | |
| 307 | + | @game_over: | |
| 308 | + | lda #1 | |
| 309 | + | sta game_over | |
| 310 | + | rts | |
| 311 | + | | |
| 312 | + | ; ----------------------------------------------------------------------------- | |
| 313 | + | ; Update All Sprites | |
| 314 | + | ; ----------------------------------------------------------------------------- | |
| 315 | + | update_all_sprites: | |
| 316 | + | jsr update_player_sprite | |
| 317 | + | jsr update_enemy_sprites | |
| 318 | + | jsr update_lives_display | |
| 319 | + | rts | |
| 320 | + | | |
| 321 | + | ; ----------------------------------------------------------------------------- | |
| 322 | + | ; Update Player Sprite | |
| 245 | 323 | ; ----------------------------------------------------------------------------- | |
| 246 | 324 | update_player_sprite: | |
| 247 | - | ; Update position | |
| 248 | - | lda player_y | |
| 325 | + | ; Check if game over | |
| 326 | + | lda game_over | |
| 327 | + | beq @alive | |
| 328 | + | ; Hide player | |
| 329 | + | lda #$FF | |
| 249 | 330 | sta oam_buffer+0 | |
| 250 | - | lda player_x | |
| 251 | - | sta oam_buffer+3 | |
| 331 | + | rts | |
| 252 | 332 | | |
| 253 | - | ; Check if flashing | |
| 254 | - | lda flash_timer | |
| 333 | + | @alive: | |
| 334 | + | ; Check invulnerability flash | |
| 335 | + | lda invuln_timer | |
| 255 | 336 | beq @no_flash | |
| 256 | - | | |
| 257 | - | ; Decrement timer | |
| 258 | - | dec flash_timer | |
| 259 | - | | |
| 260 | - | ; Flash by toggling visibility every 4 frames | |
| 261 | 337 | and #%00000100 | |
| 262 | - | beq @show_player | |
| 263 | - | ; Hide player (move off screen) | |
| 338 | + | beq @show | |
| 339 | + | ; Hide during flash | |
| 264 | 340 | lda #$FF | |
| 265 | 341 | sta oam_buffer+0 | |
| 266 | 342 | rts | |
| 267 | 343 | | |
| 268 | - | @show_player: | |
| 344 | + | @show: | |
| 345 | + | @no_flash: | |
| 269 | 346 | lda player_y | |
| 270 | 347 | sta oam_buffer+0 | |
| 348 | + | lda #SPRITE_PLAYER | |
| 349 | + | sta oam_buffer+1 | |
| 350 | + | lda #0 | |
| 351 | + | sta oam_buffer+2 | |
| 352 | + | lda player_x | |
| 353 | + | sta oam_buffer+3 | |
| 271 | 354 | rts | |
| 272 | 355 | | |
| 273 | - | @no_flash: | |
| 356 | + | ; ----------------------------------------------------------------------------- | |
| 357 | + | ; Update Enemy Sprites | |
| 358 | + | ; ----------------------------------------------------------------------------- | |
| 359 | + | update_enemy_sprites: | |
| 360 | + | ldx #0 | |
| 361 | + | ldy #4 ; Start after player sprite | |
| 362 | + | | |
| 363 | + | @loop: | |
| 364 | + | lda enemy_y, x | |
| 365 | + | sta oam_buffer, y | |
| 366 | + | iny | |
| 367 | + | lda #SPRITE_ENEMY | |
| 368 | + | sta oam_buffer, y | |
| 369 | + | iny | |
| 370 | + | lda #%00000001 | |
| 371 | + | sta oam_buffer, y | |
| 372 | + | iny | |
| 373 | + | lda enemy_x, x | |
| 374 | + | sta oam_buffer, y | |
| 375 | + | iny | |
| 376 | + | inx | |
| 377 | + | cpx #NUM_ENEMIES | |
| 378 | + | bne @loop | |
| 274 | 379 | rts | |
| 275 | 380 | | |
| 276 | 381 | ; ----------------------------------------------------------------------------- | |
| 277 | - | ; Initialise Enemies | |
| 382 | + | ; Update Lives Display - Show life icons at top of screen | |
| 278 | 383 | ; ----------------------------------------------------------------------------- | |
| 279 | - | init_enemies: | |
| 280 | - | lda #48 | |
| 281 | - | sta enemy_x+0 | |
| 282 | - | lda #48 | |
| 283 | - | sta enemy_y+0 | |
| 284 | - | lda #DIR_RIGHT | |
| 285 | - | sta enemy_dir_x+0 | |
| 286 | - | lda #DIR_DOWN | |
| 287 | - | sta enemy_dir_y+0 | |
| 384 | + | update_lives_display: | |
| 385 | + | ; OAM slots 20-31 for lives display (up to 3 lives) | |
| 386 | + | ldy #20 ; OAM offset | |
| 288 | 387 | | |
| 289 | - | lda #200 | |
| 290 | - | sta enemy_x+1 | |
| 291 | - | lda #48 | |
| 292 | - | sta enemy_y+1 | |
| 293 | - | lda #DIR_LEFT | |
| 294 | - | sta enemy_dir_x+1 | |
| 295 | - | lda #DIR_DOWN | |
| 296 | - | sta enemy_dir_y+1 | |
| 388 | + | ldx lives | |
| 389 | + | beq @hide_all_lives | |
| 297 | 390 | | |
| 298 | - | lda #48 | |
| 299 | - | sta enemy_x+2 | |
| 300 | - | lda #176 | |
| 301 | - | sta enemy_y+2 | |
| 302 | - | lda #DIR_RIGHT | |
| 303 | - | sta enemy_dir_x+2 | |
| 304 | - | lda #DIR_UP | |
| 305 | - | sta enemy_dir_y+2 | |
| 391 | + | ; Show life 1 | |
| 392 | + | lda #8 ; Y position (top of screen) | |
| 393 | + | sta oam_buffer, y | |
| 394 | + | iny | |
| 395 | + | lda #SPRITE_LIFE | |
| 396 | + | sta oam_buffer, y | |
| 397 | + | iny | |
| 398 | + | lda #0 | |
| 399 | + | sta oam_buffer, y | |
| 400 | + | iny | |
| 401 | + | lda #16 ; X position | |
| 402 | + | sta oam_buffer, y | |
| 403 | + | iny | |
| 306 | 404 | | |
| 307 | - | lda #200 | |
| 308 | - | sta enemy_x+3 | |
| 309 | - | lda #176 | |
| 310 | - | sta enemy_y+3 | |
| 311 | - | lda #DIR_LEFT | |
| 312 | - | sta enemy_dir_x+3 | |
| 313 | - | lda #DIR_UP | |
| 314 | - | sta enemy_dir_y+3 | |
| 405 | + | cpx #1 | |
| 406 | + | beq @hide_rest | |
| 407 | + | | |
| 408 | + | ; Show life 2 | |
| 409 | + | lda #8 | |
| 410 | + | sta oam_buffer, y | |
| 411 | + | iny | |
| 412 | + | lda #SPRITE_LIFE | |
| 413 | + | sta oam_buffer, y | |
| 414 | + | iny | |
| 415 | + | lda #0 | |
| 416 | + | sta oam_buffer, y | |
| 417 | + | iny | |
| 418 | + | lda #26 | |
| 419 | + | sta oam_buffer, y | |
| 420 | + | iny | |
| 421 | + | | |
| 422 | + | cpx #2 | |
| 423 | + | beq @hide_rest | |
| 424 | + | | |
| 425 | + | ; Show life 3 | |
| 426 | + | lda #8 | |
| 427 | + | sta oam_buffer, y | |
| 428 | + | iny | |
| 429 | + | lda #SPRITE_LIFE | |
| 430 | + | sta oam_buffer, y | |
| 431 | + | iny | |
| 432 | + | lda #0 | |
| 433 | + | sta oam_buffer, y | |
| 434 | + | iny | |
| 435 | + | lda #36 | |
| 436 | + | sta oam_buffer, y | |
| 437 | + | iny | |
| 438 | + | rts | |
| 439 | + | | |
| 440 | + | @hide_rest: | |
| 441 | + | ; Hide remaining life slots | |
| 442 | + | @hide_loop: | |
| 443 | + | cpy #32 | |
| 444 | + | bcs @done | |
| 445 | + | lda #$FF | |
| 446 | + | sta oam_buffer, y | |
| 447 | + | iny | |
| 448 | + | iny | |
| 449 | + | iny | |
| 450 | + | iny | |
| 451 | + | jmp @hide_loop | |
| 452 | + | | |
| 453 | + | @hide_all_lives: | |
| 454 | + | ldy #20 | |
| 455 | + | jmp @hide_loop | |
| 315 | 456 | | |
| 457 | + | @done: | |
| 316 | 458 | rts | |
| 317 | 459 | | |
| 318 | 460 | ; ----------------------------------------------------------------------------- | |
| ... | |||
| 373 | 515 | inx | |
| 374 | 516 | cpx #NUM_ENEMIES | |
| 375 | 517 | bne @enemy_loop | |
| 376 | - | rts | |
| 377 | - | | |
| 378 | - | ; ----------------------------------------------------------------------------- | |
| 379 | - | ; Update Enemy Sprites | |
| 380 | - | ; ----------------------------------------------------------------------------- | |
| 381 | - | update_enemy_sprites: | |
| 382 | - | ldx #0 | |
| 383 | - | ldy #4 | |
| 384 | - | | |
| 385 | - | @loop: | |
| 386 | - | lda enemy_y, x | |
| 387 | - | sta oam_buffer, y | |
| 388 | - | iny | |
| 389 | - | lda #SPRITE_ENEMY | |
| 390 | - | sta oam_buffer, y | |
| 391 | - | iny | |
| 392 | - | lda #%00000001 | |
| 393 | - | sta oam_buffer, y | |
| 394 | - | iny | |
| 395 | - | lda enemy_x, x | |
| 396 | - | sta oam_buffer, y | |
| 397 | - | iny | |
| 398 | - | inx | |
| 399 | - | cpx #NUM_ENEMIES | |
| 400 | - | bne @loop | |
| 401 | 518 | rts | |
| 402 | 519 | | |
| 403 | 520 | ; ----------------------------------------------------------------------------- | |
| ... | |||
| 535 | 652 | bne @attr_top | |
| 536 | 653 | | |
| 537 | 654 | ldx #6 | |
| 538 | - | @attr_floor_rows: | |
| 655 | + | @attr_floor: | |
| 539 | 656 | lda #$00 | |
| 540 | 657 | sta PPUDATA | |
| 541 | 658 | lda #%01010101 | |
| ... | |||
| 548 | 665 | lda #$00 | |
| 549 | 666 | sta PPUDATA | |
| 550 | 667 | dex | |
| 551 | - | bne @attr_floor_rows | |
| 668 | + | bne @attr_floor | |
| 552 | 669 | | |
| 553 | 670 | ldx #8 | |
| 554 | 671 | lda #$00 | |
| ... | |||
| 661 | 778 | .byte BG_COLOUR, $11, $21, $31 | |
| 662 | 779 | .byte BG_COLOUR, $13, $23, $33 | |
| 663 | 780 | .byte BG_COLOUR, $19, $29, $39 | |
| 664 | - | .byte BG_COLOUR, $16, $26, $36 | |
| 665 | - | .byte BG_COLOUR, $30, $27, $17 | |
| 666 | 781 | .byte BG_COLOUR, $16, $26, $36 | |
| 782 | + | .byte BG_COLOUR, $30, $27, $17 ; Player + lives | |
| 783 | + | .byte BG_COLOUR, $16, $26, $36 ; Enemies | |
| 667 | 784 | .byte BG_COLOUR, $30, $27, $17 | |
| 668 | 785 | .byte BG_COLOUR, $30, $27, $17 | |
| 669 | 786 | | |
| ... | |||
| 731 | 848 | .byte %11111111,%01111110,%00111100,%00011000 | |
| 732 | 849 | .byte %00000000,%00011000,%00100100,%01000010 | |
| 733 | 850 | .byte %01000010,%00100100,%00011000,%00000000 | |
| 851 | + | | |
| 852 | + | ; Tile 9: Life icon (small heart/ship) | |
| 853 | + | .byte %00000000,%00100100,%01111110,%01111110 | |
| 854 | + | .byte %00111100,%00011000,%00000000,%00000000 | |
| 855 | + | .byte %00000000,%00000000,%00000000,%00100100 | |
| 856 | + | .byte %00011000,%00000000,%00000000,%00000000 | |
| 734 | 857 | | |
| 735 | 858 | ; Fill rest | |
| 736 | - | .res 8192 - 144, $00 | |
| 859 | + | .res 8192 - 160, $00 | |
| 737 | 860 | |