GameScreen.java

package com.dragonboat.game;

import java.util.ArrayList;
import java.util.Date;
import java.util.Random;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.*;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.utils.viewport.StretchViewport;
import com.badlogic.gdx.utils.viewport.Viewport;

/**
 * Main Game Screen class for Dragon Boat Game. This is the main game loop,
 * handling all the game logic and rendering.
 */
public class GameScreen implements Screen {
    // ENVIRONMENT VARIABLES:
    private final Random rnd;
    protected DragonBoatGame game = null;

    // debug booleans
    private boolean debug_speed,debug_positions,debug_norandom,debug_verboseoutput;

    // game
    protected final Player player;
    protected final Course course;
    protected final Lane[] lanes;
    protected final ProgressBar progressBar;
    protected final Leaderboard leaderboard;
    protected final Opponent[] opponents;
    protected String[] times;
    protected boolean started = false;
    protected final float penalty = 0.016f;
    protected boolean ispaused = false;


    protected PauseMenu pauseMenu;
    protected Stage stage;

    // screen
    private final OrthographicCamera camera;
    private final Viewport viewport;

    // graphics
    private final SpriteBatch batch;
    private final Texture background;
    private final Texture healthBarFull;
    private final Texture healthBarEmpty;
    private final Texture staminaBarFull;
    private final Texture staminaBarEmpty;
    private final FreeTypeFontGenerator generator;
    private final FreeTypeFontGenerator.FreeTypeFontParameter parameter;
    private final BitmapFont font18,font28,font44;
    private final Skin skin = new Skin(Gdx.files.internal("core/assets/pixthulhu/skin/pixthulhu-ui.json"));

    // timing
    protected int backgroundOffset = 0;
    protected float totalDeltaTime = 0;

    // global parameters
    private final int WIDTH = 1080, HEIGHT = 720;

    /**
     * Sets up everything needed for a race to take place.
     *
     * @param game Represents the initial state of DragonBoatGame.
     */
    public GameScreen(DragonBoatGame game) {
        this(game, false);
    }

    /**
     * Sets up everything needed for a race to take place.
     * 
     * @param game Represents the initial state of DragonBoatGame.
     * @param loaded Represents whether the game has been loaded from a save or not.
     */
    public GameScreen(DragonBoatGame game, boolean loaded) {
        /*
         * Grab game objects from DragonBoatGame.
         */
        debug_speed = game.debug_speed;
        debug_positions = game.debug_positions;
        debug_norandom = game.debug_norandom;
        debug_verboseoutput = game.debug_verboseoutput;

        /*
        * creates stage on which pause menu will act upon when called
         */
        stage = new Stage();
        Gdx.input.setInputProcessor(stage);

        /*
        * creates pause menu
         */
        pauseMenu = new PauseMenu(game);
        pauseMenu.setSize(600,450);
        pauseMenu.setModal(true);
        pauseMenu.setVisible(false);
        pauseMenu.setMovable(false);
        pauseMenu.setPosition(Gdx.graphics.getWidth()/2 - pauseMenu.getWidth()/2,Gdx.graphics.getHeight()/2 - pauseMenu.getHeight()/2);

        stage.addActor(pauseMenu);

        this.game = game;
        this.player = this.game.player;
        this.course = this.game.course;
        this.lanes = this.game.lanes;
        this.progressBar = this.game.progressBar;
        this.opponents = this.game.opponents;
        this.rnd = this.game.rnd;

        ArrayList<Integer> possibleBoats = new ArrayList<>();
        if (!loaded) {
            for (int i = 0; i < lanes.length; i++) {
                if (i != game.playerChoice) {
                    possibleBoats.add(i);
                }
            }
            for (Opponent o : opponents) {
                int choice = o.SetRandomBoat(possibleBoats);
                possibleBoats.remove(choice);
            }
        }
        leaderboard = this.game.leaderboard;

        // setup view
        camera = new OrthographicCamera();
        viewport = new StretchViewport(WIDTH, HEIGHT, camera);

        // texture setting
        background = course.getTexture();
        batch = new SpriteBatch();
        generator = game.generator;
        parameter = game.parameter;
        parameter.size = 18;
        font18 = generator.generateFont(parameter);
        parameter.size = 28;
        font28 = generator.generateFont(parameter);
        parameter.size = 44;
        font44 = generator.generateFont(parameter);
        staminaBarFull = new Texture(Gdx.files.internal("core/assets/bar stamina yellow.png"));
        staminaBarEmpty = new Texture(Gdx.files.internal("core/assets/bar stamina grey.png"));
        healthBarFull = new Texture(Gdx.files.internal("core/assets/bar health yellow.png"));
        healthBarEmpty = new Texture(Gdx.files.internal("core/assets/bar health grey.png"));
    }

    /**
     * <p>
     * Rendering function for the game loop, handling all game logic and displaying
     * graphics.
     * </p>
     *
     * <p>
     * GAME LOOP
     * </p>
     *
     * <p>
     * - Spawns any Obstacles that need spawning.
     * </p>
     * <p>
     * - Update Player and Opponent positions.
     * </p>
     * <p>
     * - Check for collisions with Obstacles.
     * </p>
     * <p>
     * - Display Background and Obstacles
     * </p>
     * <p>
     * - Update Obstacle positions.
     * </p>
     * <p>
     * - Display Player, Player UI and Opponents.
     * </p>
     * <p>
     * - Display Progress Bar and checks which boats have finished.
     * </p>
     * <p>
     * - Display Player timer.
     * </p>
     * <p>
     * - Checks if all boats have finished, and displays a Leaderboard if so.
     * </p>
     *
     * @param deltaTime Time passed since render function was last run.
     */
    @Override
    public void render(float deltaTime) {
        String debug = "";

        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        /*
        * Checks whether Escape key is pressed,bringing up pause menu
         */
        if (Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)) {
            togglePause();
        }

        if (!ispaused) {
            /*
             * If the game has started, start incrementing time.
             */
            totalDeltaTime += started ? deltaTime : 0;
            /*
             * Check whether obstacles need to be spawned, and spawns them if so. Breaks
             * instantly if the game hasn't started, if the player has finished, or if there
             * are no more obstacles to be spawned.
             *
             * - IMPORTANT -
             * It should be noted that the obstacles currently use a
             * coordinate system relative to the screen, as they are always spawned at
             * HEIGHT + 40 (y = 760). This means all collision checking methods need to be
             * passed backgroundOffset to translate the object's y position.
             */
            for (int i = 0; i < course.getNoLanes(); i++) {
                if (!started || player.finished() || this.game.obstacleTimes[i].size() == 0)
                    break;
                if (this.game.obstacleTimes[i].get(0) - player.getY() + player.getHeight() < 1) {
                    String[] obstacleTypes = {"Goose", "Log"};
                    // spawn an obstacle in lane i.
                    int xCoord = lanes[i].getLeftBoundary()
                            + rnd.nextInt(lanes[i].getRightBoundary() - lanes[i].getLeftBoundary() - 15);
                    lanes[i].SpawnObstacle(game.spriteTextures, xCoord, HEIGHT + 40, obstacleTypes[rnd.nextInt(obstacleTypes.length)]);
                    // make sure obstacle is only spawned once.
                    this.game.obstacleTimes[i].remove(0);
                }
            }

            /*
             * Move player. Advance animation frame.
             */
            player.GetInput();
            player.MoveForward();
            if (player.getCurrentSpeed() > 0 && !started) {
                // detect start of game (might change this to a countdown)
                started = true;
            }

            /*
            * Move obstacles
             */
            for (Lane lane : lanes) {
                if (!started)
                    break;
                for (int j = 0; j < lane.obstacles.size(); j++) {
                    Obstacle o = lane.obstacles.get(j);
                    // If the background hasn't started moving yet, or if the player has reached the
                    // top of the course, move obstacle at set speed.
                    // Else add the player speed to the obstacle speed.
                    o.Move((0.4f * game.selectedDifficulty) + (backgroundOffset > 0
                                    && player.getY() + HEIGHT / 2 + player.getHeight() / 2 < course.getTexture().getHeight()
                                    ? player.getCurrentSpeed()
                                    : 0),
                            backgroundOffset);
                    if (o.getY() < -o.getHeight()) {
                        lane.RemoveObstacle(o);
                    }
                }
            }

            /*
             * Move opponents. Advance animation frame.
             */
            for (int i = 0; i < opponents.length; i++) {
                if (!started)
                    break;
                opponents[i].MoveForward();
                opponents[i].CheckCollisions(backgroundOffset);
                if (Math.round(totalDeltaTime) % 2 == 0) {
                    opponents[i].ai(backgroundOffset);
                }
            }

            /*
             * Increase the background offset so the player is centered.
             */
            if (player.getY() + HEIGHT / 2 + player.getHeight() / 2 > course.getTexture().getHeight()) {
                // Stop increasing the background offset when the player reaches the end of the
                // course.
            } else if (player.getY() + player.getHeight() / 2 > HEIGHT / 2) {
                // Start increasing the background offset when the player is above half the
                // window height.
                backgroundOffset = player.getY() + player.getHeight() / 2 - HEIGHT / 2;
            }

            player.CheckCollisions(backgroundOffset);

            /*
             * Check player boat is in their lane, if not apply penalties.
             */
            if (!player.CheckIfInLane() && !player.finished()) {
                player.applyPenalty(penalty);
                font28.setColor(Color.RED);
                batch.begin();
                font28.draw(batch, "Warning! Penalty applied for leaving lane", 240, 100);
                batch.end();
                font28.setColor(Color.WHITE);
            }
            /*
             * Check opponent boats are in their lanes, if not apply penalties.
             */
            for (Opponent opponent : opponents) {
                if (!opponent.CheckIfInLane() && !opponent.finished()) {
                    opponent.applyPenalty(penalty);
                }
            }

        } else {
            deltaTime = 0;
        }

        /*
         * Display background.
         */
        batch.begin();
        batch.draw(background, 0, 0, 0, background.getHeight() - HEIGHT - backgroundOffset, WIDTH, HEIGHT);
        batch.end();

        /*
         * Display obstacles
         */
        for (Lane lane : lanes) {
            if (!started)
                break;
            for (int j = 0; j < lane.obstacles.size(); j++) {
                Obstacle o = lane.obstacles.get(j);
                batch.begin();
                batch.draw(o.getTexture(), o.getX(), o.getY());
                batch.end();
            }
        }

        /*
         * Display player and player UI.
         */
        batch.begin();
        batch.draw(player.texture, player.getX(), player.getY() - backgroundOffset);
        batch.draw(staminaBarEmpty, player.lanes[player.laneNo].getLeftBoundary(), player.getY() - 20 - backgroundOffset);
        batch.draw(healthBarEmpty, player.lanes[player.laneNo].getLeftBoundary(), player.getY() - 40 - backgroundOffset);
        batch.draw(staminaBarFull, player.lanes[player.laneNo].getLeftBoundary(), player.getY() - 20 - backgroundOffset, 0, 0,
                Math.round(staminaBarFull.getWidth() * player.getTiredness() / Boat.MAX_TIREDNESS),
                staminaBarFull.getHeight());
        batch.draw(healthBarFull, player.lanes[player.laneNo].getLeftBoundary(), player.getY() - 40 - backgroundOffset, 0, 0,
                Math.round(healthBarFull.getWidth() * player.getDurability() / (float) Boat.MAX_DURABILITY),
                healthBarFull.getHeight());
        batch.end();

        if (debug_positions) debug += player.getName() + " pos: (" + player.getX() + "," + player.getY() + ")\n";
        if (debug_speed)
            debug += player.getName() + " speed: " + player.getCurrentSpeed() + " / " + player.getMaxSpeed() + "\n\n";

        /*
         * Display opponents.
         */
        for (Opponent o : opponents) {
            batch.begin();
            batch.draw(o.texture, o.getX(), o.getY() - backgroundOffset);
            batch.end();
            if (debug_positions) debug += o.getName() + " pos: (" + o.getX() + "," + o.getY() + ")\n";
            if (debug_speed)
                debug += o.getName() + " speed: " + o.getCurrentSpeed() + " / " + o.getMaxSpeed() + "\n\n";
        }

        /*
         * Display progress bar.
         */
        batch.begin();
        batch.draw(progressBar.getTexture(), WIDTH - progressBar.getTexture().getWidth() - 60,
                HEIGHT - progressBar.getTexture().getHeight() - 20);
        batch.end();

        /*
         * Get progress for each boat. Draw player and opponent icons on progress bar
         * with x coordinates respective to their progress.
         */
        float[] progress = progressBar.getProgress(course.getTexture().getHeight());
        for (int i = 1; i < progress.length; i++) {
            batch.begin();
            batch.draw(progressBar.getOpponentIcon(),
                    WIDTH - progressBar.getTexture().getWidth() - 50
                            + progress[i] * (progressBar.getTexture().getWidth() - 214),
                    HEIGHT - progressBar.getTexture().getHeight() / 2.0f - 10);
            batch.end();
        }

        batch.begin();
        batch.draw(progressBar.getPlayerIcon(),
                WIDTH - progressBar.getTexture().getWidth() - 50
                        + progress[0] * (progressBar.getTexture().getWidth() - 214),
                HEIGHT - progressBar.getTexture().getHeight() / 2.0f - 10);
        batch.end();

        /*
         * Display player time.
         */
        progressBar.IncrementTimer(deltaTime);
        batch.begin();
        font28.draw(batch, started ? progressBar.getPlayerTimeString() : "", WIDTH - 230, HEIGHT - 40);
        batch.end();

        /*
         * Check if player and each opponent has finished, and update their finished
         * booleans respectively.
         */
        if (progress[0] == 1 && !player.finished()) {
            player.setFinished(true);
            player.UpdateFastestTime(progressBar.getPlayerTime());
        }
        for (int i = 0; i < opponents.length; i++) {
            if (progress[i + 1] == 1 && !opponents[i].finished()) {
                opponents[i].setFinished(true);
                opponents[i].UpdateFastestTime(progressBar.getTime());
            }
        }

        /*
         * Display debug stats.
         */
        if (debug_positions || debug_speed) {
            batch.begin();
            font18.draw(batch, debug, 5, HEIGHT - 60);
            batch.end();
        }

        if (debug_verboseoutput) {
            System.out.println("-----------------------");
            System.out.println("Total time: " + totalDeltaTime + "\nDelta time: " + deltaTime);
            System.out.println("-----------------------");
            System.out.println(" -- Variables --\n"
                    + "backgroundOffset: " + backgroundOffset);
            for (int i = 0; i < lanes.length; i++) {
                System.out.println("Lane " + i + " obstacles: " + lanes[i].obstacles.size());
            }
            System.out.println("\n\n\n");
        }

        /*
         * Check if all boats have passed the finish line, if so, generate the
         * leaderboard.
         */
        if (progressBar.allFinished(course.getTexture().getHeight())) {
            // Display leaderboard, if on the third leg, display top 3 boats.
            if (game.difficulty < 3) {
                batch.begin();
                batch.draw(leaderboard.getTexture(), WIDTH / 2.0f - leaderboard.getTexture().getWidth() / 2.0f,
                        HEIGHT / 2.0f - leaderboard.getTexture().getHeight() / 2.0f);
                batch.end();
                this.times = leaderboard.getTimes(opponents.length + 1);
                for (int i = 0; i < opponents.length + 1; i++) {
                    batch.begin();
                    font44.draw(batch, this.times[i], WIDTH / 2.0f - leaderboard.getTexture().getWidth() / 3.0f,
                            620 - (75 * i));
                    batch.end();
                }
                batch.begin();
                font28.draw(batch, "Click anywhere to progress to next leg.", 200, 40);
                batch.end();
                /*
                 * Defines how to handle keyboard and mouse inputs.
                 */
                Gdx.input.setInputProcessor(new InputAdapter() {
                    @Override
                    public boolean touchDown(int screenX, int screenY, int pointer, int button) {
                        game.advanceLeg();
                        return super.touchUp(screenX, screenY, pointer, button);
                    }
                });
            } else if (game.difficulty == 3) {
                batch.begin();
                batch.draw(leaderboard.getTexture(), WIDTH / 2.0f - leaderboard.getTexture().getWidth() / 2.0f,
                        HEIGHT / 2.0f - leaderboard.getTexture().getHeight() / 2.0f);
                batch.end();
                this.times = leaderboard.getTimes(opponents.length + 1);
                for (int i = 0; i < opponents.length + 1; i++) {
                    if (i < 3)
                        font44.setColor(Color.GOLD);
                    else
                        font44.setColor(Color.WHITE);

                    batch.begin();
                    font44.draw(batch, this.times[i], WIDTH / 2.0f - leaderboard.getTexture().getWidth() / 3.0f,
                            620 - (75 * i));
                    batch.end();
                }
                if (this.times[0].startsWith("Player") || this.times[1].startsWith("Player")
                        || this.times[2].startsWith("Player")) {
                    batch.begin();
                    font28.draw(batch, "Click anywhere to progress to the final!", 200, 40);
                    batch.end();
                    /*
                     * Defines how to handle keyboard and mouse inputs.
                     */
                    Gdx.input.setInputProcessor(new InputAdapter() {
                        @Override
                        public boolean touchDown(int screenX, int screenY, int pointer, int button) {
                            game.advanceLeg();
                            return super.touchUp(screenX, screenY, pointer, button);
                        }
                    });
                } else {
                    game.endGame();
                }
            } else {
                game.endGame();
            }
        }


        /*
        * tells stage to act and draw itself
         */
        stage.act(Math.min(Gdx.graphics.getDeltaTime(), 1/30f));
        stage.draw();
    }

    /**
     * Resizes the game screen.
     * 
     * @param width  Width of the screen.
     * @param height Height of the screen.
     */
    @Override
    public void resize(int width, int height) {
        viewport.update(width, height, true);
        batch.setProjectionMatrix(camera.combined);
    }

    public void togglePause() {
        pauseMenu.setVisible(!pauseMenu.isVisible());
        ispaused = !ispaused;
    }

    @Override
    public void pause() {
        if (!ispaused) togglePause();
    }

    @Override
    public void resume() {
        if (ispaused) togglePause();
    }

    @Override
    public void hide() {

    }

    /**
     * Disposes of the screen when it is no longer needed.
     */
    @Override
    public void dispose() {
        batch.dispose();
        stage.dispose();
        background.dispose();
        player.texture.dispose();
        for (Lane lane : lanes) {
            for (int j = 0; j < lane.obstacles.size(); j++) {
                lane.obstacles.get(j).getTexture().dispose();
            }
        }
        progressBar.getTexture().dispose();
        progressBar.getOpponentIcon().dispose();
        progressBar.getPlayerIcon().dispose();
        staminaBarFull.dispose();
        staminaBarEmpty.dispose();
        healthBarEmpty.dispose();
        healthBarFull.dispose();
        generator.dispose();
        font28.dispose();
        font44.dispose();
        leaderboard.getTexture().dispose();

    }

    @Override
    public void show() {

    }
}