aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/bullets.s303
-rw-r--r--src/driver.s162
-rw-r--r--src/jetpac.s1
-rw-r--r--src/player.s3
4 files changed, 467 insertions, 2 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
diff --git a/src/driver.s b/src/driver.s
index 4c2220f..c428475 100644
--- a/src/driver.s
+++ b/src/driver.s
@@ -18,7 +18,13 @@
PAUSE_TIMER_VALUE = (HZ / 3)
zp_pause_timer = $32
- ;; Switch from the title screen to the main street. Note that this function
+ ;; Number of sprites available for sprite cycling.
+ SPRITE_CYCLING_BYTES = (64 - Player::PLAYER_SPRITES_COUNT) * 4
+
+ ;; TODO
+ zp_next_bullet_cycle = $33
+
+ ;; Switch from the title screen to the main screen. Note that this function
;; is to be called with the PPU disabled. If that's not the case, then it
;; will set the proper values to disable it on the next `nmi` call and set
;; the `title over` flag. With that, call again this function so the
@@ -80,11 +86,15 @@
@load_player:
jsr Player::init
+ jsr Bullets::init
;; Initialize pause timer.
lda #0
sta zp_pause_timer
+ ;; Initialize variables for sprite cycling.
+ sta zp_next_bullet_cycle
+
@game:
;; Check if the player is toggling the `pause` state.
lda #(Joypad::BUTTON_START | Joypad::BUTTON_SELECT)
@@ -131,8 +141,156 @@
beq @do_update
rts
+ ;; This is the actual meat of the main game, which updates the state of
+ ;; the player, bullets, enemies, etc.
@do_update:
- JAL Player::update
+ jsr Player::update
+ jsr Bullets::update
+ JAL sprite_cycling
+ ;; TODO: fall through?
+ .endproc
+
+ .proc sprite_cycling
+ ;; The 'y' register will contain the index on OAM of the sprite to be
+ ;; allocated.
+ ldy #(Player::PLAYER_SPRITES_COUNT * 4)
+
+ ;; The 'x' register will index from the different sprite pools.
+ ldx zp_next_bullet_cycle
+ lda Bullets::zp_bullets_pool_base, x
+
+ ;; Is this a valid bullet?
+ cmp #$FF
+ beq @after_first_bullet
+
+ ;; It is a valid bullet! Set it now.
+ lda Bullets::zp_bullets_pool_base + 1, x
+ sta $200, y
+ iny
+
+ ;; The tile selection depends on how many moves the bullet has done.
+ lda Bullets::zp_bullets_pool_base, x
+ and #%01111111
+ cmp #Bullets::BULLET_LAST_TRANSITION
+ bcs @last_bullet_tile
+ cmp #Bullets::BULLET_FIRST_TRANSITION
+ bcs @mid_bullet_tile
+ lda #$0E
+ bne @set_bullet_tile
+ @mid_bullet_tile:
+ lda #$0F
+ bne @set_bullet_tile
+ @last_bullet_tile:
+ lda #$1E
+ @set_bullet_tile:
+ sta $200, y
+
+ iny
+ lda #0
+ sta $200, y
+ iny
+ lda Bullets::zp_bullets_pool_base + 2, x
+ sta $200, y
+ iny
+
+ @after_first_bullet:
+ ;; Save the index that was considered for the first bullet.
+ stx Globals::zp_tmp0
+
+ ;; Increase the index for the bullets cycling. If wrapping is detected,
+ ;; then it resets this value back to zero.
+ inx
+ inx
+ inx
+ cpx #Bullets::BULLETS_POOL_CAPACITY_BYTES
+ bne @set_next_bullets_cycle
+ ldx #0
+ @set_next_bullets_cycle:
+ stx zp_next_bullet_cycle
+
+ ;; TODO: ensure 1 enemy
+ iny
+ iny
+ iny
+ iny
+
+ ;; TODO: ensure 1 item
+ iny
+ iny
+ iny
+ iny
+
+ ;; TODO: rest of bullets
+ ldx #0
+ @rest_o_bullets:
+ cpx #Bullets::BULLETS_POOL_CAPACITY_BYTES
+ beq @rest_o_enemies
+ cpx Globals::zp_tmp0
+ bne @do_bullet
+ inx
+ inx
+ inx
+ cpx #Bullets::BULLETS_POOL_CAPACITY_BYTES
+ beq @rest_o_enemies
+ @do_bullet:
+ lda Bullets::zp_bullets_pool_base, x
+ cmp #$FF
+ beq @next_bullet
+
+ lda Bullets::zp_bullets_pool_base + 1, x
+ sta $200, y
+ iny
+
+ ;; The tile selection depends on how many moves the bullet has done.
+ lda Bullets::zp_bullets_pool_base, x
+ and #%01111111
+ cmp #Bullets::BULLET_LAST_TRANSITION
+ bcs @other_last_bullet_tile
+ cmp #Bullets::BULLET_FIRST_TRANSITION
+ bcs @other_mid_bullet_tile
+ lda #$0E
+ bne @other_set_bullet_tile
+ @other_mid_bullet_tile:
+ lda #$0F
+ bne @other_set_bullet_tile
+ @other_last_bullet_tile:
+ lda #$1E
+ @other_set_bullet_tile:
+ sta $200, y
+
+ iny
+ lda #0
+ sta $200, y
+ iny
+ lda Bullets::zp_bullets_pool_base + 2, x
+ sta $200, y
+ iny
+
+ @next_bullet:
+ inx
+ inx
+ inx
+ jmp @rest_o_bullets
+
+ @rest_o_enemies:
+ ;; TODO: rest of enemies
+ ;; TODO: rest of items
+
+ ;; We are done with all the sprites we wanted to allocat. Now let's
+ ;; clear out the rest of the slots just in case there was some leftover
+ ;; from a past sprite.
+ lda #$EF
+ @check_cycle_end:
+ cpy #SPRITE_CYCLING_BYTES
+ bne @reset_sprite
+ rts
+ @reset_sprite:
+ sta $200, y
+ iny
+ iny
+ iny
+ iny
+ jmp @check_cycle_end ;TODO: maybe just bne?
.endproc
.ifdef PAL
diff --git a/src/jetpac.s b/src/jetpac.s
index 10f6df3..0c75bf4 100644
--- a/src/jetpac.s
+++ b/src/jetpac.s
@@ -40,6 +40,7 @@
.include "assets.s"
.include "background.s"
.include "player.s"
+.include "bullets.s"
.include "title.s"
.include "driver.s"
.include "vectors.s"
diff --git a/src/player.s b/src/player.s
index aac5dd0..c7b65af 100644
--- a/src/player.s
+++ b/src/player.s
@@ -110,6 +110,9 @@
;; How many frames are we allowing for each walk animation state?
WALK_COUNTER_MAX = (HZ / 10)
+ ;; Number of sprites from which the player is made of.
+ PLAYER_SPRITES_COUNT = 6
+
;; Initialize the player's sprite. Note that for the sprite to look
;; correctly on screen you still need to call `Player::update` afterwards.
.proc init