#include "Player.h"

Player player;
u8 playerRedraw = TRUE;

#define INITIAL_JUMP_VELOCITY_ -9
#define FALL_GRAVITY_ 1
#define TERMINAL_VELOCITY_ 14

// Used for ensure the current is not too fast.
u8 currentCounter_ = 0;

void initPlayer(u8 x, u8 y)
{
    u8 roomX = x / SCREEN_TILES_X;
    u8 roomY = y / SCREEN_TILES_Y;

    player.health = playerStartingHealth;
    player.object.position.x = (x % SCREEN_TILES_X) * TILE_WIDTH_BYTES;
    player.object.position.y = (y % SCREEN_TILES_Y) * TILE_HEIGHT;
    player.object.previousPosition.x = player.object.position.x;
    player.object.previousPosition.y = player.object.position.y;
    player.object.position.room = roomX + roomY * 3;
    playerRedraw = TRUE;
    player.movementMode = MOVEMENT_MODE_LAND;
    player.state = PLAYER_STATE_WALKING;
    player.projectileAmmo = 0;
    player.object.width = TILE_WIDTH_BYTES;
    player.object.height = TILE_HEIGHT;
    player.oxygen = 0;
    player.object.direction = DIRECTION_RIGHT;
    player.pickCount = 0;

    // Initialise the projectile to inactive.
    player.projectile.health = 0;

    // The current room is the room that the player is in.
    currentRoom = player.object.position.room;
    currentRoomX = currentRoom % 3;
    currentRoomY = currentRoom / 3;

    // Initialise the player graphic to the player land sprite.
    updatePlayerSprite(MOVEMENT_MODE_LAND);
}

void drawPlayer()
{
    if (playerRedraw)
    {
        drawActor(&player.object, player.playerSprite);

        if (roomChange)
        {
            roomChange = FALSE;
        }

        playerRedraw = FALSE;
    }

    drawProjectile(&player.projectile);
}

void movePlayer()
{
    // Update the player velocity based on controls currently down.
    if (gameControls.controlFlags & CONTROL_LEFT_FLAG)
    {
        player.object.velocity.x = -1;
    }

    if (gameControls.controlFlags & CONTROL_RIGHT_FLAG)
    {
        player.object.velocity.x = 1;
    }

    if (player.movementMode == MOVEMENT_MODE_WATER)
    {
        if (gameControls.controlFlags & CONTROL_UP_FLAG)
        {
            player.object.velocity.y = -1;
        }

        if (gameControls.controlFlags & CONTROL_DOWN_FLAG)
        {
            player.object.velocity.y = 1;
        }
    }

    if (gameControls.controlFlags & CONTROL_JUMP_FLAG)
    {
        // Only jump from walking state as we don't want multijump.
        if (player.state == PLAYER_STATE_WALKING)
        {
            player.state = PLAYER_STATE_JUMPING;

            // Set initial jump velocity.
            player.object.velocity.y = INITIAL_JUMP_VELOCITY_;
        }
        else if (player.state == PLAYER_STATE_WATER)
        {
            // This allows us to jump in the water to move faster but it might be overpowered.
            player.object.velocity.y = -3;
        }
    }

    if (gameControls.controlFlags & CONTROL_SHOOT_FLAG)
    {
        // Only launch a projectile if one has not previously been launched.
        if (!player.projectile.health)
        {
            // Ensure the player has a weapon with enough ammo and that the delay after the previous projectile has filed has been completed.
            // The delay ensures that the player cannot use all their shots too quickly such as shooting right next to a solid block.
            if (inventoryContainsItem(ITEM_PROJECTILE_WEAPON) && player.projectileAmmo && !projectileDestroyedTimerCount)
            {
                // Initialise the projectile to the player's current position.
                initProjectile(&player.projectile, player.object.position.x, player.object.position.y, player.object.direction);

                // Play weapon shot sound on channel A.
                playSoundEffect(SFX_WEAPON_SHOT, CHANNEL_A, MAX_VOL);

                // Reduce the player ammo and redraw the hud.
                --player.projectileAmmo;
                hudRedraw = TRUE;
            }
        }
    }
}

