Boat.java

package com.dragonboat.game;

import java.util.ArrayList;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Texture;

/**
 * Represents a Boat, controlled by either a Player or Opponent.
 * 
 * @see Player
 * @see Opponent
 */
public class Boat {
    /*
     * Direct representation based off the UML diagram
     * https://drive.google.com/file/d/15O95umnJIoApnsj8I9ejEtMxrDGYJWAC/view?usp=sharing
     */

    protected int ROBUSTNESS;
    protected float ACCELERATION, MANEUVERABILITY, MAXSPEED;
    protected int durability;
    protected float yPosition, xPosition, penalties;
    protected int width, height;
    protected float currentSpeed, fastestLegTime, tiredness;
    protected Lane[] lanes;
    protected int laneNo = 0;
    protected Texture[] textureFrames;
    protected int frameCounter = 0;
    protected float lastFrameY = 0;
    public Texture texture;
    protected String name;
    protected char label;
    protected boolean finished;
    protected int threshold = 5;

    public static int bankWidth = 40;
    public static int MAX_DURABILITY = 50;
    public static float MAX_TIREDNESS = 100;

    /**
     * Represents a savable/loadable boat.
     */
    static class BoatSpriteDescriptor {
        public int durability;
        public float yPosition, xPosition, penalties;
        public int width, height;
        public float currentSpeed, fastestLegTime, tiredness;
        public int laneNo;
        public int frameCounter;
        public float lastFrameY;
        public String name;
        public boolean finished;
        public char label;

        /**
         * Json requires an empty constructor to regenerate the class from a save file
         */
        public BoatSpriteDescriptor(){}

        /**
         * Creates a json friendly instance of the obstacle
         *
         * @param oldBoat The boat data that needs to be converted to be stored properly
         */
        public BoatSpriteDescriptor(Boat oldBoat) {
            this.durability = oldBoat.getDurability();
            this.yPosition = oldBoat.getY();
            this.xPosition = oldBoat.getX();
            this.penalties = oldBoat.getPenalty();
            this.durability = oldBoat.getDurability();
            this.width = oldBoat.width;
            this.height = oldBoat.getHeight();
            this.currentSpeed = oldBoat.getCurrentSpeed();
            this.fastestLegTime = oldBoat.getFastestTime();
            this.tiredness = oldBoat.tiredness;
            this.laneNo = oldBoat.laneNo;
            this.frameCounter = oldBoat.frameCounter;
            this.lastFrameY = oldBoat.lastFrameY;
            this.name = oldBoat.getName();
            this.finished = oldBoat.finished();
            this.label = oldBoat.label;
        }
    }

    /**
     * Creates an instance of the player boat.
     *
     * @param yPosition Y-position of the boat.
     * @param width     Width of the boat.
     * @param height    Height of the boat.
     * @param lanes     Lanes for the boat.
     * @param laneNo    Lane number for the boat.
     * @param name      Name of the boat.
     */
    public Boat(float yPosition, int width, int height, Lane[] lanes, int laneNo, String name) {
        this.xPosition = lanes[laneNo].getRightBoundary() - (lanes[laneNo].getRightBoundary() - lanes[laneNo].getLeftBoundary()) / 2.0f - width / 2.0f;
        this.yPosition = yPosition;
        this.width = width;
        this.height = height;
        this.currentSpeed = 0f;
        this.penalties = 0;
        this.durability = Boat.MAX_DURABILITY;
        this.tiredness = 0f;
        this.lanes = lanes;
        this.laneNo = laneNo;
        this.fastestLegTime = 0;
        this.lastFrameY = 0;
        this.textureFrames = new Texture[4];
        this.frameCounter = 0;
        this.name = name;
        this.threshold = 5;
    }

    /**
     * Decreases the x-position of the boat respective to the boat's maneuverability
     * and speed, and decreases the speed by 3%.
     */
    public void SteerLeft() {
        if (this.xPosition > bankWidth) {
            this.xPosition = Math.max(this.xPosition - this.MANEUVERABILITY * this.currentSpeed, bankWidth);
            this.currentSpeed *= 0.985f;
        } else {
            this.xPosition = bankWidth;
        }
    }

