aboutsummaryrefslogtreecommitdiff
path: root/src/bullets.s
diff options
context:
space:
mode:
Diffstat (limited to 'src/bullets.s')
-rw-r--r--src/bullets.s303
1 files changed, 303 insertions, 0 deletions
diff --git a/src/bullets.s b/src/bullets.s
new file mode 100644
index 0000000..ea224b3
--- /dev/null
+++ b/src/bullets.s
@@ -0,0 +1,303 @@
+.segment "CODE"
+
+;; Function and variables which deal with the pool of bullets that the
+;; `driver.s` will use in order to render and deal with bullets on screen.
+.scope Bullets
+ ;; Maximum amount of bullets allowed on screen at the same time.
+ BULLETS_POOL_CAPACITY = 20
+
+ ;; Base address for the pool of bullets used on this game. The pool has
+ ;; #BULLETS_POOL_CAPACITY bullet objects where each one is 3 bytes long:
+ ;; 1. State: which can have two formats:
+ ;; - $FF: the bullet is not active.
+ ;; - |Dxxx|xxxx|: where D is the direction bit (1: right; 0: left); and
+ ;; the rest of bits count the number of moves from this
+ ;; bullet.
+ ;; 2. Y coordinate.
+ ;; 3. X coordinate.
+ zp_bullets_pool_base = $A0
+
+ ;; The screen coordinates of the bullet being inspected right now. Used when
+ ;; computing the move of bullets and checking possible collisions with
+ ;; background/enemies.
+ zp_current_bullet_y = $A1
+ zp_current_bullet_x = $A2
+
+ ;; The capacity of the bullets pool in bytes.
+ BULLETS_POOL_CAPACITY_BYTES = BULLETS_POOL_CAPACITY * 3
+
+ ;; The current amount of bullets on screen.
+ zp_bullets_pool_size = $E0
+
+ ;; The index on the pool where the next bullet can start iterating from.
+ ;; This is a small optimization so not to start from the beginning every
+ ;; time, as consecutive allocation is a very common case.
+ zp_last_allocated_index = $E1
+
+ ;; The amount of time we are not allowing B presses. This is a rather low
+ ;; value so you can have quite some presses per frame.
+ zp_bullet_timer = $35
+ BULLET_TIMER_VALUE = HZ / 15
+
+ ;; Maximum moves that a bullet can do. The tile also transitions depending
+ ;; on the moves done so far.
+ BULLET_MAX_MOVES = 26
+ BULLET_FIRST_TRANSITION = 20
+ BULLET_LAST_TRANSITION = 25
+
+ ;; Velocity at which bullets move.
+ .ifdef PAL
+ BULLET_VELOCITY = 7
+ .else
+ BULLET_VELOCITY = 6
+ .endif
+
+ ;; Initialize the pool of bullets.
+ .proc init
+ lda #0
+ sta zp_bullet_timer
+ sta zp_bullets_pool_size
+ sta zp_last_allocated_index
+ sta zp_current_bullet_y
+ sta zp_current_bullet_x
+
+ ;; Initializing the pool is a matter of setting to $FF the state byte
+ ;; for each bullet object.
+ ldx #0
+ ldy #0
+ lda #$FF
+ @pool_init_loop:
+ sta zp_bullets_pool_base, x
+ inx
+ inx
+ inx
+
+ iny
+ cpy #BULLETS_POOL_CAPACITY
+ bne @pool_init_loop
+
+ rts
+ .endproc
+
+ ;; Update the status of the pool by doing mainly three things:
+ ;; 1. Create a new bullet if the player can and has requested it.
+ ;; 2. Move all active bullets.
+ ;; 3. Check background/enemy collisions.
+ .proc update
+ ;; Are we already full of bullets on screen? If so go move them.
+ lda zp_bullets_pool_size
+ cmp #BULLETS_POOL_CAPACITY
+ beq @move_bullets
+
+ ;; Can the B button be pressed? If not go to `@move_bullets` directly.
+ lda zp_bullet_timer
+ beq @check_bullets_pressed
+ dec zp_bullet_timer
+ jmp @move_bullets
+
+ @check_bullets_pressed:
+ ;; Is the B button pressed? If not go to `@move_bullets` directly.
+ lda #(Joypad::BUTTON_B)
+ and Joypad::zp_buttons1
+ beq @move_bullets
+
+ ;; The B button was pressed. Reset the bullet timer.
+ lda #BULLET_TIMER_VALUE
+ sta zp_bullet_timer
+
+ ;; Let's fetch a free spot for the new bullet. Note that since we have
+ ;; checked that the pools size is not the same as the capacity, there
+ ;; *must* be a free spot. If that's not the case and we get into an
+ ;; infinite loop, then that's a bug we have to fix :)
+ ldx zp_last_allocated_index
+ @find_free_bullet_bucket:
+ lda zp_bullets_pool_base, x
+ cmp #$FF
+ beq @initialize_bucket
+
+ @next_free_loop:
+ ;; Prepare the `x` register for the next iteration. Notice that if we
+ ;; are over the total size in memory, we have to roll the `x` back to
+ ;; zero. This is possible because the loop starts at
+ ;; `zp_last_allocated_index`, which is not necessarily 0.
+ inx
+ inx
+ inx
+ cpx #BULLETS_POOL_CAPACITY_BYTES
+ bne @find_free_bullet_bucket
+ ldx #0
+ beq @find_free_bullet_bucket
+
+ @initialize_bucket:
+ ;; We found a free bucket. Initialize the first byte to 0 since it has
+ ;; not moved yet. The heading is taken from the player's state.
+ lda Player::zp_state
+ asl
+ and #%10000000
+ sta zp_bullets_pool_base, x
+
+ ;; Set the Y coordinate to the player's waist.
+ inx
+ lda Player::zp_screen_y
+ clc
+ adc #(Player::PLAYER_WAIST - 1)
+ sta zp_bullets_pool_base, x
+
+ ;; Set the X coordinate to the player while also adjusting to the future
+ ;; velocity applied on `@move_bullets` which, in turn, depends on the
+ ;; player's heading stored on `Player::zp_state`.
+ inx
+ lda Player::zp_screen_x
+ bit Player::zp_state
+ clc
+ bvc @set_bullet_left
+ adc #(Player::PLAYER_WIDTH - BULLET_VELOCITY)
+ jmp @set_bullet_x
+ @set_bullet_left:
+ adc #BULLET_VELOCITY
+ @set_bullet_x:
+ sta zp_bullets_pool_base, x
+
+ ;; Save the index so it can be used in future bullet creation.
+ inx
+ stx zp_last_allocated_index
+
+ ;; Increase the number of bullets on screen.
+ inc zp_bullets_pool_size
+
+ @move_bullets:
+ ;; We will have on the 'y' register the amount of bullets on screen
+ ;; pending to be moved. If there are none, we can return early.
+ ldy zp_bullets_pool_size
+ bne @do_move
+ rts
+
+ @do_move:
+ ;; There's at least one bullet to be moved. In this case, we will
+ ;; proceed to move any active bullet and check for collisions.
+ ;;
+ ;; The 'x' register will index the pool of bullets.
+ ldx #0
+
+ @move_loop:
+ ;; Is the current bullet active?
+ lda zp_bullets_pool_base, x
+ cmp #$FF
+ bne @move_active_bullet
+
+ ;; No, go for the next one.
+ inx
+ inx
+ inx
+ jmp @move_loop
+
+ @move_active_bullet:
+ ;; Store the original value into a temporary variable, and mask out the
+ ;; direction flag.
+ sta Globals::zp_tmp1
+ and #%01111111
+
+ ;; Ok, has this bullet moved to its maximum capacity?
+ cmp #BULLET_MAX_MOVES
+ bne @do_move_active_bullet
+
+ ;; Yes! Then mark it as over.
+ lda #$FF
+ sta zp_bullets_pool_base, x
+
+ ;; Decrease the number of bullets active and go check collisions if we
+ ;; are done checking for bullets. In this case, if this was the last
+ ;; bullet active, return early.
+ dec zp_bullets_pool_size
+ beq @end
+ dey
+ beq @end
+
+ ;; We still have active bullets to move, go to the next iteration.
+ inx
+ inx
+ inx
+ bne @move_loop
+
+ @do_move_active_bullet:
+ ;; Increase the number of moves that this bullet has done.
+ stx Globals::zp_idx
+ inc zp_bullets_pool_base, x
+
+ ;; Save the position on the Y axis as the value for the current bullet,
+ ;; then convert it into tile coordinates so it can be used later for
+ ;; background collision check.
+ lda zp_bullets_pool_base + 1, x
+ sta zp_current_bullet_y
+ lsr
+ lsr
+ lsr
+ sta Globals::zp_arg0
+
+ ;; Grab the position on the X axis and apply the velocity depending on
+ ;; the direction, which was stored back on the `Globals::zp_tmp1`
+ ;; variable.
+ lda zp_bullets_pool_base + 2, x
+ bit Globals::zp_tmp1
+ bmi @move_right
+ sec
+ sbc #BULLET_VELOCITY
+ jmp @collision_check
+ @move_right:
+ clc
+ adc #BULLET_VELOCITY
+
+ @collision_check:
+ ;; We now have the future value for the X axis. Store it as the current
+ ;; value and then convert it into tile coordinates so it can be used for
+ ;; background collision check.
+ sta zp_current_bullet_x
+ lsr
+ lsr
+ lsr
+ sta Globals::zp_arg1
+
+ ;; The actual check for background collision.
+ jsr Background::collides
+ beq @check_enemy_collision
+
+ ;; There was a collision! Disable the bullet.
+ ldx Globals::zp_idx
+ lda #$FF
+ sta zp_bullets_pool_base, x
+
+ ;; Decrement the number of bullets active.
+ dec zp_bullets_pool_size
+ beq @end
+ dey
+ beq @end
+
+ ;; And go for the next iteration.
+ inx
+ inx
+ inx
+ jmp @move_loop
+
+ @check_enemy_collision:
+ ;; TODO
+
+ @save_bullet_move:
+ ;; Restore back the old value from the 'x' register.
+ ldx Globals::zp_idx
+ inx
+ inx
+
+ ;; Store the new value for the X axis, increment the 'x' register and
+ ;; decrease the number of active bullets to be moved. If we are already
+ ;; into no bullets to be moved, then fall through and consider
+ ;; collisions.
+ lda zp_current_bullet_x
+ sta zp_bullets_pool_base, x
+ inx
+ dey
+ bne @move_loop
+
+ @end:
+ rts
+ .endproc
+.endscope