void logicPlayer()
{
    u8 interactableActivated = FALSE;

    // The tiles that the player currently occupies.
    u8 occupiedXTileLeft = (player.object.position.x) >> 2;
    u8 occupiedXTileRight = (player.object.position.x + TILE_WIDTH_BYTES - 1);
    u8 occupiedYTileBottom = (player.object.position.y + TILE_HEIGHT - 1);
    u8 occupiedYTileTop = (player.object.position.y) >> 4;
    occupiedXTileRight >>= 2;
    occupiedYTileBottom >>= 4;

    // Check if the player is submerged in water.
    if (MAPA(occupiedXTileLeft, occupiedYTileTop) & CELL_WATER_FLAG ||
        MAPA(occupiedXTileRight, occupiedYTileTop) & CELL_WATER_FLAG ||
        MAPA(occupiedXTileLeft, occupiedYTileBottom) & CELL_WATER_FLAG ||
        MAPA(occupiedXTileRight, occupiedYTileBottom) & CELL_WATER_FLAG)
    {
        // TODO use more cells to work out water type.
        // Retrieve all the bits (except water flag) so that we can discount the type if it is an item or interactable.
        u8 waterType = MAPA(occupiedXTileLeft, occupiedYTileTop) & 127;

        // Only change to water mode if the player has a scuba tank.
        if (inventoryContainsItem(ITEM_SCUBA))
        {
            player.movementMode = MOVEMENT_MODE_WATER;
            player.state = PLAYER_STATE_WATER;
            updatePlayerSprite(MOVEMENT_MODE_WATER);
        }

        // The cell is either an item (32) or an interactable (64) so it will not have a water type on the cell.
        if (waterType > 31)
        {
            waterType = 0;
        }

        if (waterType)
        {
            if (currentCounter_++ > 1)
            {
                if (waterType == WATER_TYPE_LEFT_CURRENT)
                {
                    // Add x velocity (left) for a left water type.
                    player.object.velocity.x -= 1;
                }
                else if (waterType == WATER_TYPE_FAST_LEFT_CURRENT)
                {
                    // Add x velocity (left) for a fast left water type.
                    player.object.velocity.x -= 2;
                }
                else if (waterType == WATER_TYPE_RIGHT_CURRENT)
                {
                    // Add x velocity (right) for a right water type.
                    player.object.velocity.x += 1;
                }
                else if (waterType == WATER_TYPE_FAST_RIGHT_CURRENT)
                {
                    // Add x velocity (right) for a fast right water type.
                    player.object.velocity.x += 2;
                }
                else if (waterType == WATER_TYPE_UP_CURRENT)
                {
                    // Set the movement mode and state so the player it isn't falling.
                    player.movementMode = MOVEMENT_MODE_WATER;
                    player.state = PLAYER_STATE_WATER;

                    // Add y velocity (up) for an up water type.
                    player.object.velocity.y -= 2;
                }
                else if (waterType == WATER_TYPE_FAST_UP_CURRENT)
                {
                    // Set the movement mode and state so the player it isn't falling.
                    player.movementMode = MOVEMENT_MODE_WATER;
                    player.state = PLAYER_STATE_WATER;

                    // Add y velocity (up) for a fast up water type.
                    player.object.velocity.y -= 6;
                }
                else if (waterType == WATER_TYPE_DOWN_CURRENT)
                {
                    // Add y velocity (down) for a down water type.
                    // This needs to be more than 1 otherwise it is too slow.
                    player.object.velocity.y += 3;

                    // Make sure not to go faster than terminal velocity.
                    // Otherwise there is trouble with collision detection.
                    if (player.object.velocity.y > TERMINAL_VELOCITY_)
                    {
                        player.object.velocity.y = TERMINAL_VELOCITY_;
                    }

                    // Don't apply fall gravity when carrying scuba.
                    if (!inventoryContainsItem(ITEM_SCUBA))
                    {
                        // Make sure the player is falling otherwise player will move incorrectly or erratically.
                        player.state = PLAYER_STATE_FALLING;
                    }
                }
                else if (waterType == WATER_TYPE_FAST_DOWN_CURRENT)
                {
                    // Add y velocity (down) for a fast down water type.
                    player.object.velocity.y += 5;

                    // Make sure not to go faster than terminal velocity.
                    // Otherwise there is trouble with collision detection.
                    if (player.object.velocity.y > TERMINAL_VELOCITY_)
                    {
                        player.object.velocity.y = TERMINAL_VELOCITY_;
                    }

                    // Don't apply fall gravity when carrying scuba.
                    if (!inventoryContainsItem(ITEM_SCUBA))
                    {
                        // Make sure the player is falling otherwise player will move incorrectly or erratically.
                        player.state = PLAYER_STATE_FALLING;
                    }
                }
                else if (waterType == WATER_TYPE_BIOHAZARD)
                {
                    // Kill the player by losing all their health.
                    playerLoseHealth(30);

                    // TODO reset player to start position on losing a life.
                }

                currentCounter_ = 0;
            }
        }
    }
    else if (player.movementMode == MOVEMENT_MODE_WATER)
    {
        // Player is no longer submerged but still in water mode so change to land mode and walking.
        player.movementMode = MOVEMENT_MODE_LAND;
        player.state = PLAYER_STATE_WALKING;
        updatePlayerSprite(MOVEMENT_MODE_LAND);
    }

    // The use key is activated by the player when they want to use an interactable.
    if (gameControls.controlFlags & CONTROL_USE_FLAG)
    {
        // Activate any interactible in one of the squares the player occupies.
        interactableActivated += activateInteractable(occupiedXTileLeft, occupiedYTileTop, currentRoomX, currentRoomY, 0);

        // Check that the cells are not the same otherwise we get multiple activations.
        if (occupiedYTileTop != occupiedYTileBottom)
        {
            interactableActivated += activateInteractable(occupiedXTileLeft, occupiedYTileBottom, currentRoomX, currentRoomY, 0);
        }

        if (occupiedXTileLeft != occupiedXTileRight)
        {
            interactableActivated += activateInteractable(occupiedXTileRight, occupiedYTileTop, currentRoomX, currentRoomY, 0);

            if (occupiedYTileTop != occupiedYTileBottom)
            {
                interactableActivated += activateInteractable(occupiedXTileRight, occupiedYTileBottom, currentRoomX, currentRoomY, 0);
            }
        }

        // Only redraw the player occupied cells if an interactable is activated.
        if (interactableActivated)
        {
            // Redraw the cells the player occupies in case any of the interactables have changed.
            drawCellAtPosition(occupiedXTileLeft, occupiedYTileTop);
            drawCellAtPosition(occupiedXTileLeft, occupiedYTileBottom);
            drawCellAtPosition(occupiedXTileRight, occupiedYTileTop);
            drawCellAtPosition(occupiedXTileRight, occupiedYTileBottom);
        }
    }

    // Update the previous position before updating the current position with the velocity.
    player.object.previousPosition.x = player.object.position.x;
    player.object.previousPosition.y = player.object.position.y;

    // Only update the player position of the player has moved.
    if (player.object.velocity.y || player.object.velocity.x || player.state == PLAYER_STATE_WALKING)
    {
        // The tiles the player occupies following a change in velocity.
        u8 futureXTileLeft = (player.object.position.x + player.object.velocity.x) >> 2;
        u8 futureXTileRight = (player.object.position.x + TILE_WIDTH_BYTES - 1 + player.object.velocity.x);
        u8 futureYTileBottom = (player.object.position.y + TILE_HEIGHT - 1 + player.object.velocity.y);
        u8 futureYTileTop = (player.object.position.y + player.object.velocity.y) >> 4;

        futureXTileRight >>= 2;
        futureYTileBottom >>= 4;

        // Flip the player sprite horizontally based on the direction the player is moving.
        flipPlayerSprite(player.object.velocity.x);

        if (player.movementMode == MOVEMENT_MODE_WATER)
        {
            if (player.object.velocity.x < 0)
            {
                // Player going left.
                if (player.object.position.x + player.object.velocity.x >= 0)
                {
                    // Process the content of the future cells and perform collision detection.
                    u8 futureXOccupiedYTopLeft = processCell(futureXTileLeft, occupiedYTileTop);
                    u8 futureXOccupiedYBottomLeft = processCell(futureXTileLeft, occupiedYTileBottom);

                    if (futureXOccupiedYTopLeft == CELL_EMPTY && futureXOccupiedYBottomLeft == CELL_EMPTY)
                    {
                        player.object.position.x += player.object.velocity.x;
                    }
                }
                else
                {
                    // Check that there is a room on the left (not in room 0).
                    if (currentRoomX)
                    {
                        // Check that the last cell on the room to the left is empty.
                        if (processCellInRoom(SCREEN_TILES_X - 1, occupiedYTileTop, currentRoomX - 1, currentRoomY) == CELL_EMPTY &&
                            processCellInRoom(SCREEN_TILES_X - 1, occupiedYTileBottom, currentRoomX - 1, currentRoomY) == CELL_EMPTY)
                        {
                            // Update player position for the new room.
                            moveRoomX(SCREEN_X - TILE_WIDTH_BYTES, -1);
                        }
                    }
                }
            }
            else if (player.object.velocity.x > 0)
            {
                // Player going right.
                if (player.object.position.x + player.object.velocity.x <= SCREEN_X - TILE_WIDTH_BYTES)
                {
                    // Process the content of the future cells and perform collision detection.
                    u8 futureXOccupiedYTopRight = processCell(futureXTileRight, occupiedYTileTop);
                    u8 futureXOccupiedYBottomRight = processCell(futureXTileRight, occupiedYTileBottom);

                    if (futureXOccupiedYTopRight == CELL_EMPTY && futureXOccupiedYBottomRight == CELL_EMPTY)
                    {
                        player.object.position.x += player.object.velocity.x;
                    }
                }
                else
                {
                    // Check that there is a room on the right (not in room 2).
                    if (currentRoomX < 2)
                    {
                        // Check that the first cell on the room to the right is empty.
                        if (processCellInRoom(0, occupiedYTileTop, currentRoomX + 1, currentRoomY) == CELL_EMPTY &&
                            processCellInRoom(0, occupiedYTileBottom, currentRoomX + 1, currentRoomY) == CELL_EMPTY)
                        {
                            // Update player position for the new room.
                            moveRoomX(0, 1);
                        }
                    }
                }
            }

            if (player.object.velocity.y < 0)
            {
                // Player going up.
                if (player.object.position.y + player.object.velocity.y >= 0)
                {
                    // Process the content of the future cells and perform collision detection.
                    u8 occupiedXFutureYTopLeft = processCell(occupiedXTileLeft, futureYTileTop);
                    u8 occupiedXFutureYTopRight = processCell(occupiedXTileRight, futureYTileTop);

                    if (occupiedXFutureYTopLeft == CELL_EMPTY && occupiedXFutureYTopRight == CELL_EMPTY)
                    {
                        player.object.position.y += player.object.velocity.y;
                    }
                }
                else if (player.object.position.y + player.object.velocity.y == -2)
                {
                    // This allows the player to jump to get to the top of the screen
                    // when jumping instead of 2 pixels away.
                    player.object.position.y = 0;
                }
                else
                {
                    // Check that the last cell in the room above is empty.
                    if (processCellInRoom(occupiedXTileLeft, SCREEN_TILES_Y - 1, currentRoomX, currentRoomY - 1) == CELL_EMPTY &&
                        processCellInRoom(occupiedXTileRight, SCREEN_TILES_Y - 1, currentRoomX, currentRoomY - 1) == CELL_EMPTY)
                    {
                        // Update player position for the new room above.
                        // Using -24 instead of -16 prevents the hud being drawn over by the player sprite.
                        player.object.position.y = SCREEN_Y - 24;
                        player.object.previousPosition.y = SCREEN_Y - 24;

                        // Update the current room.
                        currentRoom -= 3;
                        --currentRoomY;
                        player.object.position.room = currentRoom;
                        player.object.previousPosition.room = currentRoom;
                        roomChange = TRUE;

                        // Redraw map for the new room.
                        mapRedraw = TRUE;
                    }
                }
            }
            else if (player.object.velocity.y > 0)
            {
                // Player going down.
                if (player.object.position.y + player.object.velocity.y <= SCREEN_Y_NO_HUD - TILE_HEIGHT)
                {
                    // Process the content of the future cells and perform collision detection.
                    u8 occupiedXFutureYBottomLeft = processCell(occupiedXTileLeft, futureYTileBottom);
                    u8 occupiedXFutureYBottomRight = processCell(occupiedXTileRight, futureYTileBottom);

                    if (occupiedXFutureYBottomLeft == CELL_EMPTY && occupiedXFutureYBottomRight == CELL_EMPTY)
                    {
                        player.object.position.y += player.object.velocity.y;
                    }
                }
                else
                {
                    // Check that the first cell in the room below is empty.
                    if (processCellInRoom(occupiedXTileLeft, 0, currentRoomX, currentRoomY + 1) == CELL_EMPTY &&
                        processCellInRoom(occupiedXTileRight, 0, currentRoomX, currentRoomY + 1) == CELL_EMPTY)
                    {
                        // Update player position for the new room.
                        player.object.position.y = 0;
                        player.object.previousPosition.y = 0;

                        // Update the current room.
                        currentRoom += 3;
                        ++currentRoomY;
                        player.object.position.room = currentRoom;
                        player.object.previousPosition.room = currentRoom;
                        roomChange = TRUE;

                        // Redraw map for the new room.
                        mapRedraw = TRUE;
                    }
                }
            }

            // Reset velocity to 0.
            player.object.velocity.x = 0;
            player.object.velocity.y = 0;
        }
        else
        {
            if (player.object.velocity.x < 0)
            {
                // Player going left.
                if (player.object.position.x + player.object.velocity.x >= 0)
                {
                    // Process the content of the future cells and perform collision detection.
                    u8 futureXOccupiedYTopLeft = processCell(futureXTileLeft, occupiedYTileTop);
                    u8 futureXOccupiedYBottomLeft = processCell(futureXTileLeft, occupiedYTileBottom);

                    if (futureXOccupiedYTopLeft == CELL_EMPTY && futureXOccupiedYBottomLeft == CELL_EMPTY)
                    {
                        player.object.position.x += player.object.velocity.x;
                    }
                }
                else
                {
                    // Check that there is a room on the left (not in room 0).
                    if (currentRoomX)
                    {
                        // Check that the last cell on the room to the left is empty.
                        if (processCellInRoom(SCREEN_TILES_X - 1, occupiedYTileTop, currentRoomX - 1, currentRoomY) == CELL_EMPTY &&
                            processCellInRoom(SCREEN_TILES_X - 1, occupiedYTileBottom, currentRoomX - 1, currentRoomY) == CELL_EMPTY)
                        {
                            // Update player position for the new room.
                            moveRoomX(SCREEN_X - TILE_WIDTH_BYTES, -1);
                        }
                    }
                }
            }
            else if (player.object.velocity.x > 0)
            {
                // Player going right.
                if (player.object.position.x + player.object.velocity.x <= SCREEN_X - TILE_WIDTH_BYTES)
                {
                    // Process the content of the future cells and perform collision detection.
                    u8 futureXOccupiedYTopRight = processCell(futureXTileRight, occupiedYTileTop);
                    u8 futureXOccupiedYBottomRight = processCell(futureXTileRight, occupiedYTileBottom);

                    if (futureXOccupiedYTopRight == CELL_EMPTY && futureXOccupiedYBottomRight == CELL_EMPTY)
                    {
                        player.object.position.x += player.object.velocity.x;
                    }
                }
                else
                {
                    // Check that there is a room on the right (not in room 2).
                    if (currentRoomX < 2)
                    {
                        // Check that the first cell on the room to the right is empty.
                        if (processCellInRoom(0, occupiedYTileTop, currentRoomX + 1, currentRoomY) == CELL_EMPTY &&
                            processCellInRoom(0, occupiedYTileBottom, currentRoomX + 1, currentRoomY) == CELL_EMPTY)
                        {
                            // Update player position for the new room.
                            moveRoomX(0, 1);
                        }
                    }
                }
            }

            // Reset the x velocity after moving the player.
            // If you want an interesting sliding effect like when going down the stares then remove this.
            player.object.velocity.x = 0;

            // Occupy x tile positions following change of position.
            occupiedXTileLeft = (player.object.position.x) >> 2;
            occupiedXTileRight = (player.object.position.x + TILE_WIDTH_BYTES - 1);
            occupiedXTileRight >>= 2;

            futureXTileLeft = (player.object.position.x + player.object.velocity.x) >> 2;
            futureXTileRight = (player.object.position.x + TILE_WIDTH_BYTES - 1 + player.object.velocity.x);
            futureXTileRight >>= 2;

            if (player.state == PLAYER_STATE_WALKING)
            {
                u8 belowOccupiedYTileBottom = occupiedYTileBottom + 1;

                // Check whether the player is part way through a tile.
                if (player.object.position.y % TILE_HEIGHT)
                {
                    // This fixes the issue where the player move out of a water region mid tile (left or right)
                    // and instead of falling they hang in the air.
                    belowOccupiedYTileBottom -= 1;
                }

                if (processCell(futureXTileLeft, belowOccupiedYTileBottom) == CELL_EMPTY && processCell(futureXTileRight, belowOccupiedYTileBottom) == CELL_EMPTY)
                {
                    player.state = PLAYER_STATE_FALLING;
                    player.object.velocity.y = FALL_GRAVITY_;
                }
                else
                {
                    // Reset velocity to 0.
                    player.object.velocity.x = 0;
                    player.object.velocity.y = 0;
                }
            }
            else if (player.state == PLAYER_STATE_JUMPING)
            {
                if (player.object.velocity.y < 0)
                {
                    u8 occupiedXFutureYTopLeft;
                    u8 occupiedXFutureYTopRight;

                    player.object.velocity.y += FALL_GRAVITY_;

                    // The tiles the player occupies following a change in velocity.
                    futureYTileTop = (player.object.position.y + player.object.velocity.y) >> 4;
                    futureXTileRight >>= 2;

                    if (futureYTileTop == 255)
                    {
                        // Check that the last cell in the room above is empty.
                        if (processCellInRoom(occupiedXTileLeft, SCREEN_TILES_Y - 1, currentRoomX, currentRoomY - 1) == CELL_EMPTY &&
                            processCellInRoom(occupiedXTileRight, SCREEN_TILES_Y - 1, currentRoomX, currentRoomY - 1) == CELL_EMPTY)
                        {
                            // Update player position for the new room above.
                            // Using -24 instead of -16 prevents the hud being drawn over by the player sprite.
                            player.object.position.y = SCREEN_Y - 24;
                            player.object.previousPosition.y = SCREEN_Y - 24;

                            // Update the current room.
                            currentRoom -= 3;
                            --currentRoomY;
                            player.object.position.room = currentRoom;
                            player.object.previousPosition.room = currentRoom;
                            roomChange = TRUE;

                            // Add to velocity to prevent the player being able to jump a greater number of tiles
                            // than normal when moving rooms.
                            player.object.velocity.y += FALL_GRAVITY_;

                            // Redraw map for the new room.
                            mapRedraw = TRUE;

                            // Update futureYTileTop for collision detection.
                            futureYTileTop = SCREEN_TILES_Y;
                        }
                        else
                        {
                            // Set the position to the maximum position so you can scrape right against the top of the screen when jumping.
                            player.object.position.y = 0;

                            // Solid above the player so set velocity to 0 so they start falling.
                            player.object.velocity.y = 0;
                        }
                    }

                    // Process the content of the future cells and perform collision detection.
                    occupiedXFutureYTopLeft = processCell(occupiedXTileLeft, futureYTileTop);
                    occupiedXFutureYTopRight = processCell(occupiedXTileRight, futureYTileTop);

                    if (occupiedXFutureYTopLeft == CELL_EMPTY && occupiedXFutureYTopRight == CELL_EMPTY)
                    {
                        player.object.position.y += player.object.velocity.y;
                    }
                }

                if (player.object.velocity.y == 0)
                {
                    player.state = PLAYER_STATE_FALLING;
                    player.object.velocity.y = FALL_GRAVITY_;
                }
            }
            else if (player.state == PLAYER_STATE_FALLING)
            {
                // Non zero velocity means we must be moving.
                if (player.object.velocity.y)
                {
                    // Only increase velocity due to gravity until we reach terminal velocity.
                    if (player.object.velocity.y < TERMINAL_VELOCITY_)
                    {
                        player.object.velocity.y += FALL_GRAVITY_;
                    }


                    // Future y tile button needs to be updated for the new velocity.
                    futureYTileBottom = (player.object.position.y + TILE_HEIGHT - 1 + player.object.velocity.y);
                    futureYTileBottom >>= 4;

                    do
                    {
                        // Process the content of the future cells and perform collision detection.
                        u8 occupiedXFutureYBottomLeft = processCell(occupiedXTileLeft, futureYTileBottom);
                        u8 occupiedXFutureYBottomRight = processCell(occupiedXTileRight, futureYTileBottom);

                        if (futureYTileBottom >= 12)
                        {
                            // Check that the first cell in the room below is empty.
                            if (processCellInRoom(occupiedXTileLeft, 0, currentRoomX, currentRoomY + 1) == CELL_EMPTY &&
                                processCellInRoom(occupiedXTileRight, 0, currentRoomX, currentRoomY + 1) == CELL_EMPTY)
                            {
                                // Update player position for the new room.
                                player.object.position.y = 0;
                                player.object.previousPosition.y = 0;

                                // Update the current room.
                                currentRoom += 3;
                                ++currentRoomY;
                                player.object.position.room = currentRoom;
                                player.object.previousPosition.room = currentRoom;
                                roomChange = TRUE;

                                // Redraw map for the new room.
                                mapRedraw = TRUE;

                                break;
                            }
                        }

                        if (occupiedXFutureYBottomLeft == CELL_EMPTY && occupiedXFutureYBottomRight == CELL_EMPTY)
                        {
                            player.object.position.y += player.object.velocity.y;

                            // Cell underneath is empty so break out of the loop as velocity can be added in full.
                            break;
                        }
                        else
                        {
                            // Velocity cannot be added in full so reduce velocity by one and retest the cells below with reduced velocity.
                            if (player.object.velocity.y > 1)
                            {
                                --player.object.velocity.y;
                                futureYTileBottom = (player.object.position.y + TILE_HEIGHT - 1 + player.object.velocity.y);
                                futureYTileBottom >>= 4;
                            }
                            else
                            {
                                // Velocity is one and still bocked so change to walking as player is on the ground now.
                                player.object.velocity.y = 0;
                                player.state = PLAYER_STATE_WALKING;
                            }
                        }
                    }
                    while (player.object.velocity.y);
                }
            }
        }

        // Redraw the player at the new position if they have moved.
        if (player.object.position.x != player.object.previousPosition.x || player.object.position.y != player.object.previousPosition.y)
        {
            playerRedraw = TRUE;
        }

        // If a spike was detected in any of the player movement it is now necessary
        // to check if the player actually collided with the spike.
        // This is necessary because the processCell code does not know whether it is
        // handling the player's future or current position which could end up being blocked
        // by a tile or other solid cell.
        if (player.spikeDetected)
        {
            u8 spikeCollision = 0;

            occupiedXTileLeft = (player.object.position.x) >> 2;
            occupiedXTileRight = (player.object.position.x + TILE_WIDTH_BYTES - 1);
            occupiedYTileBottom = (player.object.position.y + TILE_HEIGHT - 1);
            occupiedYTileTop = (player.object.position.y) >> 4;
            occupiedXTileRight >>= 2;
            occupiedYTileBottom >>= 4;

            spikeCollision += cellContainsSpike(occupiedXTileLeft, occupiedYTileTop);
            spikeCollision += cellContainsSpike(occupiedXTileRight, occupiedYTileTop);
            spikeCollision += cellContainsSpike(occupiedXTileLeft, occupiedYTileBottom);
            spikeCollision += cellContainsSpike(occupiedXTileRight, occupiedYTileBottom);

            if (spikeCollision)
            {
                playerLoseHealth(spikeDamageAmount);

                // Invulnerability timer.
                activateCollisionTimer();

                // Update health on the hud.
                hudRedraw = TRUE;
            }

            // Reset the spike detected flag after the spike detection has been processed.
            player.spikeDetected = FALSE;
        }
    }

    //printf("ppy=%i py=%i\n", player.object.previousPosition.y, player.object.position.y);

    // Test for collisions with enemies.
    playerEnemyCollision();

    // Run logic for the player projectile after running player logic.
    logicProjectile(&player.projectile);

    // This fixes an issue where jumping out of a down current results in the player getting stuck mid air.
    if (player.state == PLAYER_STATE_FALLING && player.object.velocity.y == 0 && player.movementMode == MOVEMENT_MODE_LAND)
    {
        player.state = PLAYER_STATE_WALKING;
    }

    // This fixes an issue where the player does not have scuba gear but movement mode has been set to water mode because the player was in an up current.
    // There would be a better fix for this but I don't want to make major changes to the movement system at this stage.
    if (player.object.velocity.y == 0 && (player.movementMode == MOVEMENT_MODE_WATER || player.state == PLAYER_STATE_WATER) && !inventoryContainsItem(ITEM_SCUBA))
    {
        player.state = PLAYER_STATE_WALKING;
        player.movementMode = MOVEMENT_MODE_LAND;
    }

    // Check that the player is in water (oxygen is then depleted).
    if (MAPA(occupiedXTileLeft, occupiedYTileTop) & CELL_WATER_FLAG)
    {
        depleteOxygen(MAPA(occupiedXTileLeft, occupiedYTileTop) & CELL_GFX_RANGE);
    }
    else
    {
        // Player no longer in water so oxygen is no longer being depleted.
        oxygenDepleting = FALSE;
    }

    if (roomChange)
    {
        // Destroy the projectile when the player changes room.
        player.projectile.health = 0;

        // Reset the water timer on change room.
        // Otherwise if multiple rooms have chasing water the water in the second room could move faster then it should.
        waterTimerCount = 0;
        runWaterLogic = FALSE;

        // Player needs to be redrawn on room change.
        playerRedraw = TRUE;
    }

    if (!playerLives)
    {
        gameRunning = FALSE;
        levelState = LEVEL_GAME_OVER;

        // Play game over sound and display black on screen.
        playGameOverSound();

        // Reset border to dark blue after black game over screen.
        cpct_setBorder(4);
    }
}

