From 8a8a635db70b011883eb8fec17c801600fe20974 Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Fri, 15 May 2026 23:53:25 +0200 Subject: [PATCH 1/3] Fix: concurrent access in Upper and Lower List, added img cache --- .../RMI/Client/ClientCallbackImpl.java | 17 +- .../it/polimi/ingsw/gc14/View/GUI/GUI.java | 42 +-- .../gc14/View/GUI/MainFXMLController.java | 264 ++++++++---------- .../java/it/polimi/ingsw/gc14/View/IView.java | 1 + .../it/polimi/ingsw/gc14/View/TUI/TUI.java | 5 + 5 files changed, 156 insertions(+), 173 deletions(-) diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Client/ClientCallbackImpl.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Client/ClientCallbackImpl.java index d434fac..74aa52b 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Client/ClientCallbackImpl.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Client/ClientCallbackImpl.java @@ -53,15 +53,12 @@ public class ClientCallbackImpl extends UnicastRemoteObject implements IClientCa */ @Override public void onAction(NetworkEvent event) throws RemoteException { - if(event.getIsError()) { - clientController.view.showError(event.toString()); - } else { - synchronized (clientController) - { - event.apply(clientController.miniModel); - } - clientController.view.render(); - - } + synchronized (clientController) { + if(event.getIsError()) { + clientController.view.showError(event.toString()); + } else { + clientController.view.applyAndRender(event); + } + } } } diff --git a/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java b/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java index f1b14b3..5081e6b 100644 --- a/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java +++ b/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java @@ -2,6 +2,7 @@ package it.polimi.ingsw.gc14.View.GUI; import it.polimi.ingsw.gc14.Controller.ClientController; import it.polimi.ingsw.gc14.Model.MiniModel; +import it.polimi.ingsw.gc14.Network.NetworkEvent; import it.polimi.ingsw.gc14.View.GUI.LoginFXMLController; import it.polimi.ingsw.gc14.View.GUI.MainFXMLController; import it.polimi.ingsw.gc14.View.IView; @@ -29,9 +30,9 @@ public class GUI extends Application implements IView { FXMLLoader loaderMain; Scene mainScene; MainFXMLController controllerMain; + MiniModel miniModel; private ClientController controller; - private MiniModel model; public GUI(Stage stage) { this.primaryStage = stage; @@ -72,30 +73,37 @@ public class GUI extends Application implements IView { @Override public void setModel(MiniModel miniModel) { - this.model=model; + this.miniModel=miniModel; } public void setController(ClientController controller) { this.controller=controller; } + public void applyAndRender(NetworkEvent event) { + Platform.runLater(() -> { + event.apply(miniModel); + if (controller.miniModel.currentState.getGameStage() == TOTEM_CHOICE) { + controllerTotem.render(); + primaryStage.setScene(totemScene); + } else { + controllerMain.render(event); + primaryStage.setScene(mainScene); + } + }); + } + public void render() { - if(controller.miniModel.currentState.getGameStage()==TOTEM_CHOICE) { - Platform.runLater(() -> { - synchronized (this) { - controllerTotem.render(); - primaryStage.setScene(totemScene); - primaryStage.setFullScreen(true); - } - }); + if (!Platform.isFxApplicationThread()) { + Platform.runLater(this::render); + return; + } + if (controller.miniModel.currentState.getGameStage() == TOTEM_CHOICE) { + controllerTotem.render(); + primaryStage.setScene(totemScene); } else { - Platform.runLater(() -> { - synchronized (this) { - controllerMain.render(); - primaryStage.setScene(mainScene); - primaryStage.setFullScreen(true); - } - }); + controllerMain.render(); + primaryStage.setScene(mainScene); } } diff --git a/src/main/java/it/polimi/ingsw/gc14/View/GUI/MainFXMLController.java b/src/main/java/it/polimi/ingsw/gc14/View/GUI/MainFXMLController.java index ae17de4..35fa593 100644 --- a/src/main/java/it/polimi/ingsw/gc14/View/GUI/MainFXMLController.java +++ b/src/main/java/it/polimi/ingsw/gc14/View/GUI/MainFXMLController.java @@ -4,12 +4,11 @@ import it.polimi.ingsw.gc14.Controller.ClientController; import it.polimi.ingsw.gc14.Model.*; import it.polimi.ingsw.gc14.Model.Cards.BuildingCard; import it.polimi.ingsw.gc14.Model.Cards.TribeCard; -import it.polimi.ingsw.gc14.Model.GamePackage.Board; -import it.polimi.ingsw.gc14.Model.GamePackage.CurrentState; import it.polimi.ingsw.gc14.Model.GamePackage.GameStages; import it.polimi.ingsw.gc14.Model.Orders.OrderPlayer; +import it.polimi.ingsw.gc14.Network.EventType; +import it.polimi.ingsw.gc14.Network.NetworkEvent; import javafx.animation.ScaleTransition; -import javafx.application.Platform; import javafx.event.Event; import javafx.fxml.FXML; import javafx.geometry.Insets; @@ -30,10 +29,6 @@ import javafx.stage.Window; import javafx.util.Duration; import java.util.*; -import java.util.stream.Collectors; - -import static it.polimi.ingsw.gc14.Model.GamePackage.GameStages.RES_ACTIONS; -import static it.polimi.ingsw.gc14.Model.GamePackage.GameStages.SLOT_CHOICE; public class MainFXMLController { @@ -55,8 +50,14 @@ public class MainFXMLController { private HBox popupCards; private ClientController controller; - private GameStages old_state_stage; - private String old_state_username; + + // ==== IMAGE CACHE ==== + private static final Map imageCache = new HashMap<>(); + + private Image loadImage(String path) { + return imageCache.computeIfAbsent(path, + p -> new Image(getClass().getResourceAsStream(p))); + } public void setController(ClientController controller) { @@ -64,60 +65,41 @@ public class MainFXMLController { } - - @FXML public void initialize() { - old_state_stage=null; - old_state_username=""; - - - // Import font and apply CSS Font.loadFont(getClass().getResourceAsStream("/Fonts/InknutAntiqua-Regular.ttf"), 14); mainHBox.sceneProperty().addListener((obs, oldScene, newScene) -> { if (newScene != null) { newScene.getRoot().applyCss(); } }); - - // Initialize popup initPopup(); + renderBackground(); // solo una volta } + // ==== RENDER ==== + public void render() { + renderUpper(); + renderBoard(); + renderLower(); + renderMyHand(); + renderSidePanel(); + } - public synchronized void render() { - if (old_state_stage==null) { - renderBackground(); - renderUpper(); + public void render(NetworkEvent event) { + if (event.getEventType() == EventType.SLOT_CHOICE) { renderBoard(); - renderLower(); - renderMyHand(); - renderSidePanel(); - } else if (old_state_stage==SLOT_CHOICE ) { - System.out.println("OLD: "+old_state_stage+", NEW: "+controller.miniModel.currentState.getGameStage()); - System.out.println("RIDISEGNO BOARD\n"); - renderBoard(); - } else if (old_state_stage==RES_ACTIONS) { - System.out.println("OLD: "+old_state_stage+", NEW: "+controller.miniModel.currentState.getGameStage()); - System.out.println("RIDISEGNO TUTTO. USERNAME: "+old_state_username); + } else { renderUpper(); renderBoard(); renderLower(); renderSidePanel(); - if (Objects.equals(old_state_username, controller.myUsername)) { + if (Objects.equals(event.getUsername(), controller.myUsername) + || event.getEventType() == EventType.NEXT_ROUND) { renderMyHand(); } - - // TODO: se sono io a pescare renderizzo myHand, altrimenti no (non è scontato) - } else { - System.out.println("NON GESTITO!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); } - - old_state_stage=controller.miniModel.currentState.getGameStage(); - old_state_username=controller.miniModel.currentState.getCurrentPlayer().getUserName(); - // TODO: le info in sidepanel non vengono aggiornate sempre aggiornate, bisogna prima capire dove metterle - } @@ -130,6 +112,7 @@ public class MainFXMLController { clip.setArcHeight(20); node.setClip(clip); } + private void addHoverZoom(Node node) { ScaleTransition scaleUp = new ScaleTransition(Duration.millis(150), node); scaleUp.setToX(1.1); @@ -138,8 +121,9 @@ public class MainFXMLController { 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); }); + node.addEventHandler(MouseEvent.MOUSE_EXITED, e -> { scaleDown.play(); node.setCursor(Cursor.DEFAULT); }); } + private void addShadow(Node node) { DropShadow shadow = new DropShadow(); shadow.setColor(Color.rgb(0, 0, 0, 0.3)); @@ -164,23 +148,23 @@ public class MainFXMLController { // ==== ELEMENTS ==== private StackPane createOrder(String num) { - ImageView img = new ImageView(new Image(getClass().getResourceAsStream("/GUIImages/Orders/order-" + num + ".png"))); + ImageView img = new ImageView(loadImage("/GUIImages/Orders/order-" + num + ".png")); img.setFitHeight(240); img.setPreserveRatio(true); addClip(img); - StackPane wrapper = new StackPane(img); - return wrapper; + return new StackPane(img); } + private StackPane createSlot(Slot slot, boolean withZoom, boolean withShadow) { - ImageView img = new ImageView(new Image(getClass().getResourceAsStream("/GUIImages/Fronts/card-" + slot.getSlotId() + ".png"))); + ImageView img = new ImageView(loadImage("/GUIImages/Fronts/card-" + slot.getSlotId() + ".png")); img.setFitHeight(240); img.setPreserveRatio(true); addClip(img); StackPane wrapper = new StackPane(img); Player player = controller.miniModel.slotPlayerMap.get(slot); - if (player != null && controller.miniModel.getPositionByUsername(player.getUserName())!=-1) { - ImageView totem = new ImageView(new Image(getClass().getResourceAsStream("/GUIImages/Totems/totem_" + player.totem.toString().toLowerCase(Locale.ROOT) + ".png"))); + if (player != null && controller.miniModel.getPositionByUsername(player.getUserName()) != -1) { + ImageView totem = new ImageView(loadImage("/GUIImages/Totems/totem_" + player.totem.toString().toLowerCase(Locale.ROOT) + ".png")); totem.setFitHeight(70); totem.setPreserveRatio(true); StackPane.setAlignment(totem, Pos.TOP_LEFT); @@ -188,32 +172,23 @@ public class MainFXMLController { wrapper.getChildren().add(totem); } - if (withShadow) { - addShadow(wrapper); - } - if (withZoom) { - addHoverZoom(wrapper); - } + if (withShadow) addShadow(wrapper); + if (withZoom) addHoverZoom(wrapper); return wrapper; } + private StackPane createCard(PlayableCard card, boolean withZoom, boolean withShadow) { - String id = card.getIdIMG(); - ImageView img; - img = new ImageView(new Image(getClass().getResourceAsStream("/GUIImages/Fronts/card-" + id + ".png"))); + ImageView img = new ImageView(loadImage("/GUIImages/Fronts/card-" + card.getIdIMG() + ".png")); img.setFitHeight(200); img.setPreserveRatio(true); addClip(img); - StackPane wrapper = new StackPane(img); - if (withShadow) { - addShadow(wrapper); - } - if (withZoom) { - addHoverZoom(wrapper); - } + if (withShadow) addShadow(wrapper); + if (withZoom) addHoverZoom(wrapper); return wrapper; } + private void renderIcons(HBox iconsRow1, HBox iconsRow2, Player player) { String[][] iconTypes = { {"Artist", "artists"}, @@ -232,7 +207,7 @@ public class MainFXMLController { icon.setFitWidth(28); icon.setFitHeight(28); icon.setPreserveRatio(true); - icon.setImage(new Image(getClass().getResourceAsStream("/GUIImages/Icons/" + iconName + ".png"))); + icon.setImage(loadImage("/GUIImages/Icons/" + iconName + ".png")); ArrayList cards = getPlayerCards(player.getUserName(), fieldName); if (cards.isEmpty()) { @@ -248,6 +223,7 @@ public class MainFXMLController { else iconsRow2.getChildren().add(icon); } } + private ArrayList getPlayerCards(String username, String type) { Player p = controller.miniModel.players.get(username); return switch (type) { @@ -268,59 +244,55 @@ public class MainFXMLController { upperList.setAlignment(Pos.CENTER); upperList.getChildren().clear(); - //synchronized (controller) - //{ - int i = 0; - for (TribeCard card : controller.miniModel.board.upperListTribe) { - final int index = i; - StackPane img = createCard(card, true, true); - img.setOnMouseClicked(e -> controller.drawUpperTribeCard(controller.myUsername, index)); - upperList.getChildren().add(img); - i++; - } - i = 0; - for (BuildingCard card : controller.miniModel.board.upperListBuilding) { - final int index = i; - StackPane img = createCard(card, true, true); - img.setOnMouseClicked(e -> controller.drawUpperBuildingCard(controller.myUsername, index)); - upperList.getChildren().add(img); - i++; - } - //} + int i = 0; + for (TribeCard card : controller.miniModel.board.upperListTribe) { + final int index = i; + StackPane img = createCard(card, true, true); + img.setOnMouseClicked(e -> controller.drawUpperTribeCard(controller.myUsername, index)); + upperList.getChildren().add(img); + i++; + } + i = 0; + for (BuildingCard card : controller.miniModel.board.upperListBuilding) { + final int index = i; + StackPane img = createCard(card, true, true); + img.setOnMouseClicked(e -> controller.drawUpperBuildingCard(controller.myUsername, index)); + upperList.getChildren().add(img); + i++; + } } + private void renderLower() { lowerList.setSpacing(14); lowerList.setAlignment(Pos.CENTER); lowerList.getChildren().clear(); - //synchronized (controller) - //{ - int i = 0; - for (TribeCard card : controller.miniModel.board.lowerListTribe) { - final int index = i; - StackPane img = createCard(card, true, true); - img.setOnMouseClicked(e -> controller.drawLowerTribeCard(controller.myUsername, index)); - lowerList.getChildren().add(img); - i++; - } - i = 0; - for (BuildingCard card : controller.miniModel.board.lowerListBuilding) { - final int index = i; - StackPane img = createCard(card, true, true); - img.setOnMouseClicked(e -> controller.drawLowerBuildingCard(controller.myUsername, index)); - lowerList.getChildren().add(img); - i++; - } - //} - } - private void renderDeck() { - String path = ""; - switch (controller.miniModel.currentState.getEra()) { - case 1 -> path = "/GUIImages/Backs/back-001.png"; - case 2 -> path = "/GUIImages/Backs/back-030.png"; - case 3 -> path = "/GUIImages/Backs/back-058.png"; + int i = 0; + for (TribeCard card : controller.miniModel.board.lowerListTribe) { + final int index = i; + StackPane img = createCard(card, true, true); + img.setOnMouseClicked(e -> controller.drawLowerTribeCard(controller.myUsername, index)); + lowerList.getChildren().add(img); + i++; } - ImageView back = new ImageView(new Image(getClass().getResourceAsStream(path))); + i = 0; + for (BuildingCard card : controller.miniModel.board.lowerListBuilding) { + final int index = i; + StackPane img = createCard(card, true, true); + img.setOnMouseClicked(e -> controller.drawLowerBuildingCard(controller.myUsername, index)); + lowerList.getChildren().add(img); + i++; + } + } + + private void renderDeck() { + String path = switch (controller.miniModel.currentState.getEra()) { + case 1 -> "/GUIImages/Backs/back-001.png"; + case 2 -> "/GUIImages/Backs/back-030.png"; + case 3 -> "/GUIImages/Backs/back-058.png"; + default -> "/GUIImages/Backs/back-001.png"; + }; + ImageView back = new ImageView(loadImage(path)); back.setFitHeight(240); back.setPreserveRatio(true); addClip(back); @@ -328,6 +300,7 @@ public class MainFXMLController { addHoverZoom(back); board.getChildren().add(back); } + private void renderOrder() { StackPane card = createOrder(Integer.toString(controller.miniModel.players.size())); @@ -337,10 +310,10 @@ public class MainFXMLController { StackPane.setMargin(overlay, new Insets(9, 0, 0, 57)); for (OrderPlayer player : controller.miniModel.orderLogicCard.playerList) { - ImageView totem = new ImageView(new Image(getClass().getResourceAsStream("/GUIImages/Totems/totem_" + player.player.totem.toString().toLowerCase(Locale.ROOT) + ".png"))); + ImageView totem = new ImageView(loadImage("/GUIImages/Totems/totem_" + player.player.totem.toString().toLowerCase(Locale.ROOT) + ".png")); totem.setFitHeight(70); totem.setPreserveRatio(true); - if(controller.miniModel.slotPlayerMap.containsValue(player.player)) { + if (controller.miniModel.slotPlayerMap.containsValue(player.player)) { totem.setOpacity(0.5); } overlay.getChildren().add(totem); @@ -348,6 +321,7 @@ public class MainFXMLController { card.getChildren().add(overlay); board.getChildren().add(card); } + private void renderSlotMap() { int i = 0; for (Map.Entry entry : controller.miniModel.slotPlayerMap.entrySet()) { @@ -359,6 +333,7 @@ public class MainFXMLController { i++; } } + private void drawMyHandList(ArrayList cardList) { if (!cardList.isEmpty()) { StackPane img = createCard(cardList.getLast(), true, true); @@ -378,6 +353,7 @@ public class MainFXMLController { myHand.getChildren().add(placeholder); } } + private void renderPlayer(Player player) { VBox card = new VBox(3); VBox.setMargin(card, new Insets(5)); @@ -386,10 +362,10 @@ public class MainFXMLController { addHoverZoom(card); addShadow(card); - // Riga 1: icona totem + nome + // Riga 1: totem + nome HBox headerRow = new HBox(8); headerRow.setAlignment(Pos.CENTER); - ImageView totem = new ImageView(new Image(getClass().getResourceAsStream("/GUIImages/Totems/totem_" + player.totem.toString().toLowerCase(Locale.ROOT) + ".png"))); + ImageView totem = new ImageView(loadImage("/GUIImages/Totems/totem_" + player.totem.toString().toLowerCase(Locale.ROOT) + ".png")); totem.setFitHeight(20); totem.setPreserveRatio(true); Label usernameLabel = new Label(player.getUserName()); @@ -405,7 +381,7 @@ public class MainFXMLController { HBox foodBox = new HBox(4); foodBox.setAlignment(Pos.CENTER); - ImageView foodIcon = new ImageView(new Image(getClass().getResourceAsStream("/GUIImages/Icons/Food.png"))); + ImageView foodIcon = new ImageView(loadImage("/GUIImages/Icons/Food.png")); foodIcon.setFitHeight(20); foodIcon.setPreserveRatio(true); Label foodLabel = new Label(String.valueOf(player.getFoodValue())); @@ -414,7 +390,7 @@ public class MainFXMLController { HBox prestigeBox = new HBox(1); prestigeBox.setAlignment(Pos.CENTER); - ImageView PPIcon = new ImageView(new Image(getClass().getResourceAsStream("/GUIImages/Icons/PrestigePoint.png"))); + ImageView PPIcon = new ImageView(loadImage("/GUIImages/Icons/PrestigePoint.png")); PPIcon.setFitHeight(35); PPIcon.setPreserveRatio(true); Label prestigeLabel = new Label(String.valueOf(player.getPrestigeValue())); @@ -423,12 +399,12 @@ public class MainFXMLController { statsRow.getChildren().addAll(foodBox, prestigeBox); - // Righe 3-4: icone carte (3 per riga) + // Righe 3-4: icone carte HBox iconsRow1 = new HBox(6); iconsRow1.setAlignment(Pos.CENTER); HBox iconsRow2 = new HBox(6); iconsRow2.setAlignment(Pos.CENTER); - renderIcons(iconsRow1, iconsRow2, player); // vedi sotto + renderIcons(iconsRow1, iconsRow2, player); card.getChildren().addAll(headerRow, statsRow, iconsRow1, iconsRow2); sidePanel.getChildren().add(card); @@ -442,13 +418,14 @@ public class MainFXMLController { false, false, true, true ); mainHBox.setBackground(new Background(new BackgroundImage( - new Image(getClass().getResourceAsStream("/GUIImages/Background.png")), + loadImage("/GUIImages/Background.png"), BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.CENTER, size ))); } + private void renderBoard() { board.setSpacing(14); board.setAlignment(Pos.CENTER); @@ -457,19 +434,22 @@ public class MainFXMLController { renderOrder(); renderSlotMap(); } + private void renderMyHand() { myHand.getChildren().clear(); myHand.setSpacing(14); myHand.setAlignment(Pos.CENTER); - drawMyHandList(controller.miniModel.players.get(controller.myUsername).artists); - drawMyHandList(controller.miniModel.players.get(controller.myUsername).gatherers); - drawMyHandList(controller.miniModel.players.get(controller.myUsername).inventors); - drawMyHandList(controller.miniModel.players.get(controller.myUsername).builders); - drawMyHandList(controller.miniModel.players.get(controller.myUsername).shamans); - drawMyHandList(controller.miniModel.players.get(controller.myUsername).hunters); - drawMyHandList(controller.miniModel.players.get(controller.myUsername).buildingCards); + Player me = controller.miniModel.players.get(controller.myUsername); + drawMyHandList(me.artists); + drawMyHandList(me.gatherers); + drawMyHandList(me.inventors); + drawMyHandList(me.builders); + drawMyHandList(me.shamans); + drawMyHandList(me.hunters); + drawMyHandList(me.buildingCards); } + private void renderSidePanel() { sidePanel.getChildren().clear(); sidePanel.setSpacing(6); @@ -486,15 +466,7 @@ public class MainFXMLController { } - - - - - - - - - // POPUP + // ==== POPUP ==== private void initPopup() { popupCards = new HBox(10); popupCards.setAlignment(Pos.CENTER); @@ -507,29 +479,29 @@ public class MainFXMLController { "-fx-background-radius: 10;" ); - addClip(popupCards); - popup = new Popup(); popup.getContent().add(popupCards); popup.setAutoHide(true); - // Aspetta che la scena del popup esista, poi la rendi trasparente popup.addEventHandler(Event.ANY, e -> { if (popup.getScene() != null) { popup.getScene().setFill(Color.TRANSPARENT); } }); } + private void openPopup(ArrayList cardList) { popupCards.getChildren().clear(); for (PlayableCard card : cardList) { - StackPane img = createCard(card, false, false); - popupCards.getChildren().add(img); + popupCards.getChildren().add(createCard(card, false, false)); } - Window window = myHand.getScene().getWindow(); - popup.show(window, (window.getWidth() - popup.getWidth()) / 2, (window.getHeight() - popup.getHeight()) / 2); - // La scena esiste solo dopo show() + Window window = myHand.getScene().getWindow(); + popup.show(window, 0, 0); popup.getScene().setFill(Color.TRANSPARENT); + + // centra dopo show() quando le dimensioni sono disponibili + popup.setX(window.getX() + (window.getWidth() - popup.getWidth()) / 2); + popup.setY(window.getY() + (window.getHeight() - popup.getHeight()) / 2); } -} +} \ No newline at end of file diff --git a/src/main/java/it/polimi/ingsw/gc14/View/IView.java b/src/main/java/it/polimi/ingsw/gc14/View/IView.java index dc0aaa4..f10b639 100644 --- a/src/main/java/it/polimi/ingsw/gc14/View/IView.java +++ b/src/main/java/it/polimi/ingsw/gc14/View/IView.java @@ -7,6 +7,7 @@ import it.polimi.ingsw.gc14.Network.NetworkEvent; public interface IView { public void setModel(MiniModel miniModel); public void render(); + public void applyAndRender(NetworkEvent event); public void showMessage(String message); public void showError(String message); diff --git a/src/main/java/it/polimi/ingsw/gc14/View/TUI/TUI.java b/src/main/java/it/polimi/ingsw/gc14/View/TUI/TUI.java index deed706..b463966 100644 --- a/src/main/java/it/polimi/ingsw/gc14/View/TUI/TUI.java +++ b/src/main/java/it/polimi/ingsw/gc14/View/TUI/TUI.java @@ -1,6 +1,7 @@ package it.polimi.ingsw.gc14.View.TUI; import it.polimi.ingsw.gc14.Model.*; import it.polimi.ingsw.gc14.Model.GamePackage.GameStages; +import it.polimi.ingsw.gc14.Network.NetworkEvent; import it.polimi.ingsw.gc14.View.IView; import java.util.ArrayList; @@ -58,6 +59,10 @@ public class TUI implements IView { this.model = model; this.username = ""; } + public void applyAndRender(NetworkEvent event) { + event.apply(model); + render(); + } /** * Sets the username of the local player. From 571da6552da9044d459b0fa0932471a42bb32d90 Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Sat, 16 May 2026 00:52:28 +0200 Subject: [PATCH 2/3] Added: Leaderboard FXML Scene --- .../it/polimi/ingsw/gc14/View/GUI/GUI.java | 19 +- .../View/GUI/LeaderboardFXMLController.java | 357 ++++++++++++++++++ .../resources/GUIImages/Icons/Building.png | Bin 0 -> 4581 bytes src/main/resources/GUIScene/standing.fxml | 30 ++ 4 files changed, 405 insertions(+), 1 deletion(-) create mode 100644 src/main/java/it/polimi/ingsw/gc14/View/GUI/LeaderboardFXMLController.java create mode 100644 src/main/resources/GUIImages/Icons/Building.png create mode 100644 src/main/resources/GUIScene/standing.fxml diff --git a/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java b/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java index 5081e6b..2b1e3d3 100644 --- a/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java +++ b/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java @@ -1,6 +1,7 @@ package it.polimi.ingsw.gc14.View.GUI; import it.polimi.ingsw.gc14.Controller.ClientController; +import it.polimi.ingsw.gc14.Model.GamePackage.GameStages; import it.polimi.ingsw.gc14.Model.MiniModel; import it.polimi.ingsw.gc14.Network.NetworkEvent; import it.polimi.ingsw.gc14.View.GUI.LoginFXMLController; @@ -32,6 +33,11 @@ public class GUI extends Application implements IView { MainFXMLController controllerMain; MiniModel miniModel; + FXMLLoader loaderLeaderboard; + Scene leaderboardScene; + LeaderboardFXMLController controllerLeaderboard; + + private ClientController controller; public GUI(Stage stage) { @@ -58,6 +64,12 @@ public class GUI extends Application implements IView { controllerMain.setController(controller); return controllerMain; }); + + loaderLeaderboard = new FXMLLoader(getClass().getResource("/GUIScene/standing.fxml")); + leaderboardScene = new Scene(loaderLeaderboard.load()); + controllerLeaderboard = loaderLeaderboard.getController(); + controllerLeaderboard.setController(controller); + mainScene = new Scene(loaderMain.load()); controllerMain = loaderMain.getController(); @@ -86,7 +98,12 @@ public class GUI extends Application implements IView { if (controller.miniModel.currentState.getGameStage() == TOTEM_CHOICE) { controllerTotem.render(); primaryStage.setScene(totemScene); - } else { + } + else if (controller.miniModel.currentState.getGameStage() == GameStages.ENDED) { + controllerLeaderboard.render(); + primaryStage.setScene(leaderboardScene); + primaryStage.setFullScreen(true); + }else { controllerMain.render(event); primaryStage.setScene(mainScene); } diff --git a/src/main/java/it/polimi/ingsw/gc14/View/GUI/LeaderboardFXMLController.java b/src/main/java/it/polimi/ingsw/gc14/View/GUI/LeaderboardFXMLController.java new file mode 100644 index 0000000..0aa8e3b --- /dev/null +++ b/src/main/java/it/polimi/ingsw/gc14/View/GUI/LeaderboardFXMLController.java @@ -0,0 +1,357 @@ +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.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.*; + +public class LeaderboardFXMLController { + + @FXML private StackPane rootPane; + @FXML private VBox mainVBox; + @FXML private VBox rankingList; + // titleLabel rimossa: il testo Winner/GameOver è aggiunto dinamicamente in render() + + private ClientController controller; + + private Popup popup; + private VBox popupContent; + + // Cache condivisa con MainFXMLController + private static final Map imageCache = new HashMap<>(); + + private Image loadImage(String path) { + return imageCache.computeIfAbsent(path, + p -> new Image(getClass().getResourceAsStream(p))); + } + + + public void setController(ClientController controller) { + this.controller = controller; + } + + + @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(); + } + + + public void render() { + rankingList.getChildren().clear(); + + List sorted = new ArrayList<>(controller.miniModel.players.values()); + sorted.sort(Comparator.comparingInt(Player::getPrestigeValue).reversed()); + + boolean iWon = sorted.get(0).getUserName().equals(controller.myUsername); + + // 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); + + // Separatore + 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 ==== + 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); }); + } + + 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 ==== + 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); + + // Sfumatura scura semitrasparente come sfondo della riga + 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); + + // Posizione medaglia + 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 + ";"); + + + // Totem + ImageView totem = new ImageView(loadImage( + "/GUIImages/Totems/totem_" + player.totem.toString().toLowerCase(Locale.ROOT) + ".png")); + totem.setFitHeight(55); + totem.setPreserveRatio(true); + + // Username + Label nameLabel = new Label(player.getUserName()); + nameLabel.setStyle( + "-fx-font-size: 22px; -fx-font-weight: bold; -fx-text-fill: " + + (player.getUserName().equals(controller.myUsername) ? "#ff6b6b;" : "#FFFFFF;") + ); + HBox.setHgrow(nameLabel, Priority.ALWAYS); + + // Statistiche (icona + numero) + HBox stats = createStatsBox(player); + + // Punti prestigio in evidenza a destra + 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; + } + + 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.artists.size()), 26)); + stats.getChildren().add(createStatItem("/GUIImages/Icons/Gatherer.png", String.valueOf(player.gatherers.size()), 26)); + stats.getChildren().add(createStatItem("/GUIImages/Icons/Inventor.png", String.valueOf(player.inventors.size()), 26)); + stats.getChildren().add(createStatItem("/GUIImages/Icons/Builder.png", String.valueOf(player.builders.size()), 26)); + stats.getChildren().add(createStatItem("/GUIImages/Icons/Shaman.png", String.valueOf(player.shamans.size()), 26)); + stats.getChildren().add(createStatItem("/GUIImages/Icons/Hunter.png", String.valueOf(player.hunters.size()), 26)); + stats.getChildren().add(createStatItem("/GUIImages/Icons/Building.png", String.valueOf(player.buildingCards.size()), 26)); + + return stats; + } + + 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;"); + // Ombra sul testo per leggibilità extra + 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 ==== + 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); + }); + } + + private void openPlayerPopup(Player player) { + popupContent.getChildren().clear(); + + // Intestazione con totem + nome + HBox header = new HBox(10); + header.setAlignment(Pos.CENTER); + ImageView totem = new ImageView(loadImage( + "/GUIImages/Totems/totem_" + player.totem.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); + + // Tutte le carte in un unico FlowPane, raggruppate per tipo con separatore icona + String[][] types = { + {"Artist", "artists"}, + {"Gatherer", "gatherers"}, + {"Inventor", "inventors"}, + {"Builder", "builders"}, + {"Shaman", "shamans"}, + {"Hunter", "hunters"}, + {"Building", "buildingCards"} + }; + + // Griglia unica: icona tipo | carte... per ogni riga + 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 cards = getPlayerCards(player.getUserName(), field); + if (cards.isEmpty()) continue; + hasAnyCard = true; + + HBox row = new HBox(6); + row.setAlignment(Pos.CENTER_LEFT); + + // Icona tipo come intestazione riga + ImageView typeIcon = new ImageView(loadImage("/GUIImages/Icons/"+typeName+".png")); + typeIcon.setFitHeight(22); + typeIcon.setPreserveRatio(true); + row.getChildren().add(typeIcon); + + // Carte piccole in riga + for (PlayableCard card : cards) { + ImageView img = new ImageView(loadImage("/GUIImages/Fronts/card-" + card.getIdIMG() + ".png")); + img.setFitHeight(160); + img.setPreserveRatio(true); + // Tooltip al hover: mostra la carta grande in un secondo popup + img.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> img.setFitHeight(180)); + img.addEventHandler(MouseEvent.MOUSE_EXITED, e -> img.setFitHeight(160)); + img.setCursor(Cursor.HAND); + row.getChildren().add(img); + } + + grid.getChildren().add(row); + } + + if (!hasAnyCard) { + Label empty = new Label("Nessuna carta"); + empty.setStyle("-fx-font-size: 14px; -fx-text-fill: #888888;"); + grid.getChildren().add(empty); + } + + grid.setMaxHeight(460); + grid.setMaxWidth(680); + popupContent.getChildren().add(grid); + + Window window = rootPane.getScene().getWindow(); + 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); + } + + private ArrayList getPlayerCards(String username, String type) { + Player p = controller.miniModel.players.get(username); + return switch (type) { + case "artists" -> new ArrayList<>(p.artists); + case "gatherers" -> new ArrayList<>(p.gatherers); + case "inventors" -> new ArrayList<>(p.inventors); + case "builders" -> new ArrayList<>(p.builders); + case "shamans" -> new ArrayList<>(p.shamans); + case "hunters" -> new ArrayList<>(p.hunters); + case "buildingCards" -> new ArrayList<>(p.buildingCards); + default -> new ArrayList<>(); + }; + } + + // ==== BACKGROUND ==== + 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 ==== + @FXML + private void onNewGame() { + //controller.requestNewGame(); + } +} diff --git a/src/main/resources/GUIImages/Icons/Building.png b/src/main/resources/GUIImages/Icons/Building.png new file mode 100644 index 0000000000000000000000000000000000000000..b140952f4d46c35c5a87331a3c9565c683d957e7 GIT binary patch literal 4581 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 P00000NkvXXu0mjfB$x<} literal 0 HcmV?d00001 diff --git a/src/main/resources/GUIScene/standing.fxml b/src/main/resources/GUIScene/standing.fxml new file mode 100644 index 0000000..de0ffa8 --- /dev/null +++ b/src/main/resources/GUIScene/standing.fxml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + +