Fix: full view refactor

This commit is contained in:
2026-06-14 16:00:35 +02:00
parent 6c070e1ce5
commit 275dc51409
8 changed files with 64 additions and 86 deletions
@@ -11,10 +11,18 @@ import java.util.Iterator;
import java.util.List;
import java.util.Random;
/**
* Ambient fire particle system for the main game scene.
*
* <p>Spawns embers, sparks, and dust from the four screen corners and animates
* them toward the center using an {@link AnimationTimer} capped at 30 fps.
* Add the pane returned by {@link #getPane()} to the scene root, then call
* {@link #start()} to begin the animation.
*/
public class FireParticleSystem {
private static final int MAX_PARTICLES = 30;
private static final long SPAWN_INTERVAL_NS = 1_00_000_000L;
private static final long SPAWN_INTERVAL_NS = 100_000_000L;
private static final long FRAME_INTERVAL_NS = 1_000_000_000L / 30; // 30 fps cap
private static final int STEPS_PER_FRAME = 3; // physics steps per frame → 3× speed
@@ -34,8 +42,10 @@ public class FireParticleSystem {
pane.setPickOnBounds(false);
}
/** Returns the transparent overlay pane that holds all particle nodes. */
public Pane getPane() { return pane; }
/** Starts the animation timer. */
public void start() {
timer = new AnimationTimer() {
@Override
@@ -48,6 +58,7 @@ public class FireParticleSystem {
timer.start();
}
/** Stops the animation timer. */
public void stop() {
if (timer != null) timer.stop();
}
@@ -34,24 +34,23 @@ import static it.polimi.ingsw.gc14.Model.GamePackage.GameStages.TOTEM_CHOICE;
*/
public class GUI extends Application implements IView {
private Stage primaryStage;
MiniModel miniModel;
private volatile MiniModel miniModel;
private FXMLLoader loaderLogin;
private Scene loginScene;
private LoginFXMLController controllerLogin;
FXMLLoader loaderLogin;
Scene loginScene;
LoginFXMLController controllerLogin;
private FXMLLoader loaderTotem;
private Scene totemScene;
private TotemFXMLController controllerTotem;
FXMLLoader loaderTotem;
Scene totemScene;
TotemFXMLController controllerTotem;
private FXMLLoader loaderMain;
private Scene mainScene;
private MainFXMLController controllerMain;
FXMLLoader loaderMain;
Scene mainScene;
MainFXMLController controllerMain;
FXMLLoader loaderLeaderboard;
Scene leaderboardScene;
LeaderboardFXMLController controllerLeaderboard;
private FXMLLoader loaderLeaderboard;
private Scene leaderboardScene;
private LeaderboardFXMLController controllerLeaderboard;
private ClientController controller;
@@ -164,7 +163,7 @@ public class GUI extends Application implements IView {
*/
@Override
public void setModel(MiniModel miniModel) {
this.miniModel=miniModel;
this.miniModel = miniModel;
}
/**
@@ -183,17 +182,15 @@ public class GUI extends Application implements IView {
public void render() {
Platform.runLater(() -> {
if (miniModel == null) return;
synchronized (miniModel) {
if (controller.getMiniModel().currentState.getGameStage() == TOTEM_CHOICE) {
controllerTotem.render();
fadeToScene(totemScene);
} else if (controller.getMiniModel().currentState.getGameStage() == ENDED) {
controllerLeaderboard.render();
fadeToScene(leaderboardScene);
} else {
controllerMain.render();
fadeToScene(mainScene);
}
if (controller.getMiniModel().currentState.getGameStage() == TOTEM_CHOICE) {
controllerTotem.render();
fadeToScene(totemScene);
} else if (controller.getMiniModel().currentState.getGameStage() == ENDED) {
controllerLeaderboard.render();
fadeToScene(leaderboardScene);
} else {
controllerMain.render();
fadeToScene(mainScene);
}
});
}
@@ -262,10 +259,8 @@ public class GUI extends Application implements IView {
fadeToScene(loginScene);
} else {
if (miniModel == null) return;
synchronized (miniModel) {
controllerMain.isError = true;
controllerMain.render();
}
controllerMain.setError(true);
controllerMain.render();
}
});
}
@@ -40,17 +40,15 @@ 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()
// title label removed; outcome text is added dynamically in render()
private ClientController controller;
private Popup popup;
private VBox popupContent;
// Cache condivisa con MainFXMLController
private static final Map<String, Image> imageCache = new HashMap<>();
private Scene loginScene;
private Stage primaryStage;
private Runnable action;
/** Returns a cached {@link Image} for the given classpath {@code path}, loading it on first access. */
@@ -69,8 +67,8 @@ public class LeaderboardFXMLController {
*/
public void setController(ClientController controller, Runnable action, Scene loginScene) {
this.controller = controller;
this.loginScene=loginScene;
this.action=action;
this.loginScene = loginScene;
this.action = action;
}
@@ -109,7 +107,6 @@ public class LeaderboardFXMLController {
outcomeLabel.setEffect(glow);
rankingList.getChildren().add(outcomeLabel);
// Separatore
Region sep = new Region();
sep.setPrefHeight(16);
rankingList.getChildren().add(sep);
@@ -153,7 +150,6 @@ public class LeaderboardFXMLController {
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)),
@@ -168,7 +164,6 @@ public class LeaderboardFXMLController {
addShadow(row);
addHoverZoom(row);
// Posizione medaglia
Label posLabel = new Label(position + "°");
posLabel.setMinWidth(55);
String medalColor = switch (position) {
@@ -180,13 +175,11 @@ public class LeaderboardFXMLController {
posLabel.setStyle("-fx-font-size: 28px; -fx-font-weight: bold; -fx-text-fill: " + medalColor + ";");
// Totem
ImageView totem = new ImageView(loadImage(
"/GUIImages/Totems/totem_" + player.getTotem().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: " +
@@ -194,10 +187,8 @@ public class LeaderboardFXMLController {
);
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"));
@@ -238,7 +229,6 @@ public class LeaderboardFXMLController {
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);
@@ -274,7 +264,6 @@ public class LeaderboardFXMLController {
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(
@@ -286,7 +275,6 @@ public class LeaderboardFXMLController {
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"},
@@ -297,7 +285,6 @@ public class LeaderboardFXMLController {
{"Building", "buildingCards"}
};
// Griglia unica: icona tipo | carte... per ogni riga
VBox grid = new VBox(6);
grid.setAlignment(Pos.CENTER_LEFT);
boolean hasAnyCard = false;
@@ -312,13 +299,11 @@ public class LeaderboardFXMLController {
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(230);
@@ -331,7 +316,7 @@ public class LeaderboardFXMLController {
}
if (!hasAnyCard) {
Label empty = new Label("Nessuna carta");
Label empty = new Label("No cards");
empty.setStyle("-fx-font-size: 14px; -fx-text-fill: #888888;");
grid.getChildren().add(empty);
}
@@ -342,9 +327,8 @@ public class LeaderboardFXMLController {
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
// altezza massima = 80% della finestra
Window window = rootPane.getScene().getWindow();
scrollPane.setMaxHeight(window.getHeight() * 0.8);
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);
@@ -45,7 +45,6 @@ public class LoginFXMLController {
private boolean isRMI = true;
private ClientController controller;
// Definiamo lo pseudo-stato custom per il CSS corrispondente a ":active-protocol"
private final PseudoClass activeProtocolPseudo = PseudoClass.getPseudoClass("active-protocol");
/**
@@ -71,7 +70,6 @@ public class LoginFXMLController {
protocolToggle.setOnMouseClicked(e -> switchProtocol());
protocolToggle.setStyle(protocolToggle.getStyle() + " -fx-cursor: hand;");
// Inizializza lo stato grafico iniziale (RMI attivo)
labelRMI.pseudoClassStateChanged(activeProtocolPseudo, true);
labelTCP.pseudoClassStateChanged(activeProtocolPseudo, false);
@@ -87,7 +85,7 @@ public class LoginFXMLController {
tt.setToX(isRMI ? 0 : 110);
tt.play();
// Modifica in modo sicuro lo stato senza distruggere i parametri di layout dei nodi
// pseudoClassStateChanged preserves layout properties unlike direct style mutation
labelRMI.pseudoClassStateChanged(activeProtocolPseudo, isRMI);
labelTCP.pseudoClassStateChanged(activeProtocolPseudo, !isRMI);
}
@@ -95,10 +93,10 @@ public class LoginFXMLController {
/** Validates form input and starts a background thread to connect to the server. */
@FXML
private void onAccediClick() {
String nome = campoNome.getText().trim();
String name = campoNome.getText().trim();
String ip = campoIP.getText().trim();
if (nome.isEmpty()) {
if (name.isEmpty()) {
showError("Please select a name.");
return;
}
@@ -112,13 +110,13 @@ public class LoginFXMLController {
}
updateLoginButton(false);
controller.setMyUsername(nome);
controller.setMyUsername(name);
new Thread(() -> {
try {
String localInterface = InterfaceResolver.resolveLocalInterface(ip);
System.setProperty("java.rmi.server.hostname", localInterface);
connect(nome, ip, numPlayers, localInterface);
connect(name, ip, numPlayers, localInterface);
} catch (Exception ex) {
Platform.runLater(() -> {
showError("Network error: " + ex.getMessage());
@@ -69,7 +69,10 @@ public class MainFXMLController {
private ClientController controller;
/** {@code true} when the last received event was an error response. */
public boolean isError;
private boolean isError;
/** Sets the error flag; called by {@link GUI} before delegating to {@link #render()}. */
public void setError(boolean error) { this.isError = error; }
// ==== IMAGE CACHE ====
private static final Map<String, Image> imageCache = new HashMap<>();
@@ -215,7 +218,6 @@ public class MainFXMLController {
card.getStyleClass().add("player-card");
card.setPadding(new Insets(4));
// Riga 1: totem + nome
HBox headerRow = new HBox(8);
headerRow.setAlignment(Pos.CENTER);
ImageView totem = new ImageView(loadImage("/GUIImages/Totems/totem_"
@@ -233,7 +235,6 @@ public class MainFXMLController {
}
headerRow.getChildren().addAll(totem, usernameLabel);
// Riga 2: cibo + prestigio + building
HBox statsRow = new HBox(10);
statsRow.setAlignment(Pos.CENTER);
@@ -269,7 +270,6 @@ public class MainFXMLController {
statsRow.getChildren().addAll(foodBox, prestigeBox, buildingIcon);
// Righe 3-4: icone carte
HBox iconsRow1 = new HBox(6);
iconsRow1.setSpacing(20);
iconsRow1.setAlignment(Pos.CENTER);
@@ -287,7 +287,7 @@ public class MainFXMLController {
};
List<ImageView> icons = new ArrayList<>();
// aggiungo anche building come prima icona della lista per updateSidePanel
// building must be index 0; updateSidePanel iterates icons in ["building","artists",...] order
icons.add(buildingIcon);
for (int i = 0; i < iconTypes.length; i++) {
@@ -406,7 +406,6 @@ public class MainFXMLController {
icons.get(i).setEffect(empty ? new ColorAdjust() : null);
}
// aggiorna current player highlight
VBox card = playerCards.get(u);
if(controller.getMiniModel().disconnectedPlayers.contains(u))
{
@@ -432,7 +431,6 @@ public class MainFXMLController {
card.setStyle("");
}
// animazione shake su errore
if (isError && u.equals(controller.getMyUsername())) {
TranslateTransition tt = new TranslateTransition(Duration.millis(56), card);
tt.setFromX(0);
@@ -611,7 +609,6 @@ public class MainFXMLController {
for (int i = 0; i < controller.getMiniModel().orderLogicCard.getPlayerList().size(); i++) {
OrderPlayer op = controller.getMiniModel().orderLogicCard.getPlayerList().get(i);
// Carica immagine totem come in createSlot
ImageView totem = new ImageView(loadImage(
"/GUIImages/Totems/totem_" + op.getPlayer().getTotem().toString().toLowerCase(Locale.ROOT) + ".png"
));
@@ -626,10 +623,8 @@ public class MainFXMLController {
overlay.prefHeightProperty().bind(card.heightProperty());
overlay.prefWidthProperty().bind(card.widthProperty());
// Scala altezza totem proporzionalmente come in createSlot (0.127 della card)
totem.fitHeightProperty().bind(card.heightProperty().multiply(0.323));
// Posizione Y proporzionale, X fissa
card.heightProperty().addListener((obs, ov, nv) -> {
totem.setLayoutY(nv.doubleValue() * (yRatio-0.196));
});
@@ -637,7 +632,7 @@ public class MainFXMLController {
totem.setLayoutX(nv.doubleValue() * 0.364);
});
// Inizializzazione immediata se già dimensionato
// initialize immediately if already laid out
if (card.getHeight() > 0) totem.setLayoutY(card.getHeight() *(yRatio-0.196));
if (card.getWidth() > 0) totem.setLayoutX(card.getWidth() * 0.364);
@@ -25,8 +25,7 @@ import java.util.Locale;
*/
public class TotemFXMLController {
/** Background image view injected via FXML. */
public ImageView backgroundImage;
@FXML private ImageView backgroundImage;
@FXML private HBox mainHBox;
@FXML private Label turnBanner;
@FXML private Button confirmButton;
@@ -157,7 +156,6 @@ public class TotemFXMLController {
});
card.setOnMouseClicked(e -> {
// reset tutti
mainHBox.getChildren().forEach(n -> {
if (n instanceof VBox v) {
StackPane f = (StackPane) v.getChildren().get(0);
@@ -199,16 +197,13 @@ public class TotemFXMLController {
/** Returns the CSS style string for the totem card frame, highlighting it when {@code selected}. */
private String frameStyle(boolean selected) {
return "-fx-border-color: " + (selected ? "#75FF79" : "rgba(200,120,20,0.28)") + ";" +
"-fx-border-width: " + (selected ? "0" : "0") + ";" +
"-fx-border-radius: 4 4 0 0; -fx-background-radius: 4 4 0 0;" +
(selected ? "-fx-effect: dropshadow(gaussian, #ffffff, 20, 0.33, 0, 0);" : "");
return selected ? "-fx-effect: dropshadow(gaussian, #ffffff, 20, 0.33, 0, 0);" : "";
}
/** Returns the CSS style string for the totem name label, brightening it when {@code selected}. */
private String nameStyle(boolean selected) {
return "-fx-font-family: 'Cinzel'; -fx-font-size: 10; -fx-letter-spacing: 2;" +
"-fx-text-fill: " + (selected ? "#ffffff" : "#ffffff") + ";";
"-fx-text-fill: " + (selected ? "#FFD700" : "#ffffff") + ";";
}
/** Returns the totem name with only the first letter capitalised. */
@@ -10,10 +10,10 @@ public interface IView {
*
* @param miniModel the latest mini model received from the server.
*/
public void setModel(MiniModel miniModel);
void setModel(MiniModel miniModel);
/** Re-renders the view based on the current mini model state. */
public void render();
void render();
/**
* Displays an error message to the user.
@@ -21,6 +21,6 @@ public interface IView {
* @param error the error type.
* @param message a human-readable description of the error.
*/
public void showError(ErrorType error,String message);
void showError(ErrorType error, String message);
}
@@ -78,9 +78,9 @@ public class TUI implements IView {
public synchronized void render() {
if (model == null) return;
GameStages stage = model.currentState.getGameStage();
if (stage.equals(GameStages.TOTEM_CHOICE)) {
if (stage == GameStages.TOTEM_CHOICE) {
display(buildTotemsContent());
} else if (stage.equals(GameStages.ENDED)) {
} else if (stage == GameStages.ENDED) {
display(buildStandingContent() + "\nType 'rematch' to play again or 'quit' to exit");
} else {
display(buildBoardContent());
@@ -106,7 +106,7 @@ public class TUI implements IView {
*/
public void showError(ErrorType error, String message) {
String text;
if (error.equals(ErrorType.WRONG_ACTION)
if (error == ErrorType.WRONG_ACTION
&& model != null
&& model.currentState.getCurrentPlayer() != null
&& !model.currentState.getCurrentPlayer().getUserName().equals(username)) {
@@ -169,7 +169,7 @@ public class TUI implements IView {
}
private String buildStandingContent() {
if (model.standingPlayers == null) return "";
if (model.standingPlayers == null || model.standingPlayers.isEmpty()) return "";
StringBuilder sb = new StringBuilder();
int size = model.standingPlayers.size();
for (int i = 0; i < size / 2; i++) {
@@ -253,7 +253,7 @@ public class TUI implements IView {
/** Returns the display width of a string in terminal columns, stripping ANSI escape codes. */
private int visibleLength(String line) {
return AsciiTable.displayWidth(line.replaceAll("\033\\[[^m]*m", ""));
return AsciiTable.displayWidth(line);
}
// Board / players stamp helpers