void playerEnemyCollision()
{
    u8 i = 0;
    Enemy* enemy = mapEnemies;

    for (i = 0; i < mapEnemyCount; ++i)
    {
        // Check if the enemy is on screen.
        if (enemy->object.position.room == currentRoom)
        {
            // Two tiles width is 8.
            // Two tiles height is 32.
            if (difference(enemy->object.position.y, player.object.position.y) < 32 && difference(enemy->object.position.x, player.object.position.x) < 8)
            {
                // The player needs to be redrawn because of the enemy backbuffer.
                playerRedraw = TRUE;

                // The enemy needs to be drawn because of the player backbuffer.
                drawEnemy(enemy);

                // Adding the sprite to backbuffer has less flicker but can cause catastrophic memory corruption.
                // Add the enemy to the backbuffer.
                //addSpriteToBackbuffer(enemy->object.position.x, enemy->object.position.y, enemy->object.width, enemy->object.height, enemyGraphics[enemy->type - 1]);

                // Check that enemy is colliding with the player (based on current position and enemy velocity).
                if (testObjectIntersectionBVelocity(&player.object, &enemy->object))
                {
                    // Reset enemy velocity after a collision occurs.
                    enemy->object.velocity.x = 0;
                    enemy->object.velocity.y = 0;

                    // Stun the enemy causing them to no longer move.
                    enemy->stunned = TRUE;

                    // Reduce player's health after the collision
                    if (!collisionActive)
                    {
                        // Reduce player health by the difficulty amount and update lives if they have no health.
                        playerLoseHealth(enemyDamageAmount);
                        hudRedraw = TRUE;

                        // Activate the collision timer making the player temporarily invulnerable.
                        // This is also the length of time that an enemy will be stunned for.
                        activateCollisionTimer();
                    }
                }
            }
        }

        ++enemy;
    }
}

void playerLoseHealth(u8 amount)
{
    if (player.health - amount > 0)
    {
        player.health -= amount;

        // Player sound effect to let the player know they took damage (not at max volume).
        playSoundEffect(SFX_TAKE_DAMAGE, CHANNEL_A, 5);
    }
    else
    {
        if (--playerLives)
        {
            levelState = LEVEL_DEATH;
            gameRunning = FALSE;
            player.health = playerStartingHealth;

            // Play game over sound and display black on screen.
            playGameOverSound();
        }
    }
}

u8 processCellInRoom(u8 x, u8 y, u8 roomX, u8 roomY)
{
    u8 cell = mapCell(x, y, roomX, roomY);
    u8 cellType = cell & CELL_GFX_RANGE;

    if (cell & CELL_ITEM_FLAG)
    {
        // Check if the player is already carrying the item.
        if (inventoryContainsItem(cellType))
        {
            // Returning a 1 is equivalent to a tile (removing item flag) so that the player cannot pickup the item matching the one in the inventory.
            return 1;
        }

        // Set map cell to empty so the item is removed from the map grid.
        if (cell & CELL_WATER_FLAG)
        {
            updateMapCell(x, y, roomX, roomY, CELL_WATER_FLAG);
        }
        else
        {
            updateMapCell(x, y, roomX, roomY, CELL_EMPTY);
        }

        // Found an item so add it to the game score.
        levelScore += itemScore(cellType);

        // Add the item to the inventory for all supported persistent items.
        addItemToInventory(cellType);

        // Play a sound effect on picking up an item (except for oxygen which has its own sound).
        if (cellType != ITEM_OXYGEN_TANK)
        {
            playSoundEffect(SFX_PICKUP, CHANNEL_A, 4);
        }

        if (cellType == ITEM_PROJECTILE_AMMO)
        {
            player.projectileAmmo += ammoPickupAmount;
        }
        else if (cellType == ITEM_OXYGEN_TANK)
        {
            // When picking up oxygen the player can carry more oxygen than when using an oxygen point.
            if (player.oxygen + oxygenPickupAmount < maxOxygen)
            {
                player.oxygen += oxygenPickupAmount;

                // Play oxygen sound effect on oxygen tank item increasing oxygen.
                playSoundEffect(SFX_OXYGEN, CHANNEL_A, MAX_VOL);
            }
        }
        else if (cellType == ITEM_HEALTH)
        {
            if (player.health + healthPickupAmount <= maxPlayerHealth)
            {
                player.health += healthPickupAmount;
            }
            else
            {
                player.health = maxPlayerHealth;
            }
        }
        else if (cellType == ITEM_PIPE)
        {
            if (mapObjective == OBJECTIVE_TYPE_COLLECT_PIPES)
            {
                // When the pipe objective is enabled update the objective action count.
                reduceObjectiveActionCount();
            }
            else if (mapObjective == OBJECTIVE_TYPE_COLLECT_AND_PLACE_PIPES)
            {
                ++pipeCount;
            }
        }
        else if (cellType == ITEM_PICK)
        {
            ++player.pickCount;
        }

        // Redraw the hud with the updated score.
        hudRedraw = TRUE;

        // Redraw the cell following removal of the item (don't need to redraw whole screen).
        drawCellAtPosition(x, y);
    }
    else if (cell & CELL_INTERACTABLE_FLAG)
    {
        if (isInteractableReady(cellType))
        {
            if (cellType == INTERACTABLE_DOOR_RED || cellType == INTERACTABLE_DOOR_GREEN || cellType == INTERACTABLE_DOOR_YELLOW)
            {
                // Set map cell to empty or with water (depending on the AND operation) so the door interactable is removed from the map grid.
                updateMapCell(x, y, roomX, roomY, cell & CELL_WATER_FLAG);

                // Redraw the cell without the door interactable (don't need to redraw whole screen).
                drawCellAtPosition(x, y);

                // Redraw the hud for any inventory changes (such as keys used).
                hudRedraw = TRUE;
            }
            else if (cellType == INTERACTABLE_SWITCH_ON)
            {
                // The player should be able to walk over a switch.
                return 0;
            }
            else if (cellType == INTERACTABLE_SWITCH_OFF)
            {
                // The player should be able to walk over a switch.
                return 0;
            }
            else if (cellType == INTERACTABLE_VALVE_ON)
            {
                // The player should be able to walk over a valve.
                return 0;
            }
            else if (cellType == INTERACTABLE_VALVE_OFF)
            {
                // The player should be able to walk over a valve.
                return 0;
            }
            else if (cellType == INTERACTABLE_OXYGEN_POINT)
            {
                // The player should be able to walk over an oxygen point.
                return 0;
            }
            else if (cellType == INTERACTABLE_EXIT)
            {
                return 0;
            }
            else if (cellType >= INTERACTABLE_SPIKE_RIGHT && cellType <= INTERACTABLE_SPIKE_UP_LAND)
            {
                // If player is invulnerable it does not matter if there is a spike.
                if (!collisionActive)
                {
                    // Set spike detected flag to later process if the player
                    // has collided with a spike.
                    player.spikeDetected = TRUE;
                }

                // Return 0 so they can fully collide with the spike.
                return 0;
            }
            else if (cellType == INTERACTABLE_PUMP_PART1_BROKEN || cellType == INTERACTABLE_TANK_BROKEN)
            {
                // Check the player is carrying a spanner.
                if (inventoryContainsItem(ITEM_SPANNER))
                {
                    // Change the broken pump interactable to a working pump interactable.
                    updateMapCell(x, y, roomX, roomY, CELL_INTERACTABLE_FLAG | (cellType + 1));
                    drawCellAtPosition(x, y);

                    // Remove the spanner from the inventory.
                    inventoryItems &= ~INVENTORY_ITEM_SPANNER;

                    if (mapObjective == OBJECTIVE_TYPE_FIX_MACHINERY)
                    {
                        // Update the objective action count following fixing the machinery.
                        reduceObjectiveActionCount();
                    }

                    // This allows the machinery (once fixed) to activate a linked interactable such as a door.
                    activateLinkedInteractable(x, y);

                    // Play a sound effect when repairing machinery.
                    playSoundEffect(SFX_REPAIR, CHANNEL_A, MAX_VOL);

                    // Redraw the hud following inventory update.
                    hudRedraw = TRUE;

                    return 0;
                }
            }
            else if (cellType == INTERACTABLE_PIPE_LOCATION)
            {
                if (mapObjective == OBJECTIVE_TYPE_PLACE_PIPES || (mapObjective == OBJECTIVE_TYPE_COLLECT_AND_PLACE_PIPES && pipeCount))
                {
                    // Change the empty interactable pipe location to a filled location.
                    updateMapCell(x, y, roomX, roomY, CELL_INTERACTABLE_FLAG | INTERACTABLE_PIPE_LOCATION_FILLED);
                    drawCellAtPosition(x, y);

                    // Update the objective action count.
                    reduceObjectiveActionCount();

                    // Remove a pipe on use.
                    // I am ignoring the objective type as it is only used for collect and place pipes.
                    --pipeCount;

                    // Redraw the hud following the use of the pipe.
                    hudRedraw = TRUE;
                }

                // An empty pipe location is not solid, a filled location is solid.
                return 0;
            }
            else if (cellType == INTERACTABLE_CRACKED_TILE_SAND || cellType == INTERACTABLE_CRACKED_TILE_STONE || cellType == INTERACTABLE_CRACKED_TILE_DIRT)
            {
                // Set map cell to empty or with water (depending on the AND operation) so the cracked tile interactable is removed from the map grid.
                updateMapCell(x, y, roomX, roomY, cell & CELL_WATER_FLAG);

                // Redraw the cell without the cracked tile interactable (don't need to redraw whole screen).
                drawCellAtPosition(x, y);

                // Play the pick sound when clearing the cracked block.
                playSoundEffect(SFX_PICK, CHANNEL_A, MAX_VOL);

                // Redraw the hud following the use of the pick.
                hudRedraw = TRUE;
            }
            else if (cellType == INTERACTABLE_TELEPORTER_ON)
            {
                // The player should be able to walk over a teleporter.
                return 0;
            }
            else if (cellType == INTERACTABLE_TELEPORTER_OFF)
            {
                // The player should be able to walk over a teleporter.
                return 0;
            }
            else if (cellType == INTERACTABLE_CPC)
            {
                // The player should be able to be in front of the Amstrad CPC.
                return 0;
            }
            else if (cellType == INTERACTABLE_PLATFORM)
            {
                if (player.state == PLAYER_STATE_FALLING)
                {
                    // This handles coming in from the side of a platform.
                    if (player.object.velocity.x)
                    {
                        return CELL_EMPTY;
                    }

                    // Platform is only solid when falling onto it.
                    return 1;
                }
                else
                {
                    // Platform is not solid when walking or jumping into it.
                    return CELL_EMPTY;
                }
            }
        }
    }
    else if (cell & CELL_WATER_FLAG)
    {
        // Treat water as an empty cell.
        return CELL_EMPTY;
    }

    return cell;
}

