From 9ae51a4c210b8f01718e21eda592c498715a642b Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Wed, 11 Mar 2026 23:44:13 +0100 Subject: Initial implementation for items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This now only supports the appearance of shuttle parts and the fact that the player can collect them at a very specific order and drop them so to stack up the final shuttle. This is of course just the skeleton and there's a bunch of TODO's left. Signed-off-by: Miquel Sabaté Solà --- src/assets.s | 2 +- src/driver.s | 42 +++- src/interrupts.s | 18 +- src/items.s | 583 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/jetpac.s | 1 + src/over.s | 47 +++++ src/player.s | 2 + 7 files changed, 682 insertions(+), 13 deletions(-) create mode 100644 src/items.s (limited to 'src') diff --git a/src/assets.s b/src/assets.s index 139ed5f..18d6192 100644 --- a/src/assets.s +++ b/src/assets.s @@ -164,7 +164,7 @@ palettes: ;; Background ;; 0: score - .byte $0F, $30, $2C, $28 + .byte $0F, $30, $30, $28 ;; 1: floating platforms .byte $0F, $2C, $30, $2A ;; 2: ground diff --git a/src/driver.s b/src/driver.s index 7404b94..8b366b7 100644 --- a/src/driver.s +++ b/src/driver.s @@ -126,6 +126,9 @@ ora #%00001000 sta Player::zp_state + ;; Prepare the items for the scene. + jsr Items::prepare_scene + ;; Mark the state of the game as "game". That is, the player has ;; started. Also set the `ppu` flag and unset the `title over` one. lda #%01000001 @@ -159,6 +162,15 @@ dey bne @bullets_reset_loop + ;; Invalidate all items. + ldx #0 + ldy #Items::POOL_CAPACITY + @items_reset_loop: + sta Items::zp_pool_base, x + NEXT_ITEM_INDEX_X + dey + bne @items_reset_loop + ;; Set that we have done this operation so it's not done in future ;; cycles. lda #1 @@ -208,6 +220,7 @@ jsr Bullets::init jsr Enemies::init jsr Explosions::init + jsr Items::init ;; Initialize pause timer and some boolean values. lda #0 @@ -286,6 +299,7 @@ jsr Enemies::update @do_minimal_update: jsr Explosions::update + jsr Items::update ;; Has the player died? If it is dead, then we need to remove all ;; sprites except for objects and explosions, and whenever @@ -414,11 +428,24 @@ @set_next_enemies_cycle: stx zp_next_enemy_cycle - ;; TODO: ensure 1 item - ;; iny - ;; iny - ;; iny - ;; iny + ;; Allocate all valid items. Items, contrary to other sprites, don't get + ;; the special "you get a fixed first position" like others, mainly + ;; because there are so few of them on screen at any given time. For + ;; this reason as well, it's ok to just dump them all here before the + ;; rest of sprites are churned in. + ldx #0 + @rest_o_items: + cpx #Items::POOL_CAPACITY_BYTES + beq @rest_o_bullets + + lda Items::zp_pool_base, x + cmp #$FF + beq @next_item + jsr Items::allocate_x_y + + @next_item: + NEXT_ITEM_INDEX_X + jmp @rest_o_items ;; Allocate the rest of valid bullets from the pool. ldx #0 @@ -483,7 +510,7 @@ ;; enemies, then jump to items. If the current indexed enemy is the one ;; we allocated as the first fixed one, then skip it. cpx #Enemies::ENEMIES_POOL_CAPACITY_BYTES - beq @rest_o_items + beq @do_explosions cpx zp_first_enemy beq @next_enemy @@ -506,9 +533,6 @@ NEXT_ENEMY_INDEX_X jmp @rest_o_enemies_loop - @rest_o_items: - ;;; TODO - ;; At the very end, we allocate any active explosion. @do_explosions: ldx #0 diff --git a/src/interrupts.s b/src/interrupts.s index c5eb306..81709ed 100644 --- a/src/interrupts.s +++ b/src/interrupts.s @@ -62,7 +62,7 @@ ;; Do we need to update the lifes from players on the HUD? lda Player::zp_state and #%00001000 - beq @global_flags + beq @shuttle_update ;; Yeah! @@ -97,9 +97,21 @@ and #%11110111 sta Player::zp_state -@global_flags: - ;; TODO: some actions here will depend on the status of the game... +@shuttle_update: + ;; Should the shuttle be updated? lda Globals::zp_flags + tax + and #%00100000 + beq @game_status + jsr Items::update_shuttle + + ;; And unset the flag. + lda Globals::zp_flags + and #%11011111 + sta Globals::zp_flags + +@game_status: + txa and #%00000001 bne @ppu_registers diff --git a/src/items.s b/src/items.s new file mode 100644 index 0000000..c66e293 --- /dev/null +++ b/src/items.s @@ -0,0 +1,583 @@ +.segment "CODE" + +;; Assuming that the 'x' register indexes an item on its pool, increment the +;; register as many times as to point to the next one. Bound checking is not +;; performed, it's up to the caller to implement that. +.macro NEXT_ITEM_INDEX_X + inx + inx + inx +.endmacro + +.scope Items + ;; Maximum amount of items allowed on screen at the same time. + POOL_CAPACITY = 3 + + ;; The amount of bytes each pool item takes. + SIZEOF_POOL_ITEM = 3 + + ;; The capacity of the items pool in bytes. + POOL_CAPACITY_BYTES = POOL_CAPACITY * SIZEOF_POOL_ITEM + + ;; 1. State: $FF for invalid, otherwise: + ;; |PFD- CKKK|; where: + ;; | + ;; |- P: following the player + ;; |- F: falling. + ;; |- D: dropping: together with 'falling', but the player cannot re-grab it. + ;; |- C: 1: collectable (i.e. disappears on collision); 0: part (i.e. follows the player) + ;; |- K: object kind (00: high shuttle; 01: mid shuttle; 10: fuel; 11: regular item; 100: coin) + ;; + ;; 2. Y coordinate. + ;; 3. X coordinate. + zp_pool_base = $C0 ; asan:reserve POOL_CAPACITY_BYTES + + ;; Preserves the index on 'zp_pool_base' in Items::update(). + zp_pool_index = $C9 + + ;; TODO: stabilize and document. + ;; + ;; Y tile | X tile | palette + zp_current_tiles = $E7 ; asan:reserve POOL_CAPACITY_BYTES + + ;; + ;; TODO: stabilize and document. + ;; + ;; |G--- FFAA| + ;; | + ;; |- G: the player is grabbing an item + ;; |- F: number of falling items. + ;; |- A: number of active items. + zp_state = $CA + + ;; Number of shuttle parts (or fuel tanks) that have been collected so far. + zp_collected = $CB + + ;; Coordinate where the dropping of items takes place. This comes in two + ;; versions, as the "collision" is done in the tile coordinates so to give + ;; some leeway to the player; but the dropping itself has to fall from the + ;; exact screen coordinates or otherwise the dropping would feel weird.z + DROPPING_SCREEN_X = $A8 + DROPPING_TILE_X = $15 + + ;; Y screen coordinates in order for various parts to be considered as + ;; "collected". + MID_SHUTTLE_Y = $A7 + HIGH_SHUTTLE_Y = $97 + FUEL_SHUTTLE_Y = $C7 + + .proc init + lda Globals::zp_level_kind + bne @other_screens + JAL Items::init_first_screen + + @other_screens: + ;; TODO + + rts + .endproc + + ;; TODO: this is only to be done for the first time we enter. Otherwise this + ;; will be reset every time. + .proc init_first_screen + ;; We are going to allocate two shuttle parts, and hence two items. + lda #2 + sta Items::zp_state + + ;; We haven't collected anything yet, but it's convenient for us to mock + ;; that the ship part on the right side of the screen is actually + ;; collected. + lda #1 + sta Items::zp_collected + + ;; State of the top part of the shuttle. + ldx #0 + ldy #0 + sty Items::zp_pool_base, x + + ;; Screen and tile coordinates for the top part of the shuttle. + lda #$4F + sta Items::zp_pool_base + 1, x + lsr + lsr + lsr + sta Items::zp_current_tiles, x + lda #$29 + sta Items::zp_pool_base + 2, x + lsr + lsr + lsr + sta Items::zp_current_tiles + 1, x + + ;; State of the middle part of the shuttle. + iny + sty Items::zp_pool_base + 3, x + + ;; Screen and tile coordinates for the middle part of the shuttle. + lda #$67 + sta Items::zp_pool_base + 4, x + lsr + lsr + lsr + sta Items::zp_current_tiles + 3, x + lda #$81 + sta Items::zp_pool_base + 5, x + lsr + lsr + lsr + sta Items::zp_current_tiles + 4, x + + ;; Invalidte the third item. + ldy #$FF + sty Items::zp_pool_base + 6, x + + ;; Palettes. + lda #0 + sta Items::zp_current_tiles + 2, x + sta Items::zp_current_tiles + 6, x + + rts + .endproc + + ;; Allocate an item indexed by 'x' from the `zp_pool_base` buffer, and set + ;; it to OAM-reserved space indexed via 'y'. + ;; + ;; The 'y' register will be updated by increasing its value by 16, + ;; indicating the amount of bytes allocated in OAM space. + ;; + ;; The 'x' register will be _preserved_. + ;; + ;; NOTE: this function assumes that the item is in a valid state. That's up + ;; to the caller to check before calling this function. + .proc allocate_x_y + lda Items::zp_pool_base, x + and #$07 + + ;; Should we allocate a part from the shuttle? + bne @try_next_shuttle + lda #$04 + JAL allocate_shuttle_x_y + @try_next_shuttle: + cmp #$01 + bne @try_fuel + lda #$06 + JAL allocate_shuttle_x_y + + @try_fuel: + ;; TODO: validate whether we need to save/restore the 'x' register. + stx Globals::zp_tmp3 + ;; TODO + ldx Globals::zp_tmp3 + rts + .endproc + + ;; Allocate a shuttle part on the same terms as + ;; Items::allocate_shuttle_x_y(). + .proc allocate_shuttle_x_y + sta Globals::zp_tmp0 + + ;; Y coordinates + lda Items::zp_pool_base + 1, x + sta OAM::m_sprites, y ; top left + sta OAM::m_sprites + 4, y ; top right + clc + adc #8 + sta OAM::m_sprites + 8, y ; bottom left + sta OAM::m_sprites + 12, y ; bottom right + + ;; Tile IDs + lda Globals::zp_tmp0 + sta OAM::m_sprites + 1, y ; top left + clc + adc #1 + sta OAM::m_sprites + 5, y ; top right + + lda Globals::zp_tmp0 + clc + adc #$10 + sta OAM::m_sprites + 9, y ; bottom left + clc + adc #1 + sta OAM::m_sprites + 13, y ; bottom right + + ;; Attributes + lda #0 + sta OAM::m_sprites + 2, y ; top left + sta OAM::m_sprites + 6, y ; top right + sta OAM::m_sprites + 10, y ; bottom left + sta OAM::m_sprites + 14, y ; bottom right + + ;; X coordinates. + lda Items::zp_pool_base + 2, x ; top left + sta OAM::m_sprites + 3, y + sta OAM::m_sprites + 11, y ; bottom left + clc + adc #8 + sta OAM::m_sprites + 7, y ; top right + sta OAM::m_sprites + 15, y ; bottom right + + ;; And update the 'y' register. + tya + clc + adc #16 + tay + + rts + .endproc + + .proc update + ldx #0 + + ldy #POOL_CAPACITY + sty Globals::zp_idx + + ;; The player's coordinates are cached into arguments in memory so they + ;; can be used for collision checking. Note that we are targetting for + ;; the center of the player, which feels at a fair point for item + ;; interactions. + lda Player::zp_screen_y + clc + adc #Player::PLAYER_WAIST + lsr + lsr + lsr + sta Globals::zp_arg0 + lda Player::zp_screen_x + lsr + lsr + lsr + clc + adc #1 + sta Globals::zp_arg1 + + @loop: + ;; TODO: check how relevant this really is. + stx Items::zp_pool_index + + ;; Is it valid? + lda Items::zp_pool_base, x + cmp #$FF + bne @check_status + jmp @next + + @check_status: + ;; If it's resting, then just check for collision. Otherwise, we either + ;; fall/drop or follow the player. + and #$C0 + beq @check_collision + cmp #$40 + beq @do_fall + + ;;; + ;; Follow the player. + + ;; Neither of the above. Then, just follow the player. + lda Player::zp_screen_y + clc + adc #8 + sta Items::zp_pool_base + 1, x + lda Player::zp_screen_x + sta Items::zp_pool_base + 2, x + + ;; Are we at the zone where we must drop items? + ldy Globals::zp_arg1 + dey + cpy #DROPPING_TILE_X + beq @drop + jmp @next + + @drop: + ;; Yeah! Then the item stops being in 'following player' mode and is + ;; dropped (F & D set). + lda Items::zp_pool_base, x + and #$7F + ora #%01100000 + sta Items::zp_pool_base, x + + ;; Unset the 'grabbing' bit and increase the number of falling items. + lda Items::zp_state + and #$7F + clc + adc #$04 + sta Items::zp_state + + ;; And we force the item to be on the exact X screen position so to + ;; adjust from the player's subpixel movement. + lda #DROPPING_SCREEN_X + sta Items::zp_pool_base + 2, x + + jmp @next + + ;;; + ;; Fall/drop. + + @do_fall: + ;; Update the Y coordinate so the item is falling. + inc Items::zp_pool_base + 1, x + + ;; Is the item being dropped? If not, then we just check for collision. + lda Items::zp_pool_base, x + and #$20 + beq @check_collision + + ;; This is a fuel tank or a shuttle part that is aligned with the + ;; shuttle platform. We will load in 'a' the exact screen coordinates + ;; where each part should stop. + lda Items::zp_pool_base, x + and #$07 + beq @high_shuttle + cmp #1 + beq @mid_shuttle + lda #FUEL_SHUTTLE_Y + bne @drop_check + @mid_shuttle: + lda #MID_SHUTTLE_Y + bne @drop_check + @high_shuttle: + lda #HIGH_SHUTTLE_Y + + @drop_check: + ;; Does this item reach its dropping limit? If not just go to the next + ;; item. + ;; TODO: It should also work for "greater than". + cmp Items::zp_pool_base + 1, x + bne @next + + ;; Enable the 'ppu' and the 'shuttle' flags. + lda Globals::zp_flags + ora #%01100000 + sta Globals::zp_flags + + ;; Decrease the number of falling/active items. + lda Items::zp_state + sec + sbc #$05 ; NOTE: $04 (falling) + $01 (active) + sta Items::zp_state + + ;; Increase the number of collected items. + inc Items::zp_collected + + ;; And invalidate this item. + lda #$FF + sta Items::zp_pool_base, x + + ;; All collision checks that were needed for 'collision' mode have been + ;; done. We can just move to the next item. + jmp @next + + ;;; + ;; Collision checks. + + @check_collision: + ;; Collision with the player. + jsr Items::collides_with_player + beq @next + ;; TODO: background collision (when the item is not grabbed): if it + ;; happens, then the P, F, D are set to 0. The number of falling items + ;; is also decreased. + + ;; A collision happened! Get collected or follow the player (if possible). + lda Items::zp_pool_base, x + tay + and #$08 + beq @try_to_follow_player + jsr Items::collect + jmp @next + + @try_to_follow_player: + ;; If the player is already grabbing another item, don't even try it. + bit Items::zp_state + bmi @next + + ;; We don't need extra precautions except when the level kind is the + ;; first one. In that case we must guarantee the right shuttle order. + lda Globals::zp_level_kind + bne @do_follow_player + + ;; Is this the first shuttle part to be collected? + lda Items::zp_collected + cmp #1 + bne @do_follow_player + + ;; Yes! Then it _must_ be the middle part. + lda Items::zp_pool_base, x + and #$07 + cmp #$01 + bne @next + + @do_follow_player: + ;; TODO: If F was set, unset it and subtract the number of falling items. + + ;; Mark this item to be in 'following' mode. + tya + ora #$80 + sta Items::zp_pool_base, x + + ;; Mark the player's to be already grabbing an item. + lda Items::zp_state + ora #$80 + sta Items::zp_state + + @next: + NEXT_ITEM_INDEX_X + dec Globals::zp_idx + beq @end + jmp @loop + + @end: + rts + .endproc + + ;; TODO: this assumes a 4-sprite item + .proc collides_with_player + ldx Items::zp_pool_index + lda Items::zp_current_tiles, x + cmp #$FF + beq @no + + ;; Check for the Y tile coordinate. If it's not the same on either the + ;; upper or the bottom parts of the item, then it's a no. + cmp Globals::zp_arg0 + beq @check_x + clc + adc #1 + cmp Globals::zp_arg0 + bne @no + + @check_x: + ;; If the Y tile coordinate checks out, let's narrow it down to the X + ;; coordinate. + lda Items::zp_current_tiles + 1, x + cmp Globals::zp_arg1 + beq @yes + clc + adc #1 + cmp Globals::zp_arg1 + bne @no + + @yes: + lda #1 + rts + @no: + lda #0 + rts + .endproc + + ;; TODO: guarantee 'x' and 'y' safety + .proc collect + ;; TODO + rts + .endproc + + ;; Prepare the background scenary for items. Namely, the rocket parts which + ;; belong to the background. + ;; + ;; NOTE: this has to be called with the PPU disabled. + .proc prepare_scene + ;; The low part of the rocket. + bit PPU::m_status + lda #$2A + sta PPU::m_address + lda #$F5 + sta PPU::m_address + ldx #$0C + stx PPU::m_data + inx + stx PPU::m_data + + lda #$2B + sta PPU::m_address + lda #$15 + sta PPU::m_address + inx + stx PPU::m_data + inx + stx PPU::m_data + + lda Globals::zp_level_kind + beq @end + + @rest_of_the_rocket: + jsr draw_high_part_shuttle + jsr draw_middle_part_shuttle + + @end: + rts + .endproc + + ;; Update the background scenary for the shuttle. + ;; + ;; NOTE: this has to be called with the PPU disabled. + .proc update_shuttle + lda Globals::zp_level_kind + bne @fuel + + ;; Update the shuttle. + lda Items::zp_collected + cmp #3 + bne @mid_shuttle + jsr draw_high_part_shuttle + @mid_shuttle: + jsr draw_middle_part_shuttle + rts + + @fuel: + ;; TODO + + rts + .endproc + + ;; Update the background scenary to show the middle part of the shuttle. + ;; + ;; NOTE: this has to be called with the PPU disabled. + .proc draw_middle_part_shuttle + ldx #$08 + ldy #$2A + + bit PPU::m_status + sty PPU::m_address + lda #$B5 + sta PPU::m_address + stx PPU::m_data + inx + stx PPU::m_data + + bit PPU::m_status + sty PPU::m_address + lda #$D5 + sta PPU::m_address + inx + stx PPU::m_data + inx + stx PPU::m_data + + rts + .endproc + + ;; Update the background scenary to show the high part of the shuttle. + ;; + ;; NOTE: this has to be called with the PPU disabled. + .proc draw_high_part_shuttle + ldx #$04 + ldy #$2A + + bit PPU::m_status + sty PPU::m_address + lda #$75 + sta PPU::m_address + stx PPU::m_data + inx + stx PPU::m_data + + bit PPU::m_status + sty PPU::m_address + lda #$95 + sta PPU::m_address + inx + stx PPU::m_data + inx + stx PPU::m_data + + rts + .endproc +.endscope diff --git a/src/jetpac.s b/src/jetpac.s index 685c317..d2ed90b 100644 --- a/src/jetpac.s +++ b/src/jetpac.s @@ -45,6 +45,7 @@ .include "player.s" .include "enemies.s" .include "bullets.s" +.include "items.s" .include "title.s" .include "over.s" .include "driver.s" diff --git a/src/over.s b/src/over.s index 6d8ed8d..cb59ef8 100644 --- a/src/over.s +++ b/src/over.s @@ -144,6 +144,53 @@ dex bne @clear_ground_loop + ;; The low part of the rocket. + bit PPU::m_status + ldx #$2B + stx PPU::m_address + ldx #$15 + stx PPU::m_address + sta PPU::m_data + sta PPU::m_data + + ;; High part of the rocket. + bit PPU::m_status + ldy #$2A + sty PPU::m_address + ldx #$75 + stx PPU::m_address + sta PPU::m_data + sta PPU::m_data + + bit PPU::m_status + sty PPU::m_address + ldx #$95 + stx PPU::m_address + sta PPU::m_data + sta PPU::m_data + + bit PPU::m_status + sty PPU::m_address + ldx #$B5 + stx PPU::m_address + sta PPU::m_data + sta PPU::m_data + + ;; Middle part of the rocket. + bit PPU::m_status + sty PPU::m_address + ldx #$D5 + stx PPU::m_address + sta PPU::m_data + sta PPU::m_data + + bit PPU::m_status + sty PPU::m_address + ldx #$F5 + stx PPU::m_address + sta PPU::m_data + sta PPU::m_data + rts .endproc diff --git a/src/player.s b/src/player.s index 08f7872..48b0bb0 100644 --- a/src/player.s +++ b/src/player.s @@ -841,6 +841,8 @@ dec Player::zp_lifes, x bne @nmi_update + ;; TODO: if it was grabbing an item, turn it into 'falling' state. + ;; If this poor guy is over, then mark it in the multiplayer bitmap. cpx #0 bne @player_2_over -- cgit v1.2.3