    /**
     * Increases the x-position of the boat respective to the boat's maneuverability
     * and speed, and decreases the speed by 3%.
     */
    public void SteerRight() {
        if (this.xPosition + this.width < Gdx.graphics.getWidth() - bankWidth) {
            this.xPosition = Math.min(this.xPosition + this.MANEUVERABILITY * this.currentSpeed, Gdx.graphics.getWidth() - bankWidth - this.width);
            this.currentSpeed *= 0.985f;
        } else {
            this.xPosition = Gdx.graphics.getWidth() - bankWidth - this.width;
        }
    }

    /**
     * Increases the y-position of the boat respective to the boat's speed, and
     * decreases the speed by 0.08%.
     */
    public void MoveForward() {
        this.yPosition += this.currentSpeed;
        this.currentSpeed *= 0.9992f;
        if (this.yPosition - this.lastFrameY > 15.0f) {
            this.lastFrameY = this.yPosition;
            this.AdvanceTextureFrame();
        }
    }

    /**
     * If the boat has enough stamina, increase the speed of the boat by the boat's
     * acceleration, if not, do nothing.
     */
    public void IncreaseSpeed() {
        float damagedMaxSpeedMultiplier = 1.0f;
        if (this.durability <= 0) {
            damagedMaxSpeedMultiplier = 0.5f;
        }
        if (this.tiredness <= 75) {
            this.currentSpeed = Math.min(this.currentSpeed + this.ACCELERATION, this.MAXSPEED * damagedMaxSpeedMultiplier);
        }
    }

    /**
     * Decreases the speed of the boat by 0.015 if the resulting speed is greater
     * than 0.
     */
    public void DecreaseSpeed() {
        /*
         * Very basic deceleration function, acting as water friction. could be updated
         * using functions from
         * https://denysalmaral.com/2019/05/boat-sim-notes-1-water-friction.html to be
         * more realistic.
         */
        this.currentSpeed = Math.max(this.currentSpeed - 0.015f, 0);
    }

    /**
     * Checks each obstacle in the Lane for a collision.
     * 
     * @param backgroundOffset How far up the course the player is.
     * @return Boolean representing if a collision occurs.
     */
    public boolean CheckCollisions(int backgroundOffset) {
        boolean hitObstacle = false;
        // Iterate through obstacles.
        ArrayList<Obstacle> obstacles = this.lanes[this.laneNo].obstacles;
        ArrayList<Obstacle> obstaclesToRemove = new ArrayList<>();
        for (Obstacle o : obstacles) {
            if (o.getX() + o.width > this.xPosition + threshold && o.getX() < this.xPosition + this.width - threshold) {
                if (o.getY() + o.height + backgroundOffset > this.yPosition + threshold && o.getY() + backgroundOffset < this.yPosition + this.height - threshold) {
                    this.ApplyDamage(o.getDamage());
                    obstaclesToRemove.add(o);
                    hitObstacle = true;
                }
            }
        }
        for (Obstacle obstacleToRemove : obstaclesToRemove) {
            this.lanes[this.laneNo].RemoveObstacle(obstacleToRemove);
        }
        return hitObstacle;
    }

    /**
     * Decreases the durability of the boat by the obstacle damage divided by the
     * boat's robustness.
     * 
     * @param obstacleDamage Amount of damage an Obstacle inflicts on the boat.
     * @return Boolean representing whether the durability of the boat is below 0.
     */
    public boolean ApplyDamage(int obstacleDamage) {
        this.durability -= obstacleDamage / this.ROBUSTNESS;
        this.currentSpeed *= 0.9f;
        return this.durability <= 0;
    }

    /**
     * Checks if the boat is between the left boundary and the right boundary of the Lane.
     * 
     * @return Boolean representing whether the Boat is in the Lane.
     */
    public boolean CheckIfInLane() {
        return this.xPosition + threshold >= this.lanes[this.laneNo].getLeftBoundary()
            && this.xPosition + this.width - threshold <= this.lanes[this.laneNo].getRightBoundary();
    }