u8 processCell(u8 x, u8 y)
{
    return processCellInRoom(x, y, currentRoomX, currentRoomY);
}

void depleteOxygen(u8 waterType)
{
    // Activate depletion timer if currently off.
    if (!oxygenDepleting)
    {
        oxygenDepleting = TRUE;
    }

    if (waterType == WATER_TYPE_LOW_OXYGEN)
    {
        // Low oxygen water causes oxygen to delete much faster.
        oxygenDepletionTimerInterval = LOW_OXYGEN_DEPLETION_RATE;
    }
    else if (waterType == WATER_TYPE_HIGH_OXYGEN)
    {
        // High oxygen water causes oxygen to delete much slower.
        oxygenDepletionTimerInterval = HIGH_OXYGEN_DEPLETION_RATE;
    }
    else if (waterType == WATER_TYPE_OXYGEN_ADD)
    {
        // The oxygen add water makes sure the player has a decent amount of oxygen.
        if (player.oxygen < 20)
        {
            player.oxygen = 20;
            hudRedraw = TRUE;

            // Play oxygen sound effect on oxygen add water.
            playSoundEffect(SFX_OXYGEN, CHANNEL_A, MAX_VOL);
        }
    }
    else if (waterType == WATER_TYPE_OXYGEN_SUBTRACT)
    {
        // The oxygen subtract makes sure the player has a limited but non zero amount of oxygen.
        if (player.oxygen > 5)
        {
            player.oxygen = 5;
            hudRedraw = TRUE;
        }
    }
    else
    {
        oxygenDepletionTimerInterval = DEFAULT_OXYGEN_DEPLETION_RATE;
    }

    // When the timer is tripped it will set oxygenDeleting to 2.
    if (oxygenDepleting == 2)
    {
        // Deplete player oxygen (if they have scuba gear) until they run out, then deplete health.
        if (inventoryContainsItem(ITEM_SCUBA) && player.oxygen)
        {
            --player.oxygen;
        }
        else
        {
            playerLoseHealth(1);
        }

        hudRedraw = TRUE;

        // Reset oxygenDepleting to 1 so that the timer has to update it to 2 again.
        oxygenDepleting = TRUE;
    }
}

