[FEAT] Complete basic functionality including multiplayer and styling
This commit is contained in:
@@ -13,4 +13,5 @@ module uk.mgrove.ac.soton.comp1206 {
|
|||||||
exports uk.mgrove.ac.soton.comp1206.event;
|
exports uk.mgrove.ac.soton.comp1206.event;
|
||||||
exports uk.mgrove.ac.soton.comp1206.component;
|
exports uk.mgrove.ac.soton.comp1206.component;
|
||||||
exports uk.mgrove.ac.soton.comp1206.game;
|
exports uk.mgrove.ac.soton.comp1206.game;
|
||||||
|
opens uk.mgrove.ac.soton.comp1206.component to javafx.fxml;
|
||||||
}
|
}
|
||||||
171
src/main/java/uk/mgrove/ac/soton/comp1206/component/Chat.java
Normal file
171
src/main/java/uk/mgrove/ac/soton/comp1206/component/Chat.java
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package uk.mgrove.ac.soton.comp1206.component;
|
package uk.mgrove.ac.soton.comp1206.component;
|
||||||
|
|
||||||
import javafx.animation.FadeTransition;
|
import javafx.animation.FadeTransition;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.SimpleListProperty;
|
import javafx.beans.property.SimpleListProperty;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
@@ -26,28 +27,42 @@ public class ScoresList extends VBox {
|
|||||||
/**
|
/**
|
||||||
* Scores
|
* 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
|
* Initialise the scores list
|
||||||
*/
|
*/
|
||||||
public ScoresList() {
|
public ScoresList(String titleText) {
|
||||||
logger.info("Building scores list");
|
logger.info("Building scores list");
|
||||||
|
|
||||||
ObservableList<Pair<String,Integer>> observableScoresList = FXCollections.observableArrayList();
|
|
||||||
scores = new SimpleListProperty<>(observableScoresList);
|
|
||||||
|
|
||||||
setOpacity(0);
|
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);
|
logger.info("Detected change in scores list: {}", change);
|
||||||
getChildren().clear();
|
getChildren().clear();
|
||||||
|
getChildren().add(title);
|
||||||
for (var pair : scores.get()) {
|
for (var pair : scores.get()) {
|
||||||
var score = new HBox();
|
var individualScoreContainer = new HBox();
|
||||||
score.getChildren().addAll(new Text(pair.getKey()), new Text(pair.getValue().toString()));
|
individualScoreContainer.getChildren().addAll(new Text(pair.getKey()), new Text(pair.getValue().toString()));
|
||||||
getChildren().add(score);
|
getChildren().add(individualScoreContainer);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty constructor
|
||||||
|
*/
|
||||||
|
public ScoresList() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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.beans.property.IntegerProperty;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
@@ -28,19 +28,24 @@ public class StatsMenu extends VBox {
|
|||||||
|
|
||||||
Text score1 = new Text();
|
Text score1 = new Text();
|
||||||
score1.textProperty().bind(score.asString());
|
score1.textProperty().bind(score.asString());
|
||||||
|
score1.getStyleClass().add("score");
|
||||||
Text level1 = new Text();
|
Text level1 = new Text();
|
||||||
level1.textProperty().bind(level.asString());
|
level1.textProperty().bind(level.asString());
|
||||||
|
level1.getStyleClass().add("level");
|
||||||
Text lives1 = new Text();
|
Text lives1 = new Text();
|
||||||
lives1.textProperty().bind(lives.asString());
|
lives1.textProperty().bind(lives.asString());
|
||||||
|
lives1.getStyleClass().add("lives");
|
||||||
Text multiplier1 = new Text();
|
Text multiplier1 = new Text();
|
||||||
multiplier1.textProperty().bind(multiplier.asString());
|
multiplier1.textProperty().bind(multiplier.asString());
|
||||||
|
multiplier1.getStyleClass().add("level");
|
||||||
|
|
||||||
var scoreHeader = new Text("Score");
|
var scoreHeader = new Text("Score");
|
||||||
var levelHeader = new Text("Level");
|
var levelHeader = new Text("Level");
|
||||||
var livesHeader = new Text("Lives");
|
var livesHeader = new Text("Lives");
|
||||||
var multiplierHeader = new Text("Multiplier");
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,14 +3,13 @@ package uk.mgrove.ac.soton.comp1206.game;
|
|||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.IntegerProperty;
|
import javafx.beans.property.IntegerProperty;
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
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.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import uk.mgrove.ac.soton.comp1206.component.GameBlock;
|
import uk.mgrove.ac.soton.comp1206.component.GameBlock;
|
||||||
import uk.mgrove.ac.soton.comp1206.component.GameBlockCoordinate;
|
import uk.mgrove.ac.soton.comp1206.component.GameBlockCoordinate;
|
||||||
import uk.mgrove.ac.soton.comp1206.event.GameLoopListener;
|
import uk.mgrove.ac.soton.comp1206.event.*;
|
||||||
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.util.Multimedia;
|
import uk.mgrove.ac.soton.comp1206.util.Multimedia;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -49,12 +48,12 @@ public class Game {
|
|||||||
/**
|
/**
|
||||||
* Current game piece player is using
|
* Current game piece player is using
|
||||||
*/
|
*/
|
||||||
private GamePiece currentPiece;
|
protected GamePiece currentPiece;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Next game piece for player to use
|
* Next game piece for player to use
|
||||||
*/
|
*/
|
||||||
private GamePiece followingPiece;
|
protected GamePiece followingPiece;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Player's current score
|
* Player's current score
|
||||||
@@ -76,7 +75,7 @@ public class Game {
|
|||||||
/**
|
/**
|
||||||
* Listener for when new piece is received by the 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
|
* Listener for when lines of blocks are cleared
|
||||||
@@ -128,7 +127,7 @@ public class Game {
|
|||||||
currentPiece = followingPiece;
|
currentPiece = followingPiece;
|
||||||
followingPiece = spawnPiece();
|
followingPiece = spawnPiece();
|
||||||
if (nextPieceListener != null) nextPieceListener.nextPiece(currentPiece, followingPiece);
|
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;
|
return currentPiece;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +184,7 @@ public class Game {
|
|||||||
/**
|
/**
|
||||||
* Start new game loop
|
* Start new game loop
|
||||||
*/
|
*/
|
||||||
private void scheduleGameLoop() {
|
protected void scheduleGameLoop() {
|
||||||
logger.info("Scheduling game loop");
|
logger.info("Scheduling game loop");
|
||||||
|
|
||||||
if (gameTimer != null) gameTimer.cancel();
|
if (gameTimer != null) gameTimer.cancel();
|
||||||
@@ -209,7 +208,6 @@ public class Game {
|
|||||||
public void endGame() {
|
public void endGame() {
|
||||||
if (gameTimer != null) gameTimer.cancel();
|
if (gameTimer != null) gameTimer.cancel();
|
||||||
Multimedia.playAudio("sounds/explode.wav");
|
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 lines number of lines cleared
|
||||||
* @param blocks number of blocks cleared
|
* @param blocks number of blocks cleared
|
||||||
*/
|
*/
|
||||||
private void score(int lines, int blocks) {
|
protected void score(int lines, int blocks) {
|
||||||
logger.info("Updating score");
|
logger.info("Updating score with lines cleared: {}, blocks cleared: {}", lines, blocks);
|
||||||
|
|
||||||
var addScore = lines * blocks * 10 * multiplier.get();
|
var addScore = lines * blocks * 10 * multiplier.get();
|
||||||
score.set(score.get() + addScore);
|
score.set(score.get() + addScore);
|
||||||
@@ -506,4 +504,17 @@ public class Game {
|
|||||||
showScoresListener = listener;
|
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) {}
|
||||||
}
|
}
|
||||||
@@ -147,6 +147,9 @@ public class Grid {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the grid
|
||||||
|
*/
|
||||||
public void clearGrid() {
|
public void clearGrid() {
|
||||||
for (var x = 0; x < getCols(); x++) {
|
for (var x = 0; x < getCols(); x++) {
|
||||||
for (var y = 0; y < getRows(); y++) {
|
for (var y = 0; y < getRows(); y++) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import javafx.beans.property.IntegerProperty;
|
|||||||
import javafx.beans.property.SimpleIntegerProperty;
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
import javafx.scene.input.KeyEvent;
|
import javafx.scene.input.KeyEvent;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.paint.Color;
|
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.game.GamePiece;
|
||||||
import uk.mgrove.ac.soton.comp1206.ui.GamePane;
|
import uk.mgrove.ac.soton.comp1206.ui.GamePane;
|
||||||
import uk.mgrove.ac.soton.comp1206.ui.GameWindow;
|
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;
|
import uk.mgrove.ac.soton.comp1206.util.Multimedia;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,22 +42,22 @@ public class ChallengeScene extends BaseScene {
|
|||||||
/**
|
/**
|
||||||
* PieceBoard to display current piece
|
* PieceBoard to display current piece
|
||||||
*/
|
*/
|
||||||
private PieceBoard currentPieceBoard;
|
protected PieceBoard currentPieceBoard;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PieceBoard to display next piece
|
* PieceBoard to display next piece
|
||||||
*/
|
*/
|
||||||
private PieceBoard followingPieceBoard;
|
protected PieceBoard followingPieceBoard;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GameBoard for main game
|
* 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)
|
* 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
|
* Fill transition for timer countdown bar
|
||||||
@@ -68,10 +69,15 @@ public class ChallengeScene extends BaseScene {
|
|||||||
*/
|
*/
|
||||||
private ScaleTransition timerCountdownBarScaleTransition;
|
private ScaleTransition timerCountdownBarScaleTransition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main pane for UI
|
||||||
|
*/
|
||||||
|
protected BorderPane mainPane;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Top high score property
|
* 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
|
* Create a new Single Player challenge scene
|
||||||
@@ -99,10 +105,11 @@ public class ChallengeScene extends BaseScene {
|
|||||||
challengePane.getStyleClass().add("menu-background");
|
challengePane.getStyleClass().add("menu-background");
|
||||||
root.getChildren().add(challengePane);
|
root.getChildren().add(challengePane);
|
||||||
|
|
||||||
var mainPane = new BorderPane();
|
mainPane = new BorderPane();
|
||||||
challengePane.getChildren().add(mainPane);
|
challengePane.getChildren().add(mainPane);
|
||||||
|
|
||||||
board = new GameBoard(game.getGrid(),gameWindow.getWidth()/2f,gameWindow.getWidth()/2f);
|
board = new GameBoard(game.getGrid(),gameWindow.getWidth()/2f,gameWindow.getWidth()/2f);
|
||||||
|
board.setFocusTraversable(true);
|
||||||
board.setOnRightClicked(game::rotateCurrentPiece);
|
board.setOnRightClicked(game::rotateCurrentPiece);
|
||||||
mainPane.setCenter(board);
|
mainPane.setCenter(board);
|
||||||
|
|
||||||
@@ -115,20 +122,22 @@ public class ChallengeScene extends BaseScene {
|
|||||||
|
|
||||||
var topHighScoreText = new Text();
|
var topHighScoreText = new Text();
|
||||||
topHighScoreText.textProperty().bind(highScore.asString());
|
topHighScoreText.textProperty().bind(highScore.asString());
|
||||||
|
topHighScoreText.getStyleClass().add("hiscore");
|
||||||
highScore.set(getHighScore());
|
highScore.set(getHighScore());
|
||||||
|
var topHighScoreContainer = new VBox(new Text("Top high score"), topHighScoreText);
|
||||||
|
|
||||||
game.scoreProperty().addListener((ChangeListener<? super Number>) (change, oldValue, newValue) -> {
|
game.scoreProperty().addListener((ChangeListener<? super Number>) (change, oldValue, newValue) -> {
|
||||||
if (newValue.intValue() > highScore.get()) highScore.set(newValue.intValue());
|
if (newValue.intValue() > highScore.get()) highScore.set(newValue.intValue());
|
||||||
});
|
});
|
||||||
|
|
||||||
var rightMenu = new VBox();
|
var rightMenu = new VBox();
|
||||||
rightMenu.getChildren().addAll(statsMenu,currentPieceBoard,followingPieceBoard,topHighScoreText);
|
rightMenu.setSpacing(16);
|
||||||
|
rightMenu.getChildren().addAll(statsMenu,currentPieceBoard,followingPieceBoard,topHighScoreContainer);
|
||||||
|
|
||||||
mainPane.setRight(rightMenu);
|
mainPane.setRight(rightMenu);
|
||||||
|
|
||||||
timerCountdownBar = new Rectangle(gameWindow.getWidth()/2f,48);
|
timerCountdownBar = new Rectangle(gameWindow.getWidth()/2f,48);
|
||||||
BorderPane.setAlignment(timerCountdownBar, Pos.BOTTOM_CENTER);
|
BorderPane.setAlignment(timerCountdownBar, Pos.BOTTOM_CENTER);
|
||||||
// TODO: alignment of timer bar
|
|
||||||
|
|
||||||
mainPane.setBottom(timerCountdownBar);
|
mainPane.setBottom(timerCountdownBar);
|
||||||
|
|
||||||
@@ -142,7 +151,7 @@ public class ChallengeScene extends BaseScene {
|
|||||||
* Handle when a block is clicked
|
* Handle when a block is clicked
|
||||||
* @param gameBlock the Game Block that was clocked
|
* @param gameBlock the Game Block that was clocked
|
||||||
*/
|
*/
|
||||||
private void blockClicked(GameBlock gameBlock) {
|
protected void blockClicked(GameBlock gameBlock) {
|
||||||
game.blockClicked(gameBlock);
|
game.blockClicked(gameBlock);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +159,8 @@ public class ChallengeScene extends BaseScene {
|
|||||||
* Handle keypress for game keyboard controls
|
* Handle keypress for game keyboard controls
|
||||||
* @param event the keypress
|
* @param event the keypress
|
||||||
*/
|
*/
|
||||||
private void handleKeyboardControls(KeyEvent event) {
|
protected void handleKeyboardControls(KeyEvent event) {
|
||||||
|
logger.info("Handling keyboard input: {}", event.getCode());
|
||||||
switch (event.getCode()) {
|
switch (event.getCode()) {
|
||||||
case ESCAPE -> returnToMenu();
|
case ESCAPE -> returnToMenu();
|
||||||
case ENTER, X -> game.dropPiece(board.getXFocus(),board.getYFocus());
|
case ENTER, X -> game.dropPiece(board.getXFocus(),board.getYFocus());
|
||||||
@@ -200,13 +210,13 @@ public class ChallengeScene extends BaseScene {
|
|||||||
gameWindow.startMenu();
|
gameWindow.startMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showScores(Game game) {
|
protected void showScores(Game game) {
|
||||||
if (timerCountdownBarFillTransition != null) timerCountdownBarFillTransition.stop();
|
if (timerCountdownBarFillTransition != null) timerCountdownBarFillTransition.stop();
|
||||||
if (timerCountdownBarScaleTransition != null) timerCountdownBarScaleTransition.stop();
|
if (timerCountdownBarScaleTransition != null) timerCountdownBarScaleTransition.stop();
|
||||||
gameWindow.startScores(game);
|
gameWindow.startScores(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setCurrentPiece(GamePiece currentPiece, GamePiece followingPiece) {
|
protected void setCurrentPiece(GamePiece currentPiece, GamePiece followingPiece) {
|
||||||
currentPieceBoard.displayPiece(currentPiece);
|
currentPieceBoard.displayPiece(currentPiece);
|
||||||
followingPieceBoard.displayPiece(followingPiece);
|
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
|
* Reset the countdown progress bar with new game timer delay when the game loop is scheduled
|
||||||
* @param timerDelay delay until game loop is executed
|
* @param timerDelay delay until game loop is executed
|
||||||
*/
|
*/
|
||||||
private void scheduleCountdown(int timerDelay) {
|
protected void scheduleCountdown(int timerDelay) {
|
||||||
logger.info("Scheduling UI countdown timer");
|
logger.info("Scheduling UI countdown timer");
|
||||||
|
|
||||||
timerCountdownBar.setArcHeight(32);
|
timerCountdownBar.setArcHeight(32);
|
||||||
@@ -233,7 +243,7 @@ public class ChallengeScene extends BaseScene {
|
|||||||
* Get top high score
|
* Get top high score
|
||||||
* @return top high score
|
* @return top high score
|
||||||
*/
|
*/
|
||||||
private int getHighScore() {
|
protected int getHighScore() {
|
||||||
return ScoresScene.getScores("scores.txt").get(0).getValue();
|
return ScoresScene.getScores("scores.txt").get(0).getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package uk.mgrove.ac.soton.comp1206.scene;
|
package uk.mgrove.ac.soton.comp1206.scene;
|
||||||
|
|
||||||
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.input.KeyCode;
|
||||||
@@ -8,6 +9,7 @@ import javafx.scene.layout.GridPane;
|
|||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
|
import javafx.scene.text.TextAlignment;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import uk.mgrove.ac.soton.comp1206.component.PieceBoard;
|
import uk.mgrove.ac.soton.comp1206.component.PieceBoard;
|
||||||
@@ -54,20 +56,18 @@ public class InstructionsScene extends BaseScene {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void build() {
|
public void build() {
|
||||||
// TODO: styling and insets
|
|
||||||
|
|
||||||
logger.info("Building " + this.getClass().getName());
|
logger.info("Building " + this.getClass().getName());
|
||||||
|
|
||||||
root = new GamePane(gameWindow.getWidth(),gameWindow.getHeight());
|
root = new GamePane(gameWindow.getWidth(),gameWindow.getHeight());
|
||||||
|
|
||||||
var menuPane = new StackPane();
|
var instructionsPane = new StackPane();
|
||||||
menuPane.setMaxWidth(gameWindow.getWidth());
|
instructionsPane.setMaxWidth(gameWindow.getWidth());
|
||||||
menuPane.setMaxHeight(gameWindow.getHeight());
|
instructionsPane.setMaxHeight(gameWindow.getHeight());
|
||||||
menuPane.getStyleClass().add("menu-background");
|
instructionsPane.getStyleClass().add("menu-background");
|
||||||
root.getChildren().add(menuPane);
|
root.getChildren().add(instructionsPane);
|
||||||
|
|
||||||
var mainPane = new BorderPane();
|
var mainPane = new BorderPane();
|
||||||
menuPane.getChildren().add(mainPane);
|
instructionsPane.getChildren().add(mainPane);
|
||||||
|
|
||||||
//Awful title
|
//Awful title
|
||||||
var title = new Text("How to Play");
|
var title = new Text("How to Play");
|
||||||
@@ -75,10 +75,18 @@ public class InstructionsScene extends BaseScene {
|
|||||||
mainPane.setTop(title);
|
mainPane.setTop(title);
|
||||||
BorderPane.setAlignment(title, Pos.TOP_CENTER);
|
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());
|
var instructions = new ImageView(getClass().getResource("/images/Instructions.png").toExternalForm());
|
||||||
instructions.setPreserveRatio(true);
|
instructions.setPreserveRatio(true);
|
||||||
instructions.setFitHeight(gameWindow.getHeight()*0.55);
|
instructions.setFitHeight(gameWindow.getHeight()*0.5);
|
||||||
mainPane.setCenter(instructions);
|
|
||||||
|
var instructionsContainer = new VBox(instructionsText, instructions);
|
||||||
|
instructionsContainer.setAlignment(Pos.TOP_CENTER);
|
||||||
|
mainPane.setCenter(instructionsContainer);
|
||||||
|
|
||||||
var pieceGrid = new GridPane();
|
var pieceGrid = new GridPane();
|
||||||
pieceGrid.setVgap(8);
|
pieceGrid.setVgap(8);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
304
src/main/java/uk/mgrove/ac/soton/comp1206/scene/LobbyScene.java
Normal file
304
src/main/java/uk/mgrove/ac/soton/comp1206/scene/LobbyScene.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
package uk.mgrove.ac.soton.comp1206.scene;
|
package uk.mgrove.ac.soton.comp1206.scene;
|
||||||
|
|
||||||
|
import javafx.animation.Animation;
|
||||||
|
import javafx.animation.RotateTransition;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.text.Text;
|
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.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
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.GamePane;
|
||||||
import uk.mgrove.ac.soton.comp1206.ui.GameWindow;
|
import uk.mgrove.ac.soton.comp1206.ui.GameWindow;
|
||||||
import uk.mgrove.ac.soton.comp1206.util.Multimedia;
|
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);
|
private static final Logger logger = LogManager.getLogger(MenuScene.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate transition for TetrECS logo
|
||||||
|
*/
|
||||||
|
private RotateTransition titleAnimation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new menu scene
|
* Create a new menu scene
|
||||||
* @param gameWindow the Game Window this will be displayed in
|
* @param gameWindow the Game Window this will be displayed in
|
||||||
@@ -47,27 +61,39 @@ public class MenuScene extends BaseScene {
|
|||||||
var mainPane = new BorderPane();
|
var mainPane = new BorderPane();
|
||||||
menuPane.getChildren().add(mainPane);
|
menuPane.getChildren().add(mainPane);
|
||||||
|
|
||||||
//Awful title
|
var title = new ImageView(getClass().getResource("/images/TetrECS.png").toExternalForm());
|
||||||
var title = new Text("TetrECS");
|
title.setPreserveRatio(true);
|
||||||
title.getStyleClass().add("title");
|
title.setFitWidth(400);
|
||||||
mainPane.setTop(title);
|
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 menuItems = new VBox();
|
||||||
var singlePlayerButton = new Button("Single Player");
|
menuItems.setAlignment(Pos.CENTER);
|
||||||
var multiPlayerButton = new Button("Multi Player");
|
var singlePlayerButton = new Text("Single Player");
|
||||||
var howToPlayButton = new Button("How to Play");
|
var multiPlayerButton = new Text("Multiplayer");
|
||||||
var exitButton = new Button("Exit");
|
var howToPlayButton = new Text("How to Play");
|
||||||
menuItems.getChildren().addAll(singlePlayerButton,multiPlayerButton,howToPlayButton,exitButton);
|
var exitButton = new Text("Exit");
|
||||||
mainPane.setCenter(menuItems);
|
|
||||||
|
|
||||||
// TOOD: window animations
|
menuItems.getChildren().addAll(title,separator,singlePlayerButton,multiPlayerButton,howToPlayButton,exitButton);
|
||||||
|
mainPane.setCenter(menuItems);
|
||||||
|
|
||||||
Multimedia.playMusic("music/menu.mp3");
|
Multimedia.playMusic("music/menu.mp3");
|
||||||
|
|
||||||
//Bind the button action to the startGame method in the menu
|
//Bind the button action to the startGame method in the menu
|
||||||
singlePlayerButton.setOnAction(this::startSinglePlayer);
|
singlePlayerButton.setOnMouseClicked(this::startSinglePlayer);
|
||||||
multiPlayerButton.setOnAction(this::startMultiPlayer);
|
singlePlayerButton.getStyleClass().add("menuItem");
|
||||||
howToPlayButton.setOnAction(this::showInstructions);
|
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
|
@Override
|
||||||
public void initialise() {
|
public void initialise() {
|
||||||
|
logger.info("Initialising " + this.getClass().getName());
|
||||||
|
|
||||||
|
titleAnimation.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle when the Start Single Player Game button is pressed
|
* Handle when the Start Single Player Game button is pressed
|
||||||
* @param event event
|
* @param event event
|
||||||
*/
|
*/
|
||||||
private void startSinglePlayer(ActionEvent event) {
|
private void startSinglePlayer(MouseEvent event) {
|
||||||
|
titleAnimation.stop();
|
||||||
gameWindow.startChallenge();
|
gameWindow.startChallenge();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +119,8 @@ public class MenuScene extends BaseScene {
|
|||||||
* Handle when the Start Multiplayer Game button is pressed
|
* Handle when the Start Multiplayer Game button is pressed
|
||||||
* @param event event
|
* @param event event
|
||||||
*/
|
*/
|
||||||
private void startMultiPlayer(ActionEvent event) {
|
private void startMultiplayer(MouseEvent event) {
|
||||||
|
titleAnimation.stop();
|
||||||
gameWindow.startMultiplayer();
|
gameWindow.startMultiplayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,8 +128,17 @@ public class MenuScene extends BaseScene {
|
|||||||
* Handle when the How to Play button is pressed
|
* Handle when the How to Play button is pressed
|
||||||
* @param event event
|
* @param event event
|
||||||
*/
|
*/
|
||||||
private void showInstructions(ActionEvent event) {
|
private void showInstructions(MouseEvent event) {
|
||||||
|
titleAnimation.stop();
|
||||||
gameWindow.startInstructions();
|
gameWindow.startInstructions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle when the exit button is pressed
|
||||||
|
* @param event event
|
||||||
|
*/
|
||||||
|
private void shutdown(MouseEvent event) {
|
||||||
|
App.getInstance().shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,19 +1,29 @@
|
|||||||
package uk.mgrove.ac.soton.comp1206.scene;
|
package uk.mgrove.ac.soton.comp1206.scene;
|
||||||
|
|
||||||
|
import javafx.animation.FadeTransition;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.SimpleListProperty;
|
import javafx.beans.property.SimpleListProperty;
|
||||||
import javafx.collections.FXCollections;
|
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.Button;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
|
import javafx.scene.text.TextAlignment;
|
||||||
|
import javafx.util.Duration;
|
||||||
import javafx.util.Pair;
|
import javafx.util.Pair;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import uk.mgrove.ac.soton.comp1206.component.ScoresList;
|
import uk.mgrove.ac.soton.comp1206.component.ScoresList;
|
||||||
import uk.mgrove.ac.soton.comp1206.game.Game;
|
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.GamePane;
|
||||||
import uk.mgrove.ac.soton.comp1206.ui.GameWindow;
|
import uk.mgrove.ac.soton.comp1206.ui.GameWindow;
|
||||||
|
|
||||||
@@ -23,6 +33,7 @@ import java.io.FileWriter;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Scanner;
|
import java.util.Scanner;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scene to show scores and record high scores
|
* Scene to show scores and record high scores
|
||||||
@@ -42,7 +53,12 @@ public class ScoresScene extends BaseScene {
|
|||||||
/**
|
/**
|
||||||
* List of scores stored locally
|
* 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
|
* Root pane in scene inside the GamePane
|
||||||
@@ -54,10 +70,20 @@ public class ScoresScene extends BaseScene {
|
|||||||
*/
|
*/
|
||||||
private ScoresList scoresUiList;
|
private ScoresList scoresUiList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node to display online high scores
|
||||||
|
*/
|
||||||
|
private ScoresList remoteScoresUiList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Container for prompting for username when high score achieved
|
* 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
|
* Create a new scores scene
|
||||||
@@ -88,15 +114,37 @@ public class ScoresScene extends BaseScene {
|
|||||||
var mainPane = new BorderPane();
|
var mainPane = new BorderPane();
|
||||||
scoresPane.getChildren().add(mainPane);
|
scoresPane.getChildren().add(mainPane);
|
||||||
|
|
||||||
ArrayList<Pair<String,Integer>> scoresList = new ArrayList<>();
|
String localScoresTitle;
|
||||||
ObservableList<Pair<String,Integer>> observableScoresList = FXCollections.observableArrayList(scoresList);
|
if (game.getClass().equals(MultiplayerGame.class)) {
|
||||||
localScores = new SimpleListProperty<>(observableScoresList);
|
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");
|
loadScores("scores.txt");
|
||||||
if (game.getScore() > localScores.get(localScores.getSize() - 1).getValue()) showHighScorePrompt(game.getScore());
|
localScoresTitle = "Local Scores";
|
||||||
|
}
|
||||||
|
loadOnlineScores();
|
||||||
|
|
||||||
scoresUiList = new ScoresList();
|
scoresUiList = new ScoresList(localScoresTitle);
|
||||||
scoresUiList.bindScores(localScores);
|
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
|
@Override
|
||||||
public void initialise() {
|
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) {
|
public void loadScores(String filePath) {
|
||||||
logger.info("Loading scores from file: {}", 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,10 +218,32 @@ public class ScoresScene extends BaseScene {
|
|||||||
public static SimpleListProperty<Pair<String,Integer>> getScores(String filePath) {
|
public static SimpleListProperty<Pair<String,Integer>> getScores(String filePath) {
|
||||||
logger.info("Retrieving scores from file: {}", filePath);
|
logger.info("Retrieving scores from file: {}", filePath);
|
||||||
|
|
||||||
var scores = new SimpleListProperty<Pair<String,Integer>>(FXCollections.observableArrayList());
|
SimpleListProperty<Pair<String, Integer>> scores;
|
||||||
try {
|
try {
|
||||||
File scoresFile = new File(filePath);
|
File scoresFile = new File(filePath);
|
||||||
Scanner scanner = new Scanner(scoresFile);
|
Scanner scanner = new Scanner(scoresFile);
|
||||||
|
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));
|
||||||
|
writeScores("scores.txt", scores);
|
||||||
|
}
|
||||||
|
logger.info("Retrieved scores: {}", scores);
|
||||||
|
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()) {
|
while (scanner.hasNextLine()) {
|
||||||
var line = scanner.nextLine();
|
var line = scanner.nextLine();
|
||||||
if (line.matches("^.+:[0-9]+$")) {
|
if (line.matches("^.+:[0-9]+$")) {
|
||||||
@@ -136,15 +253,8 @@ public class ScoresScene extends BaseScene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
scanner.close();
|
scanner.close();
|
||||||
} catch (FileNotFoundException e1) {
|
|
||||||
logger.error("Unable to load scores file, writing default scores instead: {}", filePath);
|
|
||||||
|
|
||||||
scores.add(new Pair<>("Sam",3000));
|
logger.info("Parsed scores: {}", scores);
|
||||||
scores.add(new Pair<>("Jane",2000));
|
|
||||||
scores.add(new Pair<>("Pete",1000));
|
|
||||||
writeScores("scores.txt", scores);
|
|
||||||
}
|
|
||||||
logger.info("Retrieved scores: {}", scores);
|
|
||||||
return scores;
|
return scores;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,14 +285,54 @@ public class ScoresScene extends BaseScene {
|
|||||||
private void showHighScorePrompt(int score) {
|
private void showHighScorePrompt(int score) {
|
||||||
logger.info("Showing high score username input prompt");
|
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();
|
var userNameInput = new TextField();
|
||||||
userNameInput.setPromptText("Enter username...");
|
userNameInput.setPromptText("Enter username...");
|
||||||
userNameInput.setOnAction((event) -> saveHighScore(userNameInput.getText(), score));
|
var userNameSubmit = new Text("Submit");
|
||||||
var userNameSubmit = new Button("Submit");
|
userNameSubmit.getStyleClass().add("channelItem");
|
||||||
userNameSubmit.setOnAction((event) -> saveHighScore(userNameInput.getText(), score));
|
|
||||||
userNamePrompt.getChildren().addAll(new Text("Username:"), userNameInput, userNameSubmit);
|
userNameInput.setOnAction((event) -> submitHighScore(score, userNameInput.getText()));
|
||||||
scoresPane.getChildren().add(userNamePrompt);
|
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) {
|
private void saveHighScore(String username, int score) {
|
||||||
logger.info("Saving high score: {} for user: {}", score, username);
|
logger.info("Saving high score: {} for user: {}", score, username);
|
||||||
|
|
||||||
for (var i = 0; i < localScores.getSize(); i++) {
|
for (var i = 0; i <= localScores.getSize(); i++) {
|
||||||
if (localScores.get(i).getValue() < score) {
|
if (i == localScores.getSize() || localScores.get(i).getValue() < score) {
|
||||||
localScores.add(i, new Pair<>(username, score));
|
localScores.add(i, new Pair<>(username, score));
|
||||||
break;
|
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
|
// 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
|
// so while it won't get stored, this isn't an issue
|
||||||
writeScores("scores.txt", localScores);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package uk.mgrove.ac.soton.comp1206.ui;
|
package uk.mgrove.ac.soton.comp1206.ui;
|
||||||
|
|
||||||
|
import javafx.animation.FadeTransition;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.layout.Pane;
|
import javafx.scene.layout.Pane;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import javafx.util.Duration;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import uk.mgrove.ac.soton.comp1206.App;
|
import uk.mgrove.ac.soton.comp1206.App;
|
||||||
@@ -61,8 +63,7 @@ public class GameWindow {
|
|||||||
//Setup communicator
|
//Setup communicator
|
||||||
communicator = new Communicator("ws://ofb-labs.soton.ac.uk:9700");
|
communicator = new Communicator("ws://ofb-labs.soton.ac.uk:9700");
|
||||||
|
|
||||||
//Go to menu
|
startLoadingScreen();
|
||||||
startMenu();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +78,13 @@ public class GameWindow {
|
|||||||
Font.loadFont(getClass().getResourceAsStream("/style/Orbitron-ExtraBold.ttf"),32);
|
Font.loadFont(getClass().getResourceAsStream("/style/Orbitron-ExtraBold.ttf"),32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the loading screen
|
||||||
|
*/
|
||||||
|
public void startLoadingScreen() {
|
||||||
|
loadScene(new LoadingScene(this));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the main menu
|
* Display the main menu
|
||||||
*/
|
*/
|
||||||
@@ -92,10 +100,17 @@ public class GameWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the multiplayer challenge
|
* Display the multiplayer lobby
|
||||||
*/
|
*/
|
||||||
public void startMultiplayer() {
|
public void startMultiplayer() {
|
||||||
// TODO: load multiplayer scene
|
loadScene(new LobbyScene(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the multiplayer challenge
|
||||||
|
*/
|
||||||
|
public void startMultiplayerGame() {
|
||||||
|
loadScene(new MultiplayerScene(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,12 +144,37 @@ public class GameWindow {
|
|||||||
//Create the new scene and set it up
|
//Create the new scene and set it up
|
||||||
newScene.build();
|
newScene.build();
|
||||||
currentScene = newScene;
|
currentScene = newScene;
|
||||||
|
|
||||||
|
if (scene != null) {
|
||||||
scene = newScene.setScene();
|
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);
|
stage.setScene(scene);
|
||||||
|
|
||||||
//Initialise the scene when ready
|
//Initialise the scene when ready
|
||||||
Platform.runLater(() -> currentScene.initialise());
|
Platform.runLater(() -> currentScene.initialise());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup the default scene (an empty black scene) when no scene is loaded
|
* Setup the default scene (an empty black scene) when no scene is loaded
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public class Multimedia {
|
|||||||
if (audioPlayer != null) audioPlayer.stop();
|
if (audioPlayer != null) audioPlayer.stop();
|
||||||
var media = new Media(Multimedia.class.getResource("/" + filePath).toExternalForm());
|
var media = new Media(Multimedia.class.getResource("/" + filePath).toExternalForm());
|
||||||
audioPlayer = new MediaPlayer(media);
|
audioPlayer = new MediaPlayer(media);
|
||||||
audioPlayer.play();
|
// audioPlayer.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,9 +47,8 @@ public class Multimedia {
|
|||||||
if (musicPlayer != null) musicPlayer.stop();
|
if (musicPlayer != null) musicPlayer.stop();
|
||||||
var media = new Media(Multimedia.class.getResource("/" + filePath).toExternalForm());
|
var media = new Media(Multimedia.class.getResource("/" + filePath).toExternalForm());
|
||||||
musicPlayer = new MediaPlayer(media);
|
musicPlayer = new MediaPlayer(media);
|
||||||
musicPlayer.setAutoPlay(true);
|
// musicPlayer.setAutoPlay(true);
|
||||||
musicPlayer.setCycleCount(MediaPlayer.INDEFINITE);
|
musicPlayer.setCycleCount(MediaPlayer.INDEFINITE);
|
||||||
musicPlayer.play();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,18 @@
|
|||||||
-fx-background-color: black;
|
-fx-background-color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
-fx-fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
-fx-text-fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
.menu-background {
|
.menu-background {
|
||||||
-fx-background-image: url("../images/1.jpg");
|
-fx-background-image: url("../images/1.jpg");
|
||||||
-fx-background-size: cover;
|
-fx-background-size: cover;
|
||||||
|
-fx-padding: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.challenge-background {
|
.challenge-background {
|
||||||
@@ -148,6 +157,9 @@
|
|||||||
-fx-border-color: black;
|
-fx-border-color: black;
|
||||||
-fx-stroke: black;
|
-fx-stroke: black;
|
||||||
}
|
}
|
||||||
|
.channelItem:hover {
|
||||||
|
-fx-fill: yellow;
|
||||||
|
}
|
||||||
.channelItem.selected {
|
.channelItem.selected {
|
||||||
-fx-fill: yellow;
|
-fx-fill: yellow;
|
||||||
}
|
}
|
||||||
@@ -181,7 +193,7 @@
|
|||||||
-fx-fill: white;
|
-fx-fill: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField {
|
TextField, .text-field {
|
||||||
-fx-border-color: white;
|
-fx-border-color: white;
|
||||||
-fx-border-width: 1px;
|
-fx-border-width: 1px;
|
||||||
-fx-background-color: rgba(0,0,0,0.5);
|
-fx-background-color: rgba(0,0,0,0.5);
|
||||||
|
|||||||
Reference in New Issue
Block a user