;; Set the player to its initial coordinates.
.macro RESET_PLAYER_POSITION
    lda #40
    sta Player::zp_screen_x
    sta Player::zp_screen_y
.endmacro

;; This is the player code that gets re-used on all scrolling examples. This
;; will show a meta-sprite made up of four sprites that make up diskun. You can
;; move this player with the arrows, and the movement will be pretty basic (1
;; pixel at a time). Furthermore, the sprite handles collision with background
;; elements, which is shared across all the scrolling examples.
;;
;; The sprite is stored in OAM memory $204-$213 (hence sprite0 is left out so
;; other examples can use it), and the $30 and $31 memory addresses are used for
;; keeping up with the screen coordinates.
.scope Player
    ;; The minimum value that the player is allowed to have on the Y axis.
    MIN_SCREEN_Y = 8

    ;; The maximum value of X that is allowed before we start updating the
    ;; scroll value.
    MAX_SCREEN_X = 144

    ;; The height for the player when it comes to detecting the edge of the
    ;; screen or background collisions.
    PLAYER_HEIGHT = 25

    ;; The width for the player when it comes to detecting the edge of the
    ;; screen or background collisions.
    PLAYER_WIDTH = 16

    ;; Indeces for the collisions table as defined on `check_bg_collision_x` so
    ;; the setting of the `x` register is less magical.
    TOP_COLLISION_INDEX   = (0 << 2)
    RIGHT_COLLISION_INDEX = (1 << 2)
    DOWN_COLLISION_INDEX  = (2 << 2)
    LEFT_COLLISION_INDEX  = (3 << 2)

    ;; Coordinates on the X axis.
    ;; NOTE: shadowed in collision.s. If you change it here, change it there as
    ;; well.
    zp_screen_x = $30

    ;; Coordinates on the Y axis.
    ;; NOTE: shadowed in collision.s. If you change it here, change it there as
    ;; well.
    zp_screen_y = $31

    ;; Initialize the sprite for the player.
    .proc init
        RESET_PLAYER_POSITION

        lda #$01
        sta $205
        lda #$02
        sta $209
        lda #$11
        sta $20D
        lda #$12
        sta $211

        lda #0
        sta $206
        sta $20A
        sta $20E
        sta $212

        rts
    .endproc

    ;; The update for the player is simply about checking for the d-pad. Note
    ;; that if pressing right, then the scroll value might also move, which
    ;; might mean to load the next background column until the end of the
    ;; screen.
    .proc update
        ;; Is the player requesting to go up?
        lda #Joypad::BUTTON_UP
        and Joypad::zp_buttons1
        beq @check_down

        ;; If we are already at the top disregard this button press and check
        ;; for the left button.
        lda #MIN_SCREEN_Y
        cmp zp_screen_y
        beq @check_left

        ;; If, for whatever reason, we are below the minimum value, then reset
        ;; the value to this minimum Y value and jump to check the left button.
        bcc @update_y_up
        sta zp_screen_y
        jmp @check_left

    @update_y_up:
        ;; Try to go up and check for a collision with the background.
        dec zp_screen_y
        dec zp_screen_y
        ldx #TOP_COLLISION_INDEX
        jsr check_bg_collision_x
        beq @check_left

        ;; A collision was detected. Then get back to the old value of Y and
        ;; move on.
        inc zp_screen_y
        inc zp_screen_y
        jmp @check_left

    @check_down:
        ;; Is the player requesting to go down?
        lda #Joypad::BUTTON_DOWN
        and Joypad::zp_buttons1
        beq @check_left

        ;; We have to move down unless we are already at the very bottom.
        lda zp_screen_y
        cmp #(240 - PLAYER_HEIGHT)
        bcc @update_y_down
        lda #(240 - PLAYER_HEIGHT)
        sta zp_screen_y
        jmp @check_left
    @update_y_down:
        ;; Try to go down as requested, but check for a collision with the
        ;; background.
        inc zp_screen_y
        inc zp_screen_y
        ldx #DOWN_COLLISION_INDEX
        jsr check_bg_collision_x
        beq @check_left

        ;; There was a collision with the background, restore back the value on
        ;; Y and move on.
        dec zp_screen_y
        dec zp_screen_y

    @check_left:
        ;; Is the player requesting to go left?
        lda #Joypad::BUTTON_LEFT
        and Joypad::zp_buttons1
        beq @check_right

        ;; We have to move left unless we are already at the leftmost edge.
        lda zp_screen_x
        bne :+
        rts
    :
        ;; We are not at the leftmost edge, try to go left while also checking
        ;; for a background collision.
        dec zp_screen_x
        dec zp_screen_x
        ldx #LEFT_COLLISION_INDEX
        jsr check_bg_collision_x
        bne :+
        rts
    :
        ;; There was a collision, restore back the value on X and quit.
        inc zp_screen_x
        inc zp_screen_x
        rts

    @check_right:
        ;; Last check! Is the player requesting to go right?
        lda #Joypad::BUTTON_RIGHT
        and Joypad::zp_buttons1
        bne @check_level_end
        rts

    @check_level_end:
        ;; Are we at the last screen? If so consume the rest of the missing
        ;; scroll. Whenever that is done (i.e. there are no more screens and the
        ;; scroll sits at a zero value), then we can no longer load more columns
        ;; or update the scroll: stick to updating the X position.
        lda #%00001000
        and Globals::zp_flags
        beq @check_max_screen
        lda Background::zp_scroll
        beq @update_x

    @check_max_screen:
        ;; Is the X position already at the MAX_SCREEN_X? If so, then we don't
        ;; move the character: we move the scroll instead.
        lda zp_screen_x
        cmp #Player::MAX_SCREEN_X
        bcc @update_x

        ;; Moving will then be a matter of increasing the scroll value.
        inc Background::zp_scroll
        inc Background::zp_scroll
        ldx #RIGHT_COLLISION_INDEX
        jsr check_bg_collision_x
        bne @collision_on_scroll

        ;; If there are no collisions in the scrolling scenario, then we might
        ;; need to load the next background column in advance. Whether that is
        ;; possible or something that we want to do is handled automatically by
        ;; the `Background::load_next_background` function. Hence, call this
        ;; function and quit.
        JAL Background::load_next_background

    @collision_on_scroll:
        ;; Collision was given. Hence, restore the value for the scroll and
        ;; quit.
        dec Background::zp_scroll
        dec Background::zp_scroll
        rts

    @update_x:
        ;; Do not overwrap the screen.
        lda zp_screen_x
        cmp #(256 - PLAYER_WIDTH)
        beq @end

        ;; We are not about to overwrap the screen, let's update the value on
        ;; the X axis and check whether a collision would happen.
        inc zp_screen_x
        inc zp_screen_x
        ldx #RIGHT_COLLISION_INDEX
        jsr check_bg_collision_x
        beq @end

        ;; There was a collision, roll back the value for the X axis.
        dec zp_screen_x
        dec zp_screen_x

    @end:
        rts
    .endproc

    ;; Check collision between the player and any background element. Note that
    ;; collision is not tested on all edges for the player, but you have to
    ;; provide a "collision index", which refers to which edges to test. See the
    ;; "*_COLLISION_INDEX" constants above and the `edges` data included inside
    ;; of this function. This collision index has to be provided on the `x`
    ;; register.
    ;;
    ;; Returns a non-zero value on the `a` register if a collision was detected,
    ;; zero otherwise.
    .proc check_bg_collision_x
        ;; We have to check for a collision on both the left and the right
        ;; edges, which should follow the same code. Unrolling it is simply
        ;; faster and more clear than setting up a loop.
        .repeat 2, I
            ;; Save the offset on the Y coordinate.
            lda edges, x
            sta Globals::zp_arg0

            ;; Save the offset on the X coordinate.
            inx
            lda edges, x
            sta Globals::zp_arg1

            ;; Save the edge index in preparation for the following calls.
            .if I = 0
                inx
                stx Globals::zp_idx
            .endif

            ;; Transform screen coordinates into metatile ones and fetch the
            ;; collision bitmap for it with its mask.
            jsr Collision::screen_to_mt_coordinates
            jsr Collision::get_background_collision_y

            ;; Restore back the edge index.
            .if I = 0
                ldx Globals::zp_idx
            .endif

            ;; Checking for a collision is as simple as performing an AND with the
            ;; bitmap at the `a` register and the byte that can be addressed with
            ;; the given pointer. If the result is non-zero, then we found a match.
            and (Globals::zp_arg0), y
            .if I = 0
                ;; If I = 1 we are falling through as expected anyways, so this
                ;; instruction is not needed.
                bne @collision
            .endif
        .endrepeat

        ;; The last instruction before ending up here will already have the `a`
        ;; register lined up to the proper return value. If a collision was
        ;; found (in either left/right case), `bne @collision` is good, and
        ;; hence `a` has an expected non-zero value. The same is true when not
        ;; in a collision.
    @collision:
        rts

    edges:
        ;; Up: top-left (y, x), top-right (y, x)
        .byte $02, $02, $02, $0F

        ;; Right: top-right (y, x), bottom-right (y, x)
        .byte $02, $0F, $0F, $0F

        ;; Down: bottom-left (y, x), bottom-right (y, x)
        .byte $0F, $02, $0F, $0F

        ;; Left: top-left (y, x), bottom-left (y, x)
        .byte $02, $01, $0F, $01
    .endproc

    ;; Update the memory on the PPU with the current values for the screen
    ;; coordinates of the player.
    ;;
    ;; NOTE: only call this function on NMI Code.
    .proc update_sprite
        lda zp_screen_x
        sta $207
        sta $20F
        clc
        adc #8
        sta $20B
        sta $213

        lda zp_screen_y
        sta $204
        sta $208
        clc
        adc #8
        sta $20C
        sta $210

        rts
    .endproc
.endscope
