Coverage Summary for Class: LeaderboardFXMLController (it.polimi.ingsw.gc14.View.GUI)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
| LeaderboardFXMLController |
0%
(0/1)
|
0%
(0/19)
|
0%
(0/34)
|
0%
(0/181)
|
package it.polimi.ingsw.gc14.View.GUI;
import it.polimi.ingsw.gc14.Controller.ClientController;
import it.polimi.ingsw.gc14.Model.Player;
import javafx.animation.ScaleTransition;
import it.polimi.ingsw.gc14.Model.PlayableCard;
import javafx.event.Event;
import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.stage.Popup;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.paint.CycleMethod;
import javafx.scene.text.Font;
import javafx.stage.Window;
import javafx.util.Duration;
import java.util.*;
/**
* FXML controller for the end-of-game leaderboard scene.
*
* <p>Displays the final player rankings and a winner/game-over banner.
*/
public class LeaderboardFXMLController {
/** Root pane of the leaderboard scene; used to set the background and anchor the popup. */
@FXML private StackPane rootPane;
/** Main vertical container holding the outcome label and ranking rows. */
@FXML private VBox mainVBox;
/** Vertical list populated with one {@code HBox} row per ranked player. */
@FXML private VBox rankingList;
/** Client controller for fetching game state and sending actions. */
private ClientController controller;
/** Auto-hiding popup that shows detailed player card information. */
private Popup popup;
/** Content container inside {@link #popup}. */
private VBox popupContent;
/** Shared image cache to avoid reloading resources multiple times. */
private static final Map<String, Image> imageCache = new HashMap<>();
/** Action run when the player returns to the login screen (disconnect + scene switch). */
private Runnable action;
/** Returns a cached {@link Image} for the given classpath {@code path}, loading it on first access. */
private Image loadImage(String path) {
return imageCache.computeIfAbsent(path,
p -> new Image(Objects.requireNonNull(getClass().getResourceAsStream(p))));
}
/**
* Injects the client controller, a post-game action, and the login scene reference.
*
* @param controller the client controller.
* @param action the action to run when the player returns to the login screen.
*/
public void setController(ClientController controller, Runnable action) {
this.controller = controller;
this.action = action;
}
/** Initializes the scene: loads fonts, sets up the background, and initializes the popup. */
@FXML
public void initialize() {
Font.loadFont(getClass().getResourceAsStream("/Fonts/InknutAntiqua-Regular.ttf"), 14);
mainVBox.sceneProperty().addListener((obs, oldScene, newScene) -> {
if (newScene != null) newScene.getRoot().applyCss();
});
renderBackground();
initPopup();
}
/** Populates the ranking list with the final player standings and shows the outcome banner. */
public void render() {
rankingList.getChildren().clear();
if(!controller.getMiniModel().standingPlayers.isEmpty())
{
List<Player> sorted = controller.getMiniModel().standingPlayers;
boolean iWon = sorted.get(0).getUserName().equals(controller.getMyUsername());
// Label grande Winner / Game Over
Label outcomeLabel = new Label(iWon ? "\uD83C\uDFC6 WINNER! \uD83C\uDFC6" : "GAME OVER");
outcomeLabel.setStyle(
"-fx-font-size: 64px;" +
"-fx-font-weight: bold;" +
"-fx-text-fill: " + (iWon ? "#FFD700;" : "#FF4444;")
);
DropShadow glow = new DropShadow();
glow.setColor(iWon ? Color.rgb(255, 200, 0, 0.95) : Color.rgb(220, 0, 0, 0.95));
glow.setRadius(35);
glow.setSpread(0.35);
outcomeLabel.setEffect(glow);
rankingList.getChildren().add(outcomeLabel);
Region sep = new Region();
sep.setPrefHeight(16);
rankingList.getChildren().add(sep);
for (int i = 0; i < sorted.size(); i++) {
rankingList.getChildren().add(createPlayerRow(i + 1, sorted.get(i)));
}
}
}
// ==== EFFECTS ====
/** Scales {@code node} to 1.02× on hover and sets a hand cursor. */
private void addHoverZoom(Node node) {
ScaleTransition scaleUp = new ScaleTransition(Duration.millis(150), node);
scaleUp.setToX(1.02);
scaleUp.setToY(1.02);
ScaleTransition scaleDown = new ScaleTransition(Duration.millis(150), node);
scaleDown.setToX(1.0);
scaleDown.setToY(1.0);
node.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> { scaleUp.play(); node.setCursor(Cursor.HAND); });
node.addEventHandler(MouseEvent.MOUSE_EXITED, e -> { scaleDown.play(); node.setCursor(Cursor.DEFAULT); });
}
/** Applies a static drop-shadow to {@code node}. */
private void addShadow(Node node) {
DropShadow shadow = new DropShadow();
shadow.setColor(Color.rgb(0, 0, 0, 0.6));
shadow.setRadius(12);
shadow.setOffsetX(3);
shadow.setOffsetY(3);
node.setEffect(shadow);
}
// ==== ROW ====
/** Builds a styled leaderboard row showing rank, totem, username, stats, and prestige for {@code player}. */
private HBox createPlayerRow(int position, Player player) {
HBox row = new HBox(20);
row.setAlignment(Pos.CENTER_LEFT);
row.setPadding(new Insets(14, 28, 14, 28));
row.setMaxWidth(Double.MAX_VALUE);
LinearGradient gradient = new LinearGradient(
0, 0, 1, 0, true, CycleMethod.NO_CYCLE,
new Stop(0.0, Color.rgb(0, 0, 0, 0.75)),
new Stop(0.5, Color.rgb(0, 0, 0, 0.60)),
new Stop(1.0, Color.rgb(0, 0, 0, 0.75))
);
row.setBackground(new Background(new BackgroundFill(
gradient,
new CornerRadii(12),
Insets.EMPTY
)));
addShadow(row);
addHoverZoom(row);
Label posLabel = new Label(position + "°");
posLabel.setMinWidth(55);
String medalColor = switch (position) {
case 1 -> "#FFD700"; // oro
case 2 -> "#C0C0C0"; // argento
case 3 -> "#CD7F32"; // bronzo
default -> "#EEEEEE";
};
posLabel.setStyle("-fx-font-size: 28px; -fx-font-weight: bold; -fx-text-fill: " + medalColor + ";");
ImageView totem = new ImageView(loadImage(
"/GUIImages/Totems/totem_" + player.getTotem().toString().toLowerCase(Locale.ROOT) + ".png"));
totem.setFitHeight(55);
totem.setPreserveRatio(true);
Label nameLabel = new Label(player.getUserName());
nameLabel.setStyle(
"-fx-font-size: 22px; -fx-font-weight: bold; -fx-text-fill: " +
(player.getUserName().equals(controller.getMyUsername()) ? "#ff6b6b;" : "#FFFFFF;")
);
HBox.setHgrow(nameLabel, Priority.ALWAYS);
HBox stats = createStatsBox(player);
HBox ppBox = new HBox(6);
ppBox.setAlignment(Pos.CENTER);
ImageView ppIcon = new ImageView(loadImage("/GUIImages/Icons/PrestigePoint.png"));
ppIcon.setFitHeight(45);
ppIcon.setPreserveRatio(true);
Label ppLabel = new Label(String.valueOf(player.getPrestigeValue()));
ppLabel.setStyle("-fx-font-size: 28px; -fx-font-weight: bold; -fx-text-fill: #FFD700;");
ppBox.getChildren().addAll(ppIcon, ppLabel);
row.getChildren().addAll(posLabel, totem, nameLabel, stats, ppBox);
row.setOnMouseClicked(e -> openPlayerPopup(player));
return row;
}
/** Creates a compact icon+count stats strip for all card types of {@code player}. */
private HBox createStatsBox(Player player) {
HBox stats = new HBox(14);
stats.setAlignment(Pos.CENTER);
stats.getChildren().add(createStatItem("/GUIImages/Icons/Food.png", String.valueOf(player.getFoodValue()), 26));
stats.getChildren().add(createStatItem("/GUIImages/Icons/Artist.png", String.valueOf(player.getArtists().size()), 26));
stats.getChildren().add(createStatItem("/GUIImages/Icons/Gatherer.png", String.valueOf(player.getGatherers().size()), 26));
stats.getChildren().add(createStatItem("/GUIImages/Icons/Inventor.png", String.valueOf(player.getInventors().size()), 26));
stats.getChildren().add(createStatItem("/GUIImages/Icons/Builder.png", String.valueOf(player.getBuilders().size()), 26));
stats.getChildren().add(createStatItem("/GUIImages/Icons/Shaman.png", String.valueOf(player.getShamans().size()), 26));
stats.getChildren().add(createStatItem("/GUIImages/Icons/Hunter.png", String.valueOf(player.getHunters().size()), 26));
stats.getChildren().add(createStatItem("/GUIImages/Icons/Building.png", String.valueOf(player.getBuildingCards().size()), 26));
return stats;
}
/** Creates a single icon + label widget for one stat type. */
private HBox createStatItem(String iconPath, String value, double iconHeight) {
HBox box = new HBox(4);
box.setAlignment(Pos.CENTER);
ImageView icon = new ImageView(loadImage(iconPath));
icon.setFitHeight(iconHeight);
icon.setPreserveRatio(true);
Label label = new Label(value);
label.setStyle("-fx-font-size: 16px; -fx-text-fill: #FFFFFF; -fx-font-weight: bold;");
DropShadow textShadow = new DropShadow();
textShadow.setColor(Color.rgb(0, 0, 0, 0.9));
textShadow.setRadius(4);
label.setEffect(textShadow);
box.getChildren().addAll(icon, label);
return box;
}
// ==== POPUP ====
/** Creates and configures the auto-hiding player-detail popup. */
private void initPopup() {
popupContent = new VBox(12);
popupContent.setAlignment(Pos.CENTER);
popupContent.setPadding(new Insets(16));
popupContent.setStyle(
"-fx-background-color: rgba(20,10,5,0.97);" +
"-fx-border-color: #8B4513;" +
"-fx-border-width: 3;" +
"-fx-border-radius: 14;" +
"-fx-background-radius: 14;"
);
popup = new Popup();
popup.getContent().add(popupContent);
popup.setAutoHide(true);
popup.addEventHandler(Event.ANY, e -> {
if (popup.getScene() != null) popup.getScene().setFill(Color.TRANSPARENT);
});
}
/** Opens the player-detail popup showing all cards held by {@code player}, grouped by type. */
private void openPlayerPopup(Player player) {
popupContent.getChildren().clear();
HBox header = new HBox(10);
header.setAlignment(Pos.CENTER);
ImageView totem = new ImageView(loadImage(
"/GUIImages/Totems/totem_" + player.getTotem().toString().toLowerCase(Locale.ROOT) + ".png"));
totem.setFitHeight(30);
totem.setPreserveRatio(true);
Label nameLabel = new Label(player.getUserName());
nameLabel.setStyle("-fx-font-size: 18px; -fx-font-weight: bold; -fx-text-fill: #FFD700;");
header.getChildren().addAll(totem, nameLabel);
popupContent.getChildren().add(header);
String[][] types = {
{"Artist", "artists"},
{"Gatherer", "gatherers"},
{"Inventor", "inventors"},
{"Builder", "builders"},
{"Shaman", "shamans"},
{"Hunter", "hunters"},
{"Building", "buildingCards"}
};
VBox grid = new VBox(6);
grid.setAlignment(Pos.CENTER_LEFT);
boolean hasAnyCard = false;
for (String[] type : types) {
String typeName = type[0];
String field = type[1];
ArrayList<PlayableCard> cards = getPlayerCards(player.getUserName(), field);
if (cards.isEmpty()) continue;
hasAnyCard = true;
HBox row = new HBox(6);
row.setAlignment(Pos.CENTER_LEFT);
ImageView typeIcon = new ImageView(loadImage("/GUIImages/Icons/"+typeName+".png"));
typeIcon.setFitHeight(22);
typeIcon.setPreserveRatio(true);
row.getChildren().add(typeIcon);
for (PlayableCard card : cards) {
ImageView img = new ImageView(loadImage("/GUIImages/Fronts/card-" + card.getIdIMG() + ".png"));
img.setFitHeight(230);
img.setPreserveRatio(true);
img.setCursor(Cursor.HAND);
row.getChildren().add(img);
}
grid.getChildren().add(row);
}
if (!hasAnyCard) {
Label empty = new Label("No cards");
empty.setStyle("-fx-font-size: 14px; -fx-text-fill: #888888;");
grid.getChildren().add(empty);
}
ScrollPane scrollPane = new ScrollPane(grid);
scrollPane.setFitToWidth(true);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
Window window = rootPane.getScene().getWindow();
scrollPane.setMaxHeight(window.getHeight() * 0.8); // cap scroll area to 80% of window height
popupContent.getChildren().add(scrollPane);
popup.show(window, 0, 0);
popup.getScene().setFill(Color.TRANSPARENT);
popup.setX(window.getX() + (window.getWidth() - popup.getWidth()) / 2);
popup.setY(window.getY() + (window.getHeight() - popup.getHeight()) / 2);
}
/** Returns a copy of the named card collection for {@code username}. */
private ArrayList<PlayableCard> getPlayerCards(String username, String type) {
Player p = controller.getMiniModel().players.get(username);
return switch (type) {
case "artists" -> new ArrayList<>(p.getArtists());
case "gatherers" -> new ArrayList<>(p.getGatherers());
case "inventors" -> new ArrayList<>(p.getInventors());
case "builders" -> new ArrayList<>(p.getBuilders());
case "shamans" -> new ArrayList<>(p.getShamans());
case "hunters" -> new ArrayList<>(p.getHunters());
case "buildingCards" -> new ArrayList<>(p.getBuildingCards());
default -> new ArrayList<>();
};
}
// ==== BACKGROUND ====
/** Sets the full-cover background image on the root pane. */
private void renderBackground() {
BackgroundSize size = new BackgroundSize(
BackgroundSize.AUTO, BackgroundSize.AUTO,
false, false, true, true
);
rootPane.setBackground(new Background(new BackgroundImage(
loadImage("/GUIImages/Background.png"),
BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT,
BackgroundPosition.CENTER,
size
)));
}
// ==== ACTIONS ====
/** Disconnects the client and invokes the post-game action to return to the login scene. */
@FXML
private void onNewGame() {
controller.disconnect();
action.run();
}
}