void moveRoomX(u8 newX, i8 roomDeltaX)
{
    // Update player position for the new room.
    player.object.position.x = newX;
    player.object.previousPosition.x = newX;
    player.object.velocity.x = 0;

    // Update the current room.
    currentRoom += roomDeltaX;
    currentRoomX += roomDeltaX;
    player.object.position.room = currentRoom;
    player.object.previousPosition.room = currentRoom;
    roomChange = TRUE;

    // Redraw map for the new room.
    mapRedraw = TRUE;
}

void flipPlayerSprite(i8 xVelocity)
{
    if (xVelocity > 0)
    {
        // Right
        if (player.object.direction != DIRECTION_RIGHT)
        {
            cpct_hflipSpriteM0(TILE_WIDTH_BYTES, TILE_HEIGHT, player.playerSprite);
            player.object.direction = DIRECTION_RIGHT;
        }
    }
    else if (xVelocity < 0)
    {
        // Left
        if (player.object.direction != DIRECTION_LEFT)
        {
            cpct_hflipSpriteM0(TILE_WIDTH_BYTES, TILE_HEIGHT, player.playerSprite);
            player.object.direction = DIRECTION_LEFT;
        }
    }
}

void updatePlayerSprite(u8 mode)
{
    if (mode == MOVEMENT_MODE_LAND)
    {
        cpct_memcpy(player.playerSprite, G_player_sprite_right, 64);
    }
    else
    {
        cpct_memcpy(player.playerSprite, G_player_scuba, 64);
    }

    // Player sprites are by default right so if the current direction is left we need to flip the sprite.
    if (player.object.direction == DIRECTION_LEFT)
    {
        cpct_hflipSpriteM0(TILE_WIDTH_BYTES, TILE_HEIGHT, player.playerSprite);
    }
}

void teleportPlayer(u8 destinationX, u8 destinationY, u8 destinationRoomX, u8 destinationRoomY)
{
    // Update player position.
    player.object.position.x = destinationX * TILE_WIDTH_BYTES;
    player.object.position.y = destinationY * TILE_HEIGHT;
    player.object.previousPosition.x = destinationX * TILE_WIDTH_BYTES;
    player.object.previousPosition.y = destinationY * TILE_HEIGHT;

    // Reset velocity.
    player.object.velocity.x = 0;
    player.object.velocity.y = 0;

    // Reset to walking (state will update to falling if required in move code).
    player.state = PLAYER_STATE_WALKING;

    // Update the tracking for the current room.
    currentRoom = destinationRoomX + destinationRoomY * 3;
    currentRoomX = destinationRoomX;
    currentRoomY = destinationRoomY;

    // Update player room.
    player.object.position.room = currentRoom;
    player.object.previousPosition.room = currentRoom;

    // Play teleport sound effect.
    playSoundEffect(SFX_TELEPORT, CHANNEL_A, 4);

    // Indicate the room has changed.
    roomChange = TRUE;

    // Redraw map for the new room.
    mapRedraw = TRUE;
}

void playGameOverSound()
{
    // Reusing the water timer for the death sound delay.
    runWaterLogic = FALSE;
    waterTimerCount = 300;

    // Play death sound effect (technically not game over.
    playSoundEffect(SFX_GAME_OVER, CHANNEL_A, MAX_VOL);

    // Set background to black.
    // 4 is default, 20 is black border.
    cpct_setBorder(20);
    cpct_clearScreen(cpct_px2byteM0(5, 5));

    while (!runWaterLogic)
    {
        // Delay for the death sound effect to play using the water timer.
    }
}

void reduceObjectiveActionCount()
{
    --objectiveActionCount;

    // Add 20 (200 on screen) points on progressing an objective.
    levelScore += 20;
}