    /**
     * Updates the boat's fastest time.
     * 
     * @param elapsedTime Time it took the boat to finish the current race.
     */
    public void UpdateFastestTime(float elapsedTime) {
        if (this.fastestLegTime > elapsedTime + this.penalties || this.fastestLegTime == 0) {
            this.fastestLegTime = elapsedTime + this.penalties;
        }
    }

    /**
     * Increases the tiredness of the boat by 0.75 if the tiredness is less than
     * 100.
     */
    public void IncreaseTiredness() {
        this.tiredness += this.tiredness >= 100 ? 0 : 0.75f;
    }

    /**
     * Decreases the tiredness of the boat by 1 if the tiredness is greater than 0.
     */
    public void DecreaseTiredness() {
        this.tiredness -= this.tiredness <= 0 ? 0 : 1f;
    }

    /**
     * Keeps track of which frame of the animation the boat's texture is on, and
     * sets the texture accordingly.
     */
    public void AdvanceTextureFrame() {
        this.frameCounter = this.frameCounter == this.textureFrames.length - 1 ? 0 : this.frameCounter + 1;
        this.setTexture(this.textureFrames[this.frameCounter]);
    }

    /**
     * Generates all frames for animating the boat.
     * 
     * @param boatName Boat name, used to get correct asset.
     */
    public void GenerateTextureFrames(char boatName) {
        Texture[] frames = new Texture[4];
        for (int i = 1; i <= frames.length; i++) {
            frames[i - 1] = new Texture(Gdx.files.internal("core/assets/boat" + boatName + " sprite" + i + ".png"));
        }
        this.setTextureFrames(frames);
    }

    /**
     * Resets necessary stats for the next race.
     */
    public void Reset() {
        this.xPosition = lanes[this.laneNo].getRightBoundary() - (lanes[this.laneNo].getRightBoundary() - lanes[this.laneNo].getLeftBoundary()) / 2.0f - width / 2.0f;
        this.yPosition = 0;
        this.currentSpeed = 0f;
        this.penalties = 0f;
        this.durability = 50;
        this.tiredness = 0f;
        this.finished = false;
        this.frameCounter = 0;
        this.lastFrameY = 0;
    }

    /**
     * Resets the boat's fastest leg time.
     */
    public void ResetFastestLegTime() {
        this.fastestLegTime = 0f;
    }

    // getters and setters

    /**
     * 
     * @param t Texture to use.
     */
    public void setTexture(Texture t) {
        this.texture = t;
    }

    /**
     * 
     * @param frames Texture frames for animation.
     */
    public void setTextureFrames(Texture[] frames) {
        this.textureFrames = frames;
    }

    /**
     * 
     * @return Float representing fastest race/leg time.
     */
    public float getFastestTime() {
        return this.fastestLegTime;
    }

    /**
     * 
     * @return Int representing x-position of boat.
     */
    public int getX() {
        return Math.round(this.xPosition);
    }

    /**
     * 
     * @return Int representing y-position of boat.
     */
    public int getY() {
        return Math.round(this.yPosition);
    }

    /**
     * 
     * @return Int representing the y coordinate range of the boat (length).
     */
    public int getHeight() {
        return this.height;
    }

    /**
     * 
     * @return String representing name of the boat.
     */
    public String getName() {
        return this.name;
    }

    /**
     * 
     * @return Boolean representing if the boat has finished the current race.
     */
    public boolean finished() {
        return this.finished;
    }

    /**
     * 
     * @param f Boolean representing if the boat has finished the race.
     */
    public void setFinished(boolean f) {
        this.finished = f;
    }

    /**
     * 
     * @return Float representing the current speed of the boat.
     */
    public float getCurrentSpeed() {
        return this.currentSpeed;
    }

    /**
     * 
     * @param finishY Y-position of the finish line.
     * @return Float representing the progress of the boat from 0 to 1.
     */
    public float getProgress(int finishY) {
        return Math.min((this.yPosition) / finishY, 1f);
    }

