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();
     }
 }