[FEAT] Complete basic functionality including multiplayer and styling

This commit is contained in:
2023-04-21 23:29:14 +01:00
parent bbb9ebcf4e
commit 839c7ee5c8
19 changed files with 1367 additions and 111 deletions

View File

@@ -13,4 +13,5 @@ module uk.mgrove.ac.soton.comp1206 {
exports uk.mgrove.ac.soton.comp1206.event;
exports uk.mgrove.ac.soton.comp1206.component;
exports uk.mgrove.ac.soton.comp1206.game;
opens uk.mgrove.ac.soton.comp1206.component to javafx.fxml;
}

View File

@@ -0,0 +1,171 @@
package uk.mgrove.ac.soton.comp1206.component;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.network.Communicator;
import uk.mgrove.ac.soton.comp1206.util.Multimedia;
public class Chat extends VBox {
/**
* Logger
*/
private static final Logger logger = LogManager.getLogger(Chat.class);
/**
* Scroll pane to hold messages
*/
private final ScrollPane messagesContainer = new ScrollPane();
/**
* Container for messages in channel
*/
private final VBox messages = new VBox();
/**
* Text field for messages to send to channels
*/
private final TextField newMessageText = new TextField();
/**
* Button to send message
*/
private final Text sendMessage = new Text("Send");
/**
* User's current nickname
*/
private String nickname = "";
/**
* The communicator to use
*/
private Communicator communicator;
/**
* Whether chat should scroll to bottom next time layout updates
*/
private boolean scrollToBottom;
/**
* Whether the last chat message is from the local user
*/
private boolean lastMessageFromMe;
/**
* Initialise the chat container with a communicator
* @param communicator the communicator to use
*/
public Chat(Communicator communicator, boolean compact) {
this.communicator = communicator;
newMessageText.setPromptText("Type a message...");
newMessageText.setOnAction(this::sendMessage);
messages.setSpacing(4);
messagesContainer.setContent(messages);
messagesContainer.setFitToWidth(true);
messagesContainer.setFitToHeight(true);
messagesContainer.setPrefHeight(320);
messagesContainer.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
messagesContainer.setVvalue(1.0);
messagesContainer.getStyleClass().add("scroller");
var newMessageContainer = new HBox();
newMessageContainer.setAlignment(Pos.CENTER_LEFT);
newMessageContainer.setSpacing(8);
sendMessage.getStyleClass().add("channelItem");
sendMessage.setOnMouseClicked(this::sendMessage);
if (compact) newMessageText.setPrefWidth(140);
else newMessageText.setPrefWidth(400);
logger.info("New message text has pref width of: {}", newMessageText.getPrefWidth());
newMessageContainer.getChildren().addAll(newMessageText, sendMessage);
getChildren().addAll(messagesContainer, newMessageContainer);
communicator.addListener((message) -> {
if (message.matches("^MSG .+:.+$")) {
var data = message.replaceFirst("MSG ", "").split(":");
logger.info("Received message: {} from: {}", data[1], data[0]);
if (messagesContainer.getVvalue() == 1.0f) scrollToBottom = true;
Platform.runLater(() -> {
var user = new Text(data[0] + " ");
user.getStyleClass().add("myname");
var messageText = new Text(data[1]);
var messageContainer = new TextFlow(user, messageText);
messages.getChildren().add(messageContainer);
});
if (!lastMessageFromMe) Multimedia.playAudio("sounds/message.wav");
else lastMessageFromMe = false;
}
});
}
/**
* Send a message to a channel
*
* @param event triggering action event
*/
private void sendMessage(Event event) {
if (!newMessageText.getText().isBlank()) {
if (newMessageText.getText().startsWith("/nick ")) {
var newNickname = newMessageText.getText().replaceFirst("/nick ", "");
logger.info("Setting new nickname: {}", newNickname);
communicator.send("NICK " + newNickname);
} else {
logger.info("Sending message: {}", newMessageText.getText());
communicator.send("MSG " + newMessageText.getText());
lastMessageFromMe = true;
}
newMessageText.clear();
}
}
/**
* Scroll to bottom of messages list.
*/
public void scrollToBottom() {
if (!scrollToBottom) return;
logger.info("Scrolling to bottom of chat");
messagesContainer.setVvalue(1.0);
scrollToBottom = false;
}
/**
* Focus the message input field
*/
public void focusInputField() {
newMessageText.requestFocus();
}
/**
* Set whether chat elements should be focus traversable
* @param traversable whether the elements should be focus traversable
*/
public void setChatFocusTraversable(boolean traversable) {
newMessageText.setFocusTraversable(traversable);
sendMessage.setFocusTraversable(traversable);
}
/**
* Clear the message history
*/
public void clearMessages() {
messages.getChildren().clear();
}
}

View File

@@ -0,0 +1,65 @@
package uk.mgrove.ac.soton.comp1206.component;
import javafx.application.Platform;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import javafx.util.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Map;
/**
* Component to display online leaderboard in multiplayer games
*/
public class Leaderboard extends ScoresList {
/**
* Logger
*/
private static final Logger logger = LogManager.getLogger(Leaderboard.class);
/**
* Scores - pair of username with pair of score and lives
*/
private final SimpleListProperty<Pair<String,Pair<Integer,Integer>>> scores = new SimpleListProperty<>(FXCollections.observableArrayList());
/**
* Initialise the leaderboard
*/
public Leaderboard(String titleText) {
super();
title = new Text(titleText);
title.getStyleClass().add("heading");
getChildren().add(title);
reveal();
scores.addListener((ListChangeListener<? super Pair<String,Pair<Integer, Integer>>>) change -> Platform.runLater(() -> {
logger.info("Detected change in scores list: {}", change);
getChildren().clear();
getChildren().add(title);
for (var pair : scores.get()) {
var nameField = new Text(pair.getKey());
if (pair.getValue().getValue() == -1) nameField.setStrikethrough(true);
else nameField.setText(nameField.getText() + " (" + pair.getValue().getValue().toString() + ")");
var scoreField = new Text(pair.getValue().getKey().toString());
scoreField.getStyleClass().add("myscore");
var individualScoreContainer = new HBox(nameField, scoreField);
individualScoreContainer.setSpacing(8);
getChildren().add(individualScoreContainer);
}
}));
}
/**
* Bind scores to external property
* @param scores external property to bind to
*/
public void bindLeaderboardScores(SimpleListProperty<Pair<String,Pair<Integer,Integer>>> scores) {
this.scores.bindBidirectional(scores);
}
}

View File