    /**
     * Implicitly sets the stats of the boat, given each attribute.
     * 
     * @param maxspeed        Top speed the boat can reach.
     * @param robustness      How resilient to obstacle damage the boat is.
     * @param acceleration    How much the speed increases each frame.
     * @param maneuverability How easily the boat can move left or right.
     */
    public void setStats(float maxspeed, int robustness, float acceleration, float maneuverability) {
        this.MAXSPEED = maxspeed;
        this.ROBUSTNESS = robustness;
        this.ACCELERATION = acceleration;
        this.MANEUVERABILITY = maneuverability;
    }

    /**
     * Interpolates predetermined stats from a boat label, and sets the stats based
     * on those.
     * 
     * @param boatLabel A character between A-G representing a specific boat.
     */
    public void setStats(char boatLabel) {
        float[] maxspeeds = { 2.5f, 2.0f, 2.5f, 2.5f, 2.0f, 3.5f, 2.5f };
        int[] robustnesses = { 2, 4, 1, 4, 8, 3, 5 };
        float[] accelerations = { 0.09375f, 0.03125f, 0.125f, 0.06254f, 0.046875f, 0.021875f, 0.03125f };
        float[] maneuverabilities = { 0.375f, 1.0f, 0.375f, 0.5f, 0.25f, 0.125f, 0.625f };

        int boatNo = boatLabel - 65;

        this.setStats(maxspeeds[boatNo], robustnesses[boatNo], accelerations[boatNo], maneuverabilities[boatNo]);
    }

    /**
     * 
     * @return Float representing the maneuverability of the boat.
     */
    public float getManeuverability() {
        return this.MANEUVERABILITY;
    }

    /**
     * 
     * @return Float representing the acceleration of the boat.
     */
    public float getAcceleration() {
        return this.ACCELERATION;
    }

    /**
     * 
     * @return Int representing the robustness of the boat.
     */
    public int getRobustness() {
        return this.ROBUSTNESS;
    }

    /**
     * 
     * @return Int representing the durability of the boat.
     */
    public int getDurability() {
        return this.durability;
    }

    /**
     * 
     * @return Int representing the maximum speed of the boat.
     */
    public float getMaxSpeed() {
        return this.MAXSPEED;
    }

    /**
     * 
     * @return Float representing the tiredness of the boat crew.
     */
    public float getTiredness() {
        return this.tiredness;
    }

    /**
     * 
     * @return Float representing the time penalty incurred for the current race.
     */
    public float getPenalty() {
        return this.penalties;
    }

    /**
     * 
     * @param penalty Float to add to the boat's penalty total for the current race.
     */
    public void applyPenalty(float penalty) {
        this.penalties += penalty;
    }

    /**
     * 
     * @param lanes Lanes object for the boat.
     */
    public void setLane(Lane[] lanes, int laneNo) {
        this.lanes = lanes;
        this.laneNo = laneNo;
        this.xPosition = this.lanes[this.laneNo].getRightBoundary() - (this.lanes[this.laneNo].getRightBoundary() - this.lanes[this.laneNo].getLeftBoundary()) / 2.0f - width / 2.0f;
    }

    /**
     * <p>
     * Assigns the selected boat template to the boat.
     * </p>
     * <p>
     * This includes stats and texture.
     * </p>
     *
     * @param boatNo Number of the boat template selected.
     */
    public void ChooseBoat(int boatNo) {
        ChooseBoat((char) (65 + boatNo));
    }

    /**
     * <p>
     * Assigns the selected boat template to the boat.
     * </p>
     * <p>
     * This includes stats and texture.
     * </p>
     *
     * @param boatChar Character of the boat template selected.
     */
    public void ChooseBoat(char boatChar) {
        this.label = boatChar;
        this.setTexture(new Texture(Gdx.files.internal("core/assets/boat" + label + " sprite1.png")));
        this.GenerateTextureFrames(label);
        this.setStats(label);
    }
}