Feat: Add highscore popup UI and lobby display

Highscore button in GameUI replaces "Info" Button wich was unused
(Issue #110)
This commit is contained in:
Jona Walpert
2026-04-21 20:13:13 +02:00
parent 7674ae57ad
commit 062e66cdf9
7 changed files with 341 additions and 10 deletions
@@ -0,0 +1,76 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import java.util.List;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.stage.Stage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** Controller for the highscore popup window. */
public class HighscoreViewController {
private static final Logger LOGGER = LogManager.getLogger(HighscoreViewController.class);
@FXML private ListView<String> highscoreList;
@FXML private Label statusLabel;
private LobbyClient lobbyClient;
public void setLobbyClient(LobbyClient lobbyClient) {
this.lobbyClient = lobbyClient;
}
/** Loads current highscores from the server and refreshes the list. */
@FXML
public void refreshHighscores() {
if (highscoreList == null || statusLabel == null) {
return;
}
if (lobbyClient == null) {
statusLabel.setText("Lobby client not initialized.");
return;
}
try {
List<String> entries = lobbyClient.getHighscores();
highscoreList.getItems().setAll(entries);
statusLabel.setText(
entries.isEmpty() ? "No highscores yet." : entries.size() + " entries loaded.");
} catch (RuntimeException e) {
LOGGER.error("Failed to load highscores: {}", e.getMessage());
statusLabel.setText("Failed to load highscores.");
}
}
/** Clears all highscores globally on the server and reloads the list. */
@FXML
public void clearHighscores() {
if (lobbyClient == null || statusLabel == null) {
return;
}
try {
lobbyClient.clearHighscores();
refreshHighscores();
statusLabel.setText("Highscores cleared.");
} catch (RuntimeException e) {
LOGGER.error("Failed to clear highscores: {}", e.getMessage());
statusLabel.setText("Failed to clear highscores.");
}
}
/** Closes the highscore popup window. */
@FXML
public void closeWindow() {
if (highscoreList == null || highscoreList.getScene() == null) {
return;
}
Stage stage = (Stage) highscoreList.getScene().getWindow();
stage.close();
}
}
@@ -1,10 +1,12 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService;
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
@@ -1079,4 +1081,44 @@ public class TaskbarController {
private void onBrowserButtonClick() {
CasinoBrowserController.open("wikipedia.org");
}
/** Opens the highscore popup window from the taskbar. */
@FXML
private void onHighscoreButtonClick() {
try {
var shared = ClientApp.getSharedClientService();
if (shared == null || shared.isOffline()) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("Info");
alert.setHeaderText(null);
alert.setContentText("No active server connection for highscores.");
alert.showAndWait();
return;
}
javafx.fxml.FXMLLoader loader =
new javafx.fxml.FXMLLoader(
getClass()
.getResource(
"/ui-structure/gameuicomponents/HighscoreView.fxml"));
javafx.scene.Parent root = loader.load();
HighscoreViewController controller = loader.getController();
controller.setLobbyClient(new LobbyClient(shared));
controller.refreshHighscores();
javafx.stage.Stage stage = new javafx.stage.Stage();
stage.setTitle("Casono Highscores");
javafx.scene.image.Image icon =
new javafx.scene.image.Image(
getClass().getResource("/images/logoinverted.png").toExternalForm());
stage.getIcons().add(icon);
stage.setScene(new javafx.scene.Scene(root));
stage.show();
stage.setAlwaysOnTop(true);
stage.toFront();
} catch (Exception e) {
LOGGER.error("Could not open highscore window from taskbar: {}", e.getMessage());
}
}
}
@@ -4,12 +4,15 @@ import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.HighscoreViewController;
import java.io.IOException;
import java.net.URL;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
@@ -20,6 +23,7 @@ import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -287,4 +291,33 @@ public class CasinomainuiController {
LOGGER.error("Failed to create or add lobby: {}", e.getMessage());
}
}
/** Opens the highscore popup from the lobby UI. */
@FXML
public void handleOpenHighscores() {
try {
FXMLLoader loader =
new FXMLLoader(
getClass()
.getResource(
"/ui-structure/gameuicomponents/HighscoreView.fxml"));
Parent root = loader.load();
HighscoreViewController controller = loader.getController();
controller.setLobbyClient(lobbyClient);
controller.refreshHighscores();
Stage stage = new Stage();
stage.setTitle("Casono Highscores");
String iconPath = getClass().getResource("/images/logoinverted.png").toExternalForm();
stage.getIcons().add(new Image(iconPath));
stage.setScene(new Scene(root));
stage.show();
stage.setAlwaysOnTop(true);
stage.toFront();
} catch (IOException e) {
LOGGER.warn("Could not open highscore window: {}", e.getMessage());
showAlert("Could not open highscore window.");
}
}
}
@@ -81,15 +81,17 @@
</GridPane.margin>
</Button>
<!-- Username input and login/change button -->
<HBox alignment="CENTER_LEFT" spacing="10"
<!-- Username input + change name, with highscores directly below -->
<VBox alignment="TOP_LEFT" spacing="10"
GridPane.rowIndex="0"
GridPane.columnIndex="0"
GridPane.halignment="LEFT"
GridPane.valignment="TOP">
<GridPane.margin>
<Insets top="100" left="20" />
<Insets top="140" left="20" />
</GridPane.margin>
<HBox alignment="CENTER_LEFT" spacing="10">
<TextField fx:id="usernameField"
promptText="Username"
styleClass="gray-input-field"
@@ -100,6 +102,12 @@
styleClass="button-create-lobby" />
</HBox>
<Button text="HIGHSCORES"
styleClass="button-create-lobby"
fx:id="highscoreButton"
onAction="#handleOpenHighscores" />
</VBox>
<VBox fx:id="casinoTable"
alignment="CENTER"
styleClass="casino-table"
@@ -0,0 +1,139 @@
.highscore-root {
-fx-background-color: rgba(13, 158, 59, 0.96);
-fx-background-radius: 16;
-fx-border-radius: 16;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
}
.highscore-title {
-fx-text-fill: #ffffff;
-fx-font-size: 24px;
-fx-font-weight: bold;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);
}
.highscore-status {
-fx-text-fill: #ffffff;
-fx-font-size: 13px;
}
.list-view {
-fx-background-color: #114d26;
-fx-control-inner-background: #114d26;
-fx-background-insets: 0;
-fx-background-radius: 12;
-fx-border-radius: 12;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 2;
}
.list-view .list-cell {
-fx-background-color: transparent;
-fx-text-fill: #ffe89a;
-fx-font-family: "Courier New";
-fx-font-size: 13px;
}
.list-view .list-cell:filled {
-fx-background-color: rgba(0, 0, 0, 0.14);
}
.list-view .list-cell:filled:hover {
-fx-background-color: rgba(0, 0, 0, 0.24);
}
.list-view .list-cell:filled:selected,
.list-view .list-cell:filled:selected:focused {
-fx-background-color: rgba(0, 0, 0, 0.35);
-fx-text-fill: #ffffff;
}
.gray-button,
.yellow-button,
.red-button {
-fx-background-radius: 10;
-fx-border-radius: 10;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-border-width: 2;
-fx-padding: 6 12;
-fx-cursor: hand;
}
.gray-button {
-fx-background-color: #333333;
-fx-border-color: #666666;
-fx-text-fill: #cccccc;
}
.yellow-button {
-fx-background-color: #b8860b;
-fx-border-color: #ffd700;
-fx-text-fill: #ffffff;
}
.red-button {
-fx-background-color: #8b0000;
-fx-border-color: #ff4444;
-fx-text-fill: #ffffff;
}
.highscore-root {
-fx-background-color: #0d9e3b;
-fx-background-radius: 16;
-fx-border-radius: 16;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
}
.highscore-title {
-fx-text-fill: #ffffff;
-fx-font-size: 22px;
-fx-font-weight: bold;
}
.highscore-subtitle {
-fx-text-fill: #f2f2f2;
-fx-font-size: 13px;
}
.highscore-text-area {
-fx-control-inner-background: #1b1b1b;
-fx-text-fill: #ffffff;
-fx-font-family: "Courier New";
-fx-font-size: 14px;
-fx-border-color: #666666;
-fx-border-width: 2;
-fx-border-radius: 10;
-fx-background-radius: 10;
}
.gray-button,
.green-button,
.red-button {
-fx-background-radius: 10;
-fx-border-radius: 10;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-border-width: 2;
-fx-padding: 6 14;
}
.green-button {
-fx-background-color: #1f7d33;
-fx-border-color: #35c759;
-fx-text-fill: #ffffff;
}
.red-button {
-fx-background-color: #8b0000;
-fx-border-color: #ff4444;
-fx-text-fill: #ffffff;
}
.gray-button {
-fx-background-color: #333333;
-fx-border-color: #666666;
-fx-text-fill: #cccccc;
}
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.HighscoreViewController"
spacing="12"
styleClass="highscore-root"
stylesheets="@HighscoreView.css">
<padding>
<Insets top="14" right="14" bottom="14" left="14"/>
</padding>
<Label text="Highscores" styleClass="highscore-title"/>
<ListView fx:id="highscoreList" prefHeight="320" VBox.vgrow="ALWAYS"/>
<Label fx:id="statusLabel" text="Ready." styleClass="highscore-status"/>
<HBox spacing="10">
<Button text="Refresh" onAction="#refreshHighscores" styleClass="gray-button" HBox.hgrow="ALWAYS" maxWidth="Infinity"/>
<Button text="Clear" onAction="#clearHighscores" styleClass="yellow-button" HBox.hgrow="ALWAYS" maxWidth="Infinity"/>
<Button text="Close" onAction="#closeWindow" styleClass="red-button" HBox.hgrow="ALWAYS" maxWidth="Infinity"/>
</HBox>
</VBox>
@@ -19,7 +19,7 @@
<!-- Placeholder buttons -->
<Button text="SETTINGS" styleClass="gray-button" />
<Button text="INFO" styleClass="gray-button" />
<Button text="HIGHSCORE" onAction="#onHighscoreButtonClick" styleClass="gray-button" />
<!-- Buten for the Casono Browser -->
<Button text="HELP" onAction="#onBrowserButtonClick" styleClass="gray-button" />