@@ -1,6 +1,7 @@
package uk.mgrove.ac.soton.comp1206.component;
import javafx.animation.FadeTransition;
import javafx.application.Platform;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
@@ -26,28 +27,42 @@ public class ScoresList extends VBox {
/**
* Scores
*/
private final SimpleListProperty<Pair<String,Integer>> scores;
protected final SimpleListProperty<Pair<String,Integer>> scores = new SimpleListProperty<>(FXCollections.observableArrayList());
/**
* Title for scores list
*/
protected Text title;
/**
* Initialise the scores list
*/
public ScoresList() {
public ScoresList(String titleText) {
logger.info("Building scores list");
ObservableList<Pair<String,Integer>> observableScoresList = FXCollections.observableArrayList();
scores = new SimpleListProperty<>(observableScoresList);
setOpacity(0);
scores.addListener((ListChangeListener<? super Pair<String, Integer>>) change -> {
title = new Text(titleText);
title.getStyleClass().add("title");
getChildren().add(title);
scores.addListener((ListChangeListener<? super Pair<String, Integer>>) change -> Platform.runLater(() -> {
logger.info("Detected change in scores list: {}", change);
getChildren().clear();
getChildren().add(title);
for (var pair : scores.get()) {
var score = new HBox();
score.getChildren().addAll(new Text(pair.getKey()), new Text(pair.getValue().toString()));
getChildren().add(score);
var individualScoreContainer = new HBox();
individualScoreContainer.getChildren().addAll(new Text(pair.getKey()), new Text(pair.getValue().toString()));
getChildren().add(individualScoreContainer);
}
});
}));
}
/**
* Empty constructor
*/
public ScoresList() {
}
/**

View File

@@ -1,4 +1,4 @@
package uk.mgrove.ac.soton.comp1206.ui;
package uk.mgrove.ac.soton.comp1206.component;
import javafx.beans.property.IntegerProperty;
import javafx.scene.layout.VBox;
@@ -28,19 +28,24 @@ public class StatsMenu extends VBox {
Text score1 = new Text();
score1.textProperty().bind(score.asString());
score1.getStyleClass().add("score");
Text level1 = new Text();
level1.textProperty().bind(level.asString());
level1.getStyleClass().add("level");
Text lives1 = new Text();
lives1.textProperty().bind(lives.asString());
lives1.getStyleClass().add("lives");
Text multiplier1 = new Text();
multiplier1.textProperty().bind(multiplier.asString());
multiplier1.getStyleClass().add("level");
var scoreHeader = new Text("Score");
var levelHeader = new Text("Level");
var livesHeader = new Text("Lives");
var multiplierHeader = new Text("Multiplier");
getChildren().addAll(scoreHeader, score1,levelHeader, level1,livesHeader, lives1,multiplierHeader, multiplier1);
getChildren().addAll(new VBox(scoreHeader, score1),new VBox(livesHeader, lives1),new VBox(levelHeader, level1),new VBox(multiplierHeader, multiplier1));
setSpacing(8);
}
}

View File

@@ -0,0 +1,15 @@
package uk.mgrove.ac.soton.comp1206.event;
import uk.mgrove.ac.soton.comp1206.component.GameBlock;
/**
* Listener for when multiplayer games fail - e.g. due to network errors
*/
public interface GameFailureListener {
/**
* Handle a game failure
*/
public void gameFail();
}

View File

@@ -3,14 +3,13 @@ package uk.mgrove.ac.soton.comp1206.game;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.util.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.component.GameBlock;
import uk.mgrove.ac.soton.comp1206.component.GameBlockCoordinate;
import uk.mgrove.ac.soton.comp1206.event.GameLoopListener;
import uk.mgrove.ac.soton.comp1206.event.LineClearedListener;
import uk.mgrove.ac.soton.comp1206.event.NextPieceListener;
import uk.mgrove.ac.soton.comp1206.event.ShowScoresSceneListener;
import uk.mgrove.ac.soton.comp1206.event.*;
import uk.mgrove.ac.soton.comp1206.util.Multimedia;
import java.util.*;
@@ -49,12 +48,12 @@ public class Game {
/**
* Current game piece player is using
*/
private GamePiece currentPiece;
protected GamePiece currentPiece;
/**
* Next game piece for player to use
*/
private GamePiece followingPiece;
protected GamePiece followingPiece;
/**
* Player's current score
@@ -76,7 +75,7 @@ public class Game {
/**
* Listener for when new piece is received by the game
*/
private NextPieceListener nextPieceListener;
protected NextPieceListener nextPieceListener;
/**
* Listener for when lines of blocks are cleared
@@ -128,7 +127,7 @@ public class Game {
currentPiece = followingPiece;
followingPiece = spawnPiece();
if (nextPieceListener != null) nextPieceListener.nextPiece(currentPiece, followingPiece);
logger.info("Next piece is: {} and following piece is: {}", currentPiece, followingPiece);
logger.info("New current piece is: {} and following piece is: {}", currentPiece, followingPiece);
return currentPiece;
}
@@ -185,7 +184,7 @@ public class Game {
/**
* Start new game loop
*/
private void scheduleGameLoop() {
protected void scheduleGameLoop() {
logger.info("Scheduling game loop");
if (gameTimer != null) gameTimer.cancel();
@@ -209,7 +208,6 @@ public class Game {
public void endGame() {
if (gameTimer != null) gameTimer.cancel();
Multimedia.playAudio("sounds/explode.wav");
// TODO: do processing to end game - switch to ScoresScene
}
/**
@@ -313,8 +311,8 @@ public class Game {
* @param lines number of lines cleared
* @param blocks number of blocks cleared
*/
private void score(int lines, int blocks) {
logger.info("Updating score");
protected void score(int lines, int blocks) {
logger.info("Updating score with lines cleared: {}, blocks cleared: {}", lines, blocks);
var addScore = lines * blocks * 10 * multiplier.get();
score.set(score.get() + addScore);
@@ -506,4 +504,17 @@ public class Game {
showScoresListener = listener;
}
/**
* Get leaderboard scores property - always null in this class
* @return null
*/
public SimpleListProperty<Pair<String, Pair<Integer, Integer>>> leaderboardScoresProperty() {
return null;
}
/**
* Set listener for game failure - but in this class does nothing
* @param listener listener to set
*/
public void setOnGameFail(GameFailureListener listener) {}
}

View File

@@ -147,6 +147,9 @@ public class Grid {
}
}
/**
* Clear the grid
*/
public void clearGrid() {
for (var x = 0; x < getCols(); x++) {
for (var y = 0; y < getRows(); y++) {

View File

@@ -0,0 +1,231 @@
package uk.mgrove.ac.soton.comp1206.game;
import javafx.application.Platform;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.scene.control.Alert;
import javafx.util.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.event.GameFailureListener;
import uk.mgrove.ac.soton.comp1206.network.Communicator;
import uk.mgrove.ac.soton.comp1206.util.Multimedia;
import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Scanner;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class MultiplayerGame extends Game {
/**
* Logger
*/
private static final Logger logger = LogManager.getLogger(MultiplayerGame.class);
/**
* Communicator to communicate with server
*/
private final Communicator communicator;
/**
* Queue of pieces to be played in the game
*/
private BlockingQueue<GamePiece> pieceQueue = new LinkedBlockingQueue<>();
/**
* Scores for the leaderboard
*/
private final SimpleListProperty<Pair<String,Pair<Integer,Integer>>> leaderboardScores = new SimpleListProperty<>(FXCollections.observableArrayList(new ArrayList<>()));
/**
* Listener for game failing
*/
private GameFailureListener gameFailureListener;
/**
* Create a new game with the specified rows and columns. Creates a corresponding grid model.
*
* @param cols number of columns
* @param rows number of rows
*/
public MultiplayerGame(int cols, int rows, Communicator communicator) {
super(cols, rows);
this.communicator = communicator;
communicator.addListener(this::handleCommunicatorMessage);
for (var i=0; i<5; i++) communicator.send("PIECE");
}
/**
* Retrieve next game piece and request another from the server
* @return next piece
*/
@Override
public GamePiece spawnPiece() {
communicator.send("PIECE");
GamePiece nextPiece = null;
try {
synchronized (pieceQueue) {
while (pieceQueue.size() == 0) {
wait();
}
nextPiece = pieceQueue.poll();
}
} catch (InterruptedException e) {
logger.error("Unable to retrieve piece from queue - waiting interrupted: {}", e);
Platform.runLater(() -> {
endGame();
if (gameFailureListener != null) gameFailureListener.gameFail();
var error = new Alert(Alert.AlertType.ERROR, "Unable to retrieve game data from server in time - perhaps your connection is slow?");
error.showAndWait();
});
}
logger.info("Picking next piece: {}", nextPiece.toString());
return nextPiece;
}
/**
* Handle incoming messages from the communicator
* @param message incoming message
*/
private void handleCommunicatorMessage(String message) {
if (message.startsWith("PIECE ")) {
try {
var newPiece = GamePiece.createPiece(Integer.parseInt(message.replaceFirst("PIECE ", "")));
synchronized (pieceQueue) {
pieceQueue.add(newPiece);
}
logger.info("Generated piece from server: {}", newPiece.toString());
} catch (NumberFormatException ex) {
logger.error("Unable to generate piece from server - piece value not a number: {}", ex);
}
} else if (message.startsWith("SCORES ")) {
logger.info("Setting scores from server");
leaderboardScores.set(parseScores(message.replaceFirst("SCORES ", "")));
} else if (message.startsWith("SCORE ")) {
var info = message.replaceFirst("SCORE ", "").split(":");
for (var i=0; i<leaderboardScores.size(); i++) {
if (leaderboardScores.get(i).getKey().equals(info[0])) {
logger.info("Updating score for: {} to: {}", info[0], info[1]);
leaderboardScores.set(i, new Pair<>(info[0], new Pair<>(Integer.valueOf(info[1]), leaderboardScores.get(i).getValue().getValue())));
break;
}
}
} else if (message.startsWith("DIE ")) {
var username = message.replaceFirst("DIE ", "");
for (var i=0; i<leaderboardScores.size(); i++) {
if (leaderboardScores.get(i).getKey().equals(username)) {
logger.info("Updating leaderboard as player has died: {}", username);
leaderboardScores.set(i, new Pair<>(username, new Pair<>(leaderboardScores.get(i).getValue().getKey(), -1)));
break;
}
}
}
}
/**
* Parse high scores from a string to list property
* @param data string to parse from
* @return list property containing scores loaded
*/
private static SimpleListProperty<Pair<String,Pair<Integer,Integer>>> parseScores(String data) {
logger.info("Parsing scores");
var scores = new SimpleListProperty<Pair<String,Pair<Integer,Integer>>>(FXCollections.observableArrayList());
var scanner = new Scanner(data);
while (scanner.hasNextLine()) {
var line = scanner.nextLine();
if (line.matches("^.+:[0-9]+:([0-9]+|DEAD)$")) {
var info = line.split(":");
var lives = info[2].equals("DEAD") ? -1 : Integer.valueOf(info[2]);
scores.add(new Pair<>(info[0], new Pair<>(Integer.valueOf(info[1]), lives)));
}
}
scanner.close();
logger.info("Parsed scores: {}", scores);
return scores;
}
/**
* Get the leaderboard scores property
* @return property for leaderboard scores
*/
public SimpleListProperty<Pair<String,Pair<Integer,Integer>>> leaderboardScoresProperty() {
return leaderboardScores;
}
/**
* End game
*/
@Override
public void endGame() {
super.endGame();
logger.info("Sending die message to server");
communicator.send("DIE");
}
/**
* Update the score, multiplier, and level depending on the number of lines and blocks that have been cleared
* @param lines number of lines cleared
* @param blocks number of blocks cleared
*/
@Override
protected void score(int lines, int blocks) {
var oldScore = score.get();
super.score(lines, blocks);
if (oldScore != score.get()) {
logger.info("Updating server with new score: {}", score.get());
communicator.send("SCORE " + score.get());
}
}
/**
* Set the player's remaining lives
*/
@Override
public void setLives(int lives) {
this.lives.set(lives);
communicator.send("LIVES " + lives);
}
/**
* Play current piece at given coordinates
* @param x x-coordinate
* @param y y-coordinate
*/
@Override
public void dropPiece(int x, int y) {
var canPlayPiece = grid.canPlayPiece(currentPiece,x,y);
super.dropPiece(x,y);
if (canPlayPiece) {
var communicatorMessage = "BOARD";
for (var i=0; i<grid.getRows(); i++) {
for (var j=0; j<grid.getCols(); j++) {
communicatorMessage += " " + grid.get(i,j);
}
}
logger.info("Sending current board to server");
communicator.send(communicatorMessage);
}
}
/**
* Set listener for game failure
* @param listener listener to set
*/
@Override
public void setOnGameFail(GameFailureListener listener) {
this.gameFailureListener = listener;
}
}

View File

@@ -5,6 +5,7 @@ import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Pos;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
@@ -20,7 +21,7 @@ import uk.mgrove.ac.soton.comp1206.game.Game;
import uk.mgrove.ac.soton.comp1206.game.GamePiece;
import uk.mgrove.ac.soton.comp1206.ui.GamePane;
import uk.mgrove.ac.soton.comp1206.ui.GameWindow;
import uk.mgrove.ac.soton.comp1206.ui.StatsMenu;
import uk.mgrove.ac.soton.comp1206.component.StatsMenu;
import uk.mgrove.ac.soton.comp1206.util.Multimedia;
/**
@@ -41,22 +42,22 @@ public class ChallengeScene extends BaseScene {
/**
* PieceBoard to display current piece
*/
private PieceBoard currentPieceBoard;
protected PieceBoard currentPieceBoard;
/**
* PieceBoard to display next piece
*/
private PieceBoard followingPieceBoard;
protected PieceBoard followingPieceBoard;
/**
* GameBoard for main game
*/
private GameBoard board;
protected GameBoard board;
/**
* Progress bar to indicate time left until game loop executes (i.e. life is lost)
*/
private Rectangle timerCountdownBar;
protected Rectangle timerCountdownBar;
/**
* Fill transition for timer countdown bar
@@ -68,10 +69,15 @@ public class ChallengeScene extends BaseScene {
*/
private ScaleTransition timerCountdownBarScaleTransition;
/**
* Main pane for UI
*/
protected BorderPane mainPane;
/**
* Top high score property
*/
private final IntegerProperty highScore = new SimpleIntegerProperty(0);
protected final IntegerProperty highScore = new SimpleIntegerProperty(0);
/**
* Create a new Single Player challenge scene
@@ -99,10 +105,11 @@ public class ChallengeScene extends BaseScene {
challengePane.getStyleClass().add("menu-background");
root.getChildren().add(challengePane);
var mainPane = new BorderPane();
mainPane = new BorderPane();
challengePane.getChildren().add(mainPane);
board = new GameBoard(game.getGrid(),gameWindow.getWidth()/2f,gameWindow.getWidth()/2f);
board.setFocusTraversable(true);
board.setOnRightClicked(game::rotateCurrentPiece);
mainPane.setCenter(board);
@@ -115,20 +122,22 @@ public class ChallengeScene extends BaseScene {
var topHighScoreText = new Text();
topHighScoreText.textProperty().bind(highScore.asString());
topHighScoreText.getStyleClass().add("hiscore");
highScore.set(getHighScore());
var topHighScoreContainer = new VBox(new Text("Top high score"), topHighScoreText);
game.scoreProperty().addListener((ChangeListener<? super Number>) (change, oldValue, newValue) -> {
if (newValue.intValue() > highScore.get()) highScore.set(newValue.intValue());
});
var rightMenu = new VBox();
rightMenu.getChildren().addAll(statsMenu,currentPieceBoard,followingPieceBoard,topHighScoreText);
rightMenu.setSpacing(16);
rightMenu.getChildren().addAll(statsMenu,currentPieceBoard,followingPieceBoard,topHighScoreContainer);
mainPane.setRight(rightMenu);
timerCountdownBar = new Rectangle(gameWindow.getWidth()/2f,48);
BorderPane.setAlignment(timerCountdownBar, Pos.BOTTOM_CENTER);
// TODO: alignment of timer bar
mainPane.setBottom(timerCountdownBar);
@@ -142,7 +151,7 @@ public class ChallengeScene extends BaseScene {
* Handle when a block is clicked
* @param gameBlock the Game Block that was clocked
*/
private void blockClicked(GameBlock gameBlock) {
protected void blockClicked(GameBlock gameBlock) {
game.blockClicked(gameBlock);
}
@@ -150,7 +159,8 @@ public class ChallengeScene extends BaseScene {
* Handle keypress for game keyboard controls
* @param event the keypress
*/
private void handleKeyboardControls(KeyEvent event) {
protected void handleKeyboardControls(KeyEvent event) {
logger.info("Handling keyboard input: {}", event.getCode());
switch (event.getCode()) {
case ESCAPE -> returnToMenu();
case ENTER, X -> game.dropPiece(board.getXFocus(),board.getYFocus());
@@ -200,13 +210,13 @@ public class ChallengeScene extends BaseScene {
gameWindow.startMenu();
}
private void showScores(Game game) {
protected void showScores(Game game) {
if (timerCountdownBarFillTransition != null) timerCountdownBarFillTransition.stop();
if (timerCountdownBarScaleTransition != null) timerCountdownBarScaleTransition.stop();
gameWindow.startScores(game);
}
private void setCurrentPiece(GamePiece currentPiece, GamePiece followingPiece) {
protected void setCurrentPiece(GamePiece currentPiece, GamePiece followingPiece) {
currentPieceBoard.displayPiece(currentPiece);
followingPieceBoard.displayPiece(followingPiece);
}
@@ -215,7 +225,7 @@ public class ChallengeScene extends BaseScene {
* Reset the countdown progress bar with new game timer delay when the game loop is scheduled
* @param timerDelay delay until game loop is executed
*/
private void scheduleCountdown(int timerDelay) {
protected void scheduleCountdown(int timerDelay) {
logger.info("Scheduling UI countdown timer");
timerCountdownBar.setArcHeight(32);
@@ -233,7 +243,7 @@ public class ChallengeScene extends BaseScene {
* Get top high score
* @return top high score
*/
private int getHighScore() {
protected int getHighScore() {
return ScoresScene.getScores("scores.txt").get(0).getValue();
}

View File

@@ -1,5 +1,6 @@
package uk.mgrove.ac.soton.comp1206.scene;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
@@ -8,6 +9,7 @@ import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.component.PieceBoard;
@@ -54,20 +56,18 @@ public class InstructionsScene extends BaseScene {
*/
@Override
public void build() {
// TODO: styling and insets
logger.info("Building " + this.getClass().getName());
root = new GamePane(gameWindow.getWidth(),gameWindow.getHeight());
var menuPane = new StackPane();
menuPane.setMaxWidth(gameWindow.getWidth());
menuPane.setMaxHeight(gameWindow.getHeight());
menuPane.getStyleClass().add("menu-background");
root.getChildren().add(menuPane);
var instructionsPane = new StackPane();
instructionsPane.setMaxWidth(gameWindow.getWidth());
instructionsPane.setMaxHeight(gameWindow.getHeight());
instructionsPane.getStyleClass().add("menu-background");
root.getChildren().add(instructionsPane);
var mainPane = new BorderPane();
menuPane.getChildren().add(mainPane);
instructionsPane.getChildren().add(mainPane);
//Awful title
var title = new Text("How to Play");
@@ -75,10 +75,18 @@ public class InstructionsScene extends BaseScene {
mainPane.setTop(title);
BorderPane.setAlignment(title, Pos.TOP_CENTER);
var instructionsText = new Text("TetrECS is a fast-paced gravity-free block placement game, where you must survive by clearing rows through careful placement of the upcoming blocks before the time runs out. Lose all 3 lives and you're destroyed!");
instructionsText.getStyleClass().add("instructions");
instructionsText.setWrappingWidth(700);
instructionsText.setTextAlignment(TextAlignment.CENTER);
var instructions = new ImageView(getClass().getResource("/images/Instructions.png").toExternalForm());
instructions.setPreserveRatio(true);
instructions.setFitHeight(gameWindow.getHeight()*0.55);
mainPane.setCenter(instructions);
instructions.setFitHeight(gameWindow.getHeight()*0.5);
var instructionsContainer = new VBox(instructionsText, instructions);
instructionsContainer.setAlignment(Pos.TOP_CENTER);
mainPane.setCenter(instructionsContainer);
var pieceGrid = new GridPane();
pieceGrid.setVgap(8);

View File

@@ -0,0 +1,81 @@
package uk.mgrove.ac.soton.comp1206.scene;
import javafx.animation.FadeTransition;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.ui.GamePane;
import uk.mgrove.ac.soton.comp1206.ui.GameWindow;
import uk.mgrove.ac.soton.comp1206.util.Multimedia;
import java.util.Timer;
import java.util.TimerTask;
public class LoadingScene extends BaseScene {
/**
* Logger
*/
private static final Logger logger = LogManager.getLogger(LoadingScene.class);
/**
* Fade transition for ECS Games logo
*/
private FadeTransition logoFade;
/**
* Create a new scene, passing in the GameWindow the scene will be displayed in
*
* @param gameWindow the game window
*/
public LoadingScene(GameWindow gameWindow) {
super(gameWindow);
}
/**
* Build the layout of the scene
*/
@Override
public void build() {
logger.info("Building " + this.getClass().getName());
root = new GamePane(gameWindow.getWidth(),gameWindow.getHeight());
var logo = new ImageView(getClass().getResource("/images/ECSGames.png").toExternalForm());
logo.setPreserveRatio(true);
logo.setFitWidth(380);
root.getChildren().add(logo);
StackPane.setAlignment(logo, Pos.CENTER);
logoFade = new FadeTransition(Duration.millis(2000), logo);
logoFade.setFromValue(0);
logoFade.setToValue(1);
logo.setOpacity(0);
}
/**
* Initialise this scene. Called after creation
*/
@Override
public void initialise() {
logger.info("Initialising " + this.getClass().getName());
logoFade.play();
Multimedia.playAudio("sounds/intro.mp3");
TimerTask loadMenuTimerTask = new TimerTask() {
@Override
public void run() {
Platform.runLater(() -> gameWindow.startMenu());
}
};
var loadMenuTimer = new Timer("Timer");
loadMenuTimer.schedule(loadMenuTimerTask, 4000);
}
}

View File

@@ -0,0 +1,304 @@
package uk.mgrove.ac.soton.comp1206.scene;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.component.Chat;
import uk.mgrove.ac.soton.comp1206.ui.GamePane;
import uk.mgrove.ac.soton.comp1206.ui.GameWindow;
import java.util.Timer;
import java.util.TimerTask;
import java.util.function.Predicate;
public class LobbyScene extends BaseScene {
/**
* Logger
*/
private static final Logger logger = LogManager.getLogger(LobbyScene.class);
/**
* Container for all channels
*/
private final VBox allChannels = new VBox();
/**
* Container for controls to create new channel
*/
private final VBox addChannelContainer = new VBox();
/**
* Button to open prompt to create new channel
*/
private final Text promptToCreateChannel = new Text("Create channel");
private final TextField newChannelName = new TextField();
/**
* Button to create new channel
*/
private final Text createChannel = new Text("Create");
/**
* Container to list all users in channel
*/
private final HBox channelUsers = new HBox();
/**
* Main content of scene (i.e. current channel actions)
*/
private final VBox mainContent = new VBox();
/**
* Current channel
*/
private String currentChannel;
/**
* Whether use is host of current channel
*/
private boolean isChannelHost = false;
/**
* Chat container
*/
private Chat chat;
/**
* Container for control buttons when in a channel
*/
private final HBox channelFunctionButtons = new HBox();
/**
* Create a new scene, passing in the GameWindow the scene will be displayed in
*
* @param gameWindow the game window
*/
public LobbyScene(GameWindow gameWindow) {
super(gameWindow);
}
/**
* Build the layout of the scene
*/
@Override
public void build() {
logger.info("Building " + this.getClass().getName());
root = new GamePane(gameWindow.getWidth(), gameWindow.getHeight());
var menuPane = new StackPane();
menuPane.setMaxWidth(gameWindow.getWidth());
menuPane.setMaxHeight(gameWindow.getHeight());
menuPane.getStyleClass().add("menu-background");
root.getChildren().add(menuPane);
var mainPane = new BorderPane();
menuPane.getChildren().add(mainPane);
//Awful title
var title = new Text("TetrECS Lobby");
BorderPane.setAlignment(title, Pos.TOP_CENTER);
title.getStyleClass().add("title");
mainPane.setTop(title);
var leftMenu = new VBox();
leftMenu.setSpacing(8);
BorderPane.setMargin(leftMenu, new Insets(0,30,0,0));
createChannel.setOnMouseClicked(this::createChannel);
createChannel.getStyleClass().add("channelItem");
newChannelName.setOnAction(this::createChannel);
newChannelName.setPromptText("Channel name");
promptToCreateChannel.setOnMouseClicked((event) -> {
addChannelContainer.getChildren().clear();
addChannelContainer.getChildren().addAll(newChannelName, createChannel);
newChannelName.requestFocus();
});
promptToCreateChannel.getStyleClass().add("channelItem");
addChannelContainer.getChildren().add(promptToCreateChannel);
addChannelContainer.setSpacing(4);
allChannels.setSpacing(4);
var channelsTitle = new Text("Channels");
channelsTitle.getStyleClass().add("heading");
leftMenu.getChildren().addAll(channelsTitle, allChannels, addChannelContainer);
chat = new Chat(gameWindow.getCommunicator(), false);
mainContent.setSpacing(12);
mainPane.setLeft(leftMenu);
mainPane.setCenter(mainContent);
gameWindow.getCommunicator().addListener(this::handleCommunicatorMessage);
var listChannelsRequest = new Timer("Request list of channels from communicator");
listChannelsRequest.schedule(new TimerTask() {
@Override
public void run() {
gameWindow.getCommunicator().send("LIST");
}
}, 0, 10000);
channelUsers.setSpacing(8);
channelFunctionButtons.setSpacing(8);
}
/**
* Initialise this scene. Called after creation
*/
@Override
public void initialise() {
logger.info("Initialising Lobby");
// exit lobby when escape key pressed
scene.setOnKeyPressed((event) -> {
if (event.getCode() == KeyCode.ESCAPE) {
gameWindow.startMenu();
}
});
getScene().addPostLayoutPulseListener(this::scrollChatToBottom);
}
/**
* Scroll to bottom of messages list.
*/
private void scrollChatToBottom() {
chat.scrollToBottom();
}
/**
* Handle incoming message from communicator
*
* @param message incoming message
*/
private void handleCommunicatorMessage(String message) {
if (message.startsWith("CHANNELS ")) {
logger.info("List of channels received from communicator");
var data = message.replaceFirst("CHANNELS ", "").split("\n");
Platform.runLater(() -> {
allChannels.getChildren().clear();
for (var channel : data) {
if (!channel.isBlank()) {
var channelButton = new Text(channel);
channelButton.getStyleClass().add("channelItem");
channelButton.setOnMouseClicked((event) -> {
if (currentChannel != null) gameWindow.getCommunicator().send("PART");
gameWindow.getCommunicator().send("JOIN " + channel);
});
allChannels.getChildren().add(channelButton);
}
}
});
} else if (message.startsWith("USERS ")) {
logger.info("List of users in channel received from communicator");
var data = message.replaceFirst("USERS ", "").split("\n");
Platform.runLater(() -> {
channelUsers.getChildren().clear();
var usersTitle = new Text("Users:");
channelUsers.getChildren().add(usersTitle);
for (var user : data) {
var newUser = new Text(user);
newUser.setId(user);
channelUsers.getChildren().add(newUser);
}
});
} else if (message.equals("PARTED")) {
logger.info("Left channel: {}", currentChannel);
Platform.runLater(() -> {
gameWindow.getCommunicator().send("LIST");
mainContent.getChildren().clear();
chat.clearMessages();
channelFunctionButtons.getChildren().clear();
isChannelHost = false;
currentChannel = null;
});
} else if (message.equals("HOST")) {
logger.info("User is host of current channel");
isChannelHost = true;
Platform.runLater(() -> {
logger.info("Current channel host status: {}", isChannelHost);
var startGame = new Text("Start game");
startGame.getStyleClass().add("channelItem");
startGame.setOnMouseClicked((event) -> {
gameWindow.getCommunicator().send("START");
});
channelFunctionButtons.getChildren().add(0, startGame);
});
} else if (message.matches("^NICK .+:.+$")) {
Platform.runLater(() -> {
var usernames = message.replaceFirst("NICK ", "").split(":");
if (channelUsers.getChildren().removeIf(user -> user.getId().equals(usernames[0]))) {
var newUser = new Text(usernames[1]);
newUser.setId(usernames[1]);
channelUsers.getChildren().add(newUser);
}
});
} else if (message.startsWith("JOIN ")) {
Platform.runLater(() -> joinChannel(message.replaceFirst("JOIN ", "")));
} else if (message.startsWith("ERROR ")) {
var errorText = message.replaceFirst("ERROR ", "");
logger.info("Received an error: {}", errorText);
Platform.runLater(() -> {
var error = new Alert(Alert.AlertType.ERROR, "Can't perform that action:\n\n" + errorText);
error.showAndWait();
});
} else if (message.equals("START")) {
logger.info("Starting game");
Platform.runLater(gameWindow::startMultiplayerGame);
}
}
/**
* Create new channel on action event
*
* @param event triggering event (button click or text field submit)
*/
private void createChannel(Event event) {
if (currentChannel != null) gameWindow.getCommunicator().send("PART");
gameWindow.getCommunicator().send("CREATE " + newChannelName.getText());
gameWindow.getCommunicator().send("LIST");
newChannelName.clear();
addChannelContainer.getChildren().clear();
addChannelContainer.getChildren().add(promptToCreateChannel);
}
/**
* Join channel by name
*
* @param channelName name of channel
*/
private void joinChannel(String channelName) {
logger.info("Joined channel: {}", channelName);
var leaveChannel = new Text("Leave channel");
leaveChannel.getStyleClass().add("channelItem");
leaveChannel.setOnMouseClicked((event) -> {
gameWindow.getCommunicator().send("PART");
});
channelFunctionButtons.getChildren().add(leaveChannel);
var channelTitle = new Text(channelName);
channelTitle.getStyleClass().add("heading");
mainContent.getChildren().addAll(channelTitle, channelFunctionButtons, channelUsers, new Separator(), chat);
chat.focusInputField();
currentChannel = channelName;
}
}

View File

@@ -1,11 +1,20 @@
package uk.mgrove.ac.soton.comp1206.scene;
import javafx.animation.Animation;
import javafx.animation.RotateTransition;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.util.Duration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.App;
import uk.mgrove.ac.soton.comp1206.ui.GamePane;
import uk.mgrove.ac.soton.comp1206.ui.GameWindow;
import uk.mgrove.ac.soton.comp1206.util.Multimedia;
@@ -20,6 +29,11 @@ public class MenuScene extends BaseScene {
*/
private static final Logger logger = LogManager.getLogger(MenuScene.class);
/**
* Rotate transition for TetrECS logo
*/
private RotateTransition titleAnimation;
/**
* Create a new menu scene
* @param gameWindow the Game Window this will be displayed in
@@ -47,27 +61,39 @@ public class MenuScene extends BaseScene {
var mainPane = new BorderPane();
menuPane.getChildren().add(mainPane);
//Awful title
var title = new Text("TetrECS");
title.getStyleClass().add("title");
mainPane.setTop(title);
var title = new ImageView(getClass().getResource("/images/TetrECS.png").toExternalForm());
title.setPreserveRatio(true);
title.setFitWidth(400);
titleAnimation = new RotateTransition(Duration.millis(2000), title);
titleAnimation.setFromAngle(7);
titleAnimation.setToAngle(-7);
titleAnimation.setAutoReverse(true);
titleAnimation.setCycleCount(Animation.INDEFINITE);
var separator = new VBox();
separator.setPrefHeight(70);
var menuItems = new VBox();
var singlePlayerButton = new Button("Single Player");
var multiPlayerButton = new Button("Multi Player");
var howToPlayButton = new Button("How to Play");
var exitButton = new Button("Exit");
menuItems.getChildren().addAll(singlePlayerButton,multiPlayerButton,howToPlayButton,exitButton);
mainPane.setCenter(menuItems);
menuItems.setAlignment(Pos.CENTER);
var singlePlayerButton = new Text("Single Player");
var multiPlayerButton = new Text("Multiplayer");
var howToPlayButton = new Text("How to Play");
var exitButton = new Text("Exit");
// TOOD: window animations
menuItems.getChildren().addAll(title,separator,singlePlayerButton,multiPlayerButton,howToPlayButton,exitButton);
mainPane.setCenter(menuItems);
Multimedia.playMusic("music/menu.mp3");
//Bind the button action to the startGame method in the menu
singlePlayerButton.setOnAction(this::startSinglePlayer);
multiPlayerButton.setOnAction(this::startMultiPlayer);
howToPlayButton.setOnAction(this::showInstructions);
singlePlayerButton.setOnMouseClicked(this::startSinglePlayer);
singlePlayerButton.getStyleClass().add("menuItem");
multiPlayerButton.setOnMouseClicked(this::startMultiplayer);
multiPlayerButton.getStyleClass().add("menuItem");
howToPlayButton.setOnMouseClicked(this::showInstructions);
howToPlayButton.getStyleClass().add("menuItem");
exitButton.setOnMouseClicked(this::shutdown);
exitButton.getStyleClass().add("menuItem");
}
/**
@@ -75,14 +101,17 @@ public class MenuScene extends BaseScene {
*/
@Override
public void initialise() {
logger.info("Initialising " + this.getClass().getName());
titleAnimation.play();
}
/**
* Handle when the Start Single Player Game button is pressed
* @param event event
*/
private void startSinglePlayer(ActionEvent event) {
private void startSinglePlayer(MouseEvent event) {
titleAnimation.stop();
gameWindow.startChallenge();
}
@@ -90,7 +119,8 @@ public class MenuScene extends BaseScene {
* Handle when the Start Multiplayer Game button is pressed
* @param event event
*/
private void startMultiPlayer(ActionEvent event) {
private void startMultiplayer(MouseEvent event) {
titleAnimation.stop();
gameWindow.startMultiplayer();
}
@@ -98,8 +128,17 @@ public class MenuScene extends BaseScene {
* Handle when the How to Play button is pressed
* @param event event
*/
private void showInstructions(ActionEvent event) {
private void showInstructions(MouseEvent event) {
titleAnimation.stop();
gameWindow.startInstructions();
}
/**
* Handle when the exit button is pressed
* @param event event
*/
private void shutdown(MouseEvent event) {
App.getInstance().shutdown();
}
}

View File

@@ -0,0 +1,85 @@
package uk.mgrove.ac.soton.comp1206.scene;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Pos;
import javafx.scene.control.Separator;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.component.*;
import uk.mgrove.ac.soton.comp1206.game.Game;
import uk.mgrove.ac.soton.comp1206.game.MultiplayerGame;
import uk.mgrove.ac.soton.comp1206.ui.GamePane;
import uk.mgrove.ac.soton.comp1206.ui.GameWindow;
import uk.mgrove.ac.soton.comp1206.util.Multimedia;
/**
* Class for multiplayer game scene
*/
public class MultiplayerScene extends ChallengeScene {
/**
* Logger
*/
private static final Logger logger = LogManager.getLogger(MultiplayerScene.class);
/**
* Chat container
*/
private Chat chat;
/**
* Live leaderboard
*/
private Leaderboard leaderboard;
/**
* Create a new multiplayer challenge scene
*
* @param gameWindow the Game Window
*/
public MultiplayerScene(GameWindow gameWindow) {
super(gameWindow);
}
/**
* Set up the game object and model
*/
@Override
public void setupGame() {
logger.info("Starting a new challenge");
//Start new game
game = new MultiplayerGame(5, 5, gameWindow.getCommunicator());
game.setOnGameFail(() -> Platform.runLater(() -> gameWindow.startMenu()));
gameWindow.getCommunicator().send("SCORES");
}
/**
* Build the Multiplayer Challenge window
*/
@Override
public void build() {
super.build();
var leftMenu = new VBox();
leftMenu.setSpacing(8);
leftMenu.setMaxWidth(200);
leftMenu.setPrefWidth(200);
chat = new Chat(gameWindow.getCommunicator(), true);
chat.setChatFocusTraversable(false);
leaderboard = new Leaderboard("Leaderboard");
var chatTitle = new Text("Chat");
chatTitle.getStyleClass().add("heading");
leftMenu.getChildren().addAll(leaderboard, new Separator(), chatTitle, chat);
mainPane.setLeft(leftMenu);
leaderboard.bindLeaderboardScores(game.leaderboardScoresProperty());
mainPane.requestFocus();
}
}

View File

@@ -1,19 +1,29 @@
package uk.mgrove.ac.soton.comp1206.scene;
import javafx.animation.FadeTransition;
import javafx.application.Platform;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.util.Duration;
import javafx.util.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.component.ScoresList;
import uk.mgrove.ac.soton.comp1206.game.Game;
import uk.mgrove.ac.soton.comp1206.game.MultiplayerGame;
import uk.mgrove.ac.soton.comp1206.ui.GamePane;
import uk.mgrove.ac.soton.comp1206.ui.GameWindow;
@@ -23,6 +33,7 @@ import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Scene to show scores and record high scores
@@ -42,7 +53,12 @@ public class ScoresScene extends BaseScene {
/**
* List of scores stored locally
*/
private SimpleListProperty<Pair<String,Integer>> localScores;
private final SimpleListProperty<Pair<String,Integer>> localScores = new SimpleListProperty<>(FXCollections.observableArrayList(new ArrayList<>()));
/**
* List of online scores
*/
private final SimpleListProperty<Pair<String,Integer>> remoteScores = new SimpleListProperty<>(FXCollections.observableArrayList(new ArrayList<>()));
/**
* Root pane in scene inside the GamePane
@@ -54,10 +70,20 @@ public class ScoresScene extends BaseScene {
*/
private ScoresList scoresUiList;
/**
* Node to display online high scores
*/
private ScoresList remoteScoresUiList;
/**
* Container for prompting for username when high score achieved
*/
private VBox userNamePrompt;
private VBox highScorePromptContainer;
/**
* Main container for pane
*/
private VBox mainContainer;
/**
* Create a new scores scene
@@ -88,15 +114,37 @@ public class ScoresScene extends BaseScene {
var mainPane = new BorderPane();
scoresPane.getChildren().add(mainPane);
ArrayList<Pair<String,Integer>> scoresList = new ArrayList<>();
ObservableList<Pair<String,Integer>> observableScoresList = FXCollections.observableArrayList(scoresList);
localScores = new SimpleListProperty<>(observableScoresList);
loadScores("scores.txt");
if (game.getScore() > localScores.get(localScores.getSize() - 1).getValue()) showHighScorePrompt(game.getScore());
String localScoresTitle;
if (game.getClass().equals(MultiplayerGame.class)) {
logger.info("Loading multiplayer scores from game");
var multiplayerScores = FXCollections.observableArrayList(new ArrayList<Pair<String,Integer>>());
for (var score : game.leaderboardScoresProperty().get()) {
multiplayerScores.add(new Pair<>(score.getKey(),score.getValue().getKey()));
}
localScores.set(multiplayerScores);
localScoresTitle = "Game Scores";
} else {
loadScores("scores.txt");
localScoresTitle = "Local Scores";
}
loadOnlineScores();
scoresUiList = new ScoresList();
scoresUiList = new ScoresList(localScoresTitle);
scoresUiList.bindScores(localScores);
mainPane.setCenter(scoresUiList);
scoresUiList.getStyleClass().add("scorelist");
remoteScoresUiList = new ScoresList("Online Scores");
remoteScoresUiList.bindScores(remoteScores);
remoteScoresUiList.getStyleClass().add("scorelist");
var scoresContainer = new HBox();
scoresContainer.setSpacing(40);
scoresContainer.getChildren().addAll(scoresUiList, remoteScoresUiList);
scoresContainer.setAlignment(Pos.CENTER);
mainContainer = new VBox(scoresContainer);
mainContainer.setSpacing(60);
mainContainer.setAlignment(Pos.CENTER);
mainPane.setCenter(mainContainer);
}
/**
@@ -104,6 +152,26 @@ public class ScoresScene extends BaseScene {
*/
@Override
public void initialise() {
logger.info("Initialising Scores");
// exit instructions when escape key pressed
scene.setOnKeyPressed((event) -> {
if (event.getCode() == KeyCode.ESCAPE) {
gameWindow.startMenu();
}
});
}
/**
* Return to lobby on mouse click
* @param event mouse click event
*/
private void returnToLobby(MouseEvent event) {
if (game.getClass().equals(MultiplayerGame.class)) {
gameWindow.startMultiplayer();
} else {
gameWindow.startMenu();
}
}
/**
@@ -112,7 +180,34 @@ public class ScoresScene extends BaseScene {
*/
public void loadScores(String filePath) {
logger.info("Loading scores from file: {}", filePath);
localScores = getScores(filePath);
localScores.set(getScores(filePath));
}
/**
* Load high scores from server and reveal both local and remote scores
*/
public void loadOnlineScores() {
logger.info("Loading online scores");
AtomicBoolean scoresLoaded = new AtomicBoolean(false);
gameWindow.getCommunicator().addListener((message) -> {
if (!scoresLoaded.get() && message.startsWith("HISCORES ")) {
scoresLoaded.set(true);
var strippedMessage = message.replaceFirst("HISCORES ", "");
remoteScores.set(parseScores(new Scanner(strippedMessage)));
var topLocalScore = localScores.getSize() > 0 ? localScores.get(localScores.getSize() - 1).getValue() : 0;
var topRemoteScore = remoteScores.size() > 0 ? remoteScores.get(remoteScores.getSize() - 1).getValue() : 0;
Platform.runLater(() -> {
if (game.getScore() > topLocalScore || game.getScore() > topRemoteScore)
showHighScorePrompt(game.getScore());
else {
scoresUiList.reveal();
remoteScoresUiList.reveal();
showPlayAgainButton();
}
});
}
});
gameWindow.getCommunicator().send("HISCORES");
}
/**
@@ -123,22 +218,15 @@ public class ScoresScene extends BaseScene {
public static SimpleListProperty<Pair<String,Integer>> getScores(String filePath) {
logger.info("Retrieving scores from file: {}", filePath);
var scores = new SimpleListProperty<Pair<String,Integer>>(FXCollections.observableArrayList());
SimpleListProperty<Pair<String, Integer>> scores;
try {
File scoresFile = new File(filePath);
Scanner scanner = new Scanner(scoresFile);
while (scanner.hasNextLine()) {
var line = scanner.nextLine();
if (line.matches("^.+:[0-9]+$")) {
int splitIndex = line.lastIndexOf(":");
String[] info = {line.substring(0,splitIndex), line.substring(splitIndex+1)};
scores.add(new Pair<>(info[0], Integer.valueOf(info[1])));
}
}
scanner.close();
scores = parseScores(scanner);
} catch (FileNotFoundException e1) {
logger.error("Unable to load scores file, writing default scores instead: {}", filePath);
scores = new SimpleListProperty<>(FXCollections.observableArrayList());
scores.add(new Pair<>("Sam",3000));
scores.add(new Pair<>("Jane",2000));
scores.add(new Pair<>("Pete",1000));
@@ -148,6 +236,28 @@ public class ScoresScene extends BaseScene {
return scores;
}
/**
* Parse high scores from a scanner to list property
* @param scanner scanner to parse from
* @return list property containing scores loaded
*/
private static SimpleListProperty<Pair<String,Integer>> parseScores(Scanner scanner) {
var scores = new SimpleListProperty<Pair<String,Integer>>(FXCollections.observableArrayList());
while (scanner.hasNextLine()) {
var line = scanner.nextLine();
if (line.matches("^.+:[0-9]+$")) {
int splitIndex = line.lastIndexOf(":");
String[] info = {line.substring(0,splitIndex), line.substring(splitIndex+1)};
scores.add(new Pair<>(info[0], Integer.valueOf(info[1])));
}
}
scanner.close();
logger.info("Parsed scores: {}", scores);
return scores;
}
/**
* Write list of scores to file
* @param filePath file to write to
@@ -175,14 +285,54 @@ public class ScoresScene extends BaseScene {
private void showHighScorePrompt(int score) {
logger.info("Showing high score username input prompt");
userNamePrompt = new VBox();
var usernameTitle = new Text("Enter username");
usernameTitle.getStyleClass().add("title");
usernameTitle.setTextAlignment(TextAlignment.CENTER);
var userNamePrompt = new HBox();
userNamePrompt.setAlignment(Pos.CENTER);
userNamePrompt.setSpacing(4);
var userNameInput = new TextField();
userNameInput.setPromptText("Enter username...");
userNameInput.setOnAction((event) -> saveHighScore(userNameInput.getText(), score));
var userNameSubmit = new Button("Submit");
userNameSubmit.setOnAction((event) -> saveHighScore(userNameInput.getText(), score));
userNamePrompt.getChildren().addAll(new Text("Username:"), userNameInput, userNameSubmit);
scoresPane.getChildren().add(userNamePrompt);
var userNameSubmit = new Text("Submit");
userNameSubmit.getStyleClass().add("channelItem");
userNameInput.setOnAction((event) -> submitHighScore(score, userNameInput.getText()));
userNameSubmit.setOnMouseClicked((event) -> submitHighScore(score, userNameInput.getText()));
userNamePrompt.getChildren().addAll(userNameInput, userNameSubmit);
highScorePromptContainer = new VBox(usernameTitle, userNamePrompt);
highScorePromptContainer.setSpacing(20);
scoresPane.getChildren().addAll(highScorePromptContainer);
StackPane.setAlignment(highScorePromptContainer, Pos.CENTER);
}
/**
* Submit a high score
* @param score score to submit
* @param username username of player
*/
private void submitHighScore(int score, String username) {
if (!username.isBlank()) {
saveHighScore(username, score);
writeOnlineScore(username, score);
scoresPane.getChildren().remove(highScorePromptContainer);
scoresUiList.reveal();
remoteScoresUiList.reveal();
showPlayAgainButton();
}
}
private void showPlayAgainButton() {
var playAgainButton = new Text("Continue");
playAgainButton.getStyleClass().add("menuItem");
playAgainButton.setOnMouseClicked(this::returnToLobby);
playAgainButton.setOpacity(0);
var ft = new FadeTransition(Duration.millis(1000), playAgainButton);
ft.setToValue(1);
mainContainer.getChildren().add(playAgainButton);
ft.play();
}
/**
@@ -193,8 +343,8 @@ public class ScoresScene extends BaseScene {
private void saveHighScore(String username, int score) {
logger.info("Saving high score: {} for user: {}", score, username);
for (var i = 0; i < localScores.getSize(); i++) {
if (localScores.get(i).getValue() < score) {
for (var i = 0; i <= localScores.getSize(); i++) {
if (i == localScores.getSize() || localScores.get(i).getValue() < score) {
localScores.add(i, new Pair<>(username, score));
break;
}
@@ -202,8 +352,19 @@ public class ScoresScene extends BaseScene {
// if score isn't higher than last high score then this shouldn't have been triggered
// so while it won't get stored, this isn't an issue
writeScores("scores.txt", localScores);
scoresPane.getChildren().remove(userNamePrompt);
scoresUiList.reveal();
}
private void writeOnlineScore(String username, int score) {
logger.info("Saving online high score: {} for user: {}", score, username);
gameWindow.getCommunicator().send("HISCORE " + username + ":" + (score));
for (var i = 0; i <= remoteScores.getSize(); i++) {
if (i == remoteScores.getSize() || remoteScores.get(i).getValue() < score) {
remoteScores.add(i, new Pair<>(username, score));
break;
}
}
}
/**

View File

@@ -1,11 +1,13 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.animation.FadeTransition;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.App;
@@ -61,8 +63,7 @@ public class GameWindow {
//Setup communicator
communicator = new Communicator("ws://ofb-labs.soton.ac.uk:9700");
//Go to menu
startMenu();
startLoadingScreen();
}
/**
@@ -77,6 +78,13 @@ public class GameWindow {
Font.loadFont(getClass().getResourceAsStream("/style/Orbitron-ExtraBold.ttf"),32);
}
/**
* Display the loading screen
*/
public void startLoadingScreen() {
loadScene(new LoadingScene(this));
}
/**
* Display the main menu
*/
@@ -92,10 +100,17 @@ public class GameWindow {
}
/**
* Display the multiplayer challenge
* Display the multiplayer lobby
*/
public void startMultiplayer() {
// TODO: load multiplayer scene
loadScene(new LobbyScene(this));
}
/**
* Display the multiplayer challenge
*/
public void startMultiplayerGame() {
loadScene(new MultiplayerScene(this));
}
/**
@@ -129,11 +144,36 @@ public class GameWindow {
//Create the new scene and set it up
newScene.build();
currentScene = newScene;
scene = newScene.setScene();
stage.setScene(scene);
//Initialise the scene when ready
Platform.runLater(() -> currentScene.initialise());
if (scene != null) {
scene = newScene.setScene();
var fadeOut = new FadeTransition(Duration.millis(200), stage.getScene().getRoot());
fadeOut.setFromValue(1);
fadeOut.setToValue(0);
var fadeIn = new FadeTransition(Duration.millis(200), scene.getRoot());
fadeIn.setFromValue(0);
fadeIn.setToValue(1);
fadeOut.setOnFinished((event) -> {
stage.setScene(scene);
fadeIn.play();
//Initialise the scene when ready
Platform.runLater(() -> currentScene.initialise());
});
fadeOut.play();
} else {
scene = newScene.setScene();
stage.setScene(scene);
//Initialise the scene when ready
Platform.runLater(() -> currentScene.initialise());
}
}
/**

View File

@@ -34,7 +34,7 @@ public class Multimedia {
if (audioPlayer != null) audioPlayer.stop();
var media = new Media(Multimedia.class.getResource("/" + filePath).toExternalForm());
audioPlayer = new MediaPlayer(media);
audioPlayer.play();
// audioPlayer.play();
}
/**
@@ -47,9 +47,8 @@ public class Multimedia {
if (musicPlayer != null) musicPlayer.stop();
var media = new Media(Multimedia.class.getResource("/" + filePath).toExternalForm());
musicPlayer = new MediaPlayer(media);
musicPlayer.setAutoPlay(true);
// musicPlayer.setAutoPlay(true);
musicPlayer.setCycleCount(MediaPlayer.INDEFINITE);
musicPlayer.play();
}
}

View File

@@ -6,9 +6,18 @@
-fx-background-color: black;
}
Text {
-fx-fill: white;
}
Label {
-fx-text-fill: white;
}
.menu-background {
-fx-background-image: url("../images/1.jpg");
-fx-background-size: cover;
-fx-padding: 20;
}
.challenge-background {
@@ -148,6 +157,9 @@
-fx-border-color: black;
-fx-stroke: black;
}
.channelItem:hover {
-fx-fill: yellow;
}
.channelItem.selected {
-fx-fill: yellow;
}
@@ -181,7 +193,7 @@
-fx-fill: white;
}
TextField {
TextField, .text-field {
-fx-border-color: white;
-fx-border-width: 1px;
-fx-background-color: rgba(0,0,0,0.5);