Coverage Summary for Class: MainFXMLController (it.polimi.ingsw.gc14.View.GUI)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
| MainFXMLController |
0%
(0/1)
|
0%
(0/53)
|
0%
(0/137)
|
0%
(0/448)
|
package it.polimi.ingsw.gc14.View.GUI;
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.Orders.OrderPlayer;
import it.polimi.ingsw.gc14.Network.EventType;
import javafx.application.Platform;
import javafx.animation.PauseTransition;
import javafx.animation.ScaleTransition;
import javafx.animation.TranslateTransition;
import javafx.event.Event;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.effect.ColorAdjust;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.scene.text.Font;
import javafx.stage.Popup;
import javafx.stage.Window;
import javafx.util.Duration;
import java.util.*;
/**
* FXML controller for the main game scene.
*
* <p>Renders the board (upper/lower tribe and building card rows), player stats,
* the player's hand, and action buttons (skip, details).
*/
public class MainFXMLController {
/** Scrollable container holding all player side-panel cards. */
@FXML private ScrollPane playerSide;
/** Root HBox containing all main scene elements. */
@FXML private HBox mainHBox;
/** HBox holding the board slot and order cards. */
@FXML private HBox board;
/** HBox holding the upper tribe card row (and upper building stack). */
@FXML private HBox upperList;
/** HBox holding the lower tribe card row (and lower building stack). */
@FXML private HBox lowerList;
/** HBox displaying the local player's card piles. */
@FXML private HBox myHand;
/** Button that sends a skip-turn command. */
@FXML private Button skipBtn;
/** Button that opens the game details popup. */
@FXML private Button detailsBtn;
/** Button that disconnects and closes the client. */
@FXML private Button quitBtn;
/** Label showing the current round number and game stage. */
@FXML private Label infoText;
/** Maps each player's username to their current food-value label in the side panel. */
private final Map<String, Label> foodLabels = new HashMap<>();
/** Maps each player's username to their current prestige-value label in the side panel. */
private final Map<String, Label> prestigeLabels = new HashMap<>();
/** Maps each player's username to their player-card VBox in the side panel. */
private final Map<String, VBox> playerCards = new HashMap<>();
/** Maps each player's username to their ordered list of card-type icon ImageViews. */
private final Map<String, List<ImageView>> iconViews = new HashMap<>();
/** Shared popup used for card previews and building selection. */
private Popup popup;
/** HBox inside {@link #popup} that holds the displayed card images. */
private HBox popupCards;
/** Client controller for fetching game state and sending player actions. */
private ClientController controller;
/** {@code true} when the last received event was an error response. */
private boolean isError;
/**
* Sets the error flag; called by {@link GUI} before delegating to {@link #render()}.
*
* @param error {@code true} if the last received event was an error response.
*/
public void setError(boolean error) { this.isError = error; }
// ==== IMAGE CACHE ====
private static final Map<String, Image> imageCache = new HashMap<>();
/** 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 and wires up the skip-turn button action.
*
* @param controller the client controller to use.
*/
public void setController(ClientController controller) {
this.controller = controller;
skipBtn.setOnAction(e -> controller.skipTurn());
PauseTransition quitTimer = new PauseTransition(javafx.util.Duration.seconds(3));
quitTimer.setOnFinished(e -> {
quitBtn.setText("Quit");
quitBtn.setStyle("");
});
final boolean[] quitPending = {false};
quitBtn.setOnAction(e -> {
if (quitPending[0]) {
controller.disconnect();
Platform.exit();
System.exit(0);
} else {
quitPending[0] = true;
quitBtn.setText("Sure?");
quitBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white;");
quitTimer.playFromStart();
quitTimer.setOnFinished(ev -> {
quitPending[0] = false;
quitBtn.setText("Quit");
quitBtn.setStyle("");
});
}
});
}
/** Initializes the scene: loads fonts, sets up the popup, and registers input listeners. */
@FXML
public void initialize() {
Font.loadFont(getClass().getResourceAsStream("/Fonts/InknutAntiqua-Regular.ttf"), 14);
addHoverZoom(skipBtn);
addHoverZoom(detailsBtn);
addHoverZoom(quitBtn);
popup = new Popup();
mainHBox.sceneProperty().addListener((obs, oldScene, newScene) -> {
if (newScene != null) {
newScene.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
if (popup.isShowing()) popup.hide();
});
newScene.getRoot().applyCss();
newScene.windowProperty().addListener((o2, ow, nw) -> {
if (nw != null) {
nw.focusedProperty().addListener((o3, wf, nf) -> {
if (!nf) popup.hide();
});
}
});
}
else {
popup.hide();
}
});
initPopup();
detailsBtn.setOnAction(e -> openDetailsPopup());
renderBackground();
isError=false;
}
// ==== RENDER ====
/** Incrementally re-renders only the parts of the scene affected by the last network event. */
public void render() {
if(foodLabels.isEmpty() || controller.getMiniModel().lastEvent == null
|| controller.getMiniModel().lastEvent.getEventType().equals(EventType.TOTEM_CHOICE)
|| controller.getMiniModel().lastEvent.getEventType().equals(EventType.RECONNECT_PLAYER)
|| controller.getMiniModel().lastEvent.getEventType().equals(EventType.DISCONNECTED_PLAYER))
buildSidePanel();
if(isError)
{
updateSidePanel();
isError=false;
return;
}
if(controller.getMiniModel().lastEvent==null ||controller.getMiniModel().lastEvent.getEventType().equals(EventType.TOTEM_CHOICE) || controller.getMiniModel().lastEvent.getEventType().equals(EventType.NEXT_ROUND) || controller.getMiniModel().lastEvent.getEventType().equals(EventType.RECONNECT_PLAYER) || controller.getMiniModel().lastEvent.getEventType().equals(EventType.DISCONNECTED_PLAYER)) {
renderMyHand();
renderUpper();
renderBoard();
renderLower();
updateSidePanel();
return;
}
if(controller.getMiniModel().lastEvent.getEventType().equals(EventType.DRAW_UPPER_TRIBE) || controller.getMiniModel().lastEvent.getEventType().equals(EventType.DRAW_UPPER_BUILD))
{
renderUpper();
}
if(controller.getMiniModel().lastEvent.getEventType().equals(EventType.DRAW_LOWER_TRIBE) || controller.getMiniModel().lastEvent.getEventType().equals(EventType.DRAW_LOWER_BUILD))
{
renderLower();
}
if(controller.getMiniModel().lastEvent.getUsername().equals(controller.getMyUsername()))
{
renderMyHand();
}
renderBoard();
updateSidePanel();
}
// ==== EFFECTS ====
/** Binds a rounded-rectangle clip to {@code img} so its corners are cropped. */
private void addClip(ImageView img) {
Rectangle clip = new Rectangle();
clip.setArcWidth(20);
clip.setArcHeight(20);
img.setClip(clip);
clip.heightProperty().bind(img.layoutBoundsProperty().map(b -> b.getHeight()));
clip.widthProperty().bind(img.layoutBoundsProperty().map(b -> b.getWidth()));
}
/** Scales {@code node} to 1.1× on hover and restores on exit; sets a hand cursor. */
private void addHoverZoom(Node node) {
ScaleTransition scaleUp = new ScaleTransition(Duration.millis(150), node);
scaleUp.setToX(1.1);
scaleUp.setToY(1.1);
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 drop-shadow to {@code node} that intensifies on hover. */
private void addShadow(Node node) {
DropShadow shadow = new DropShadow();
shadow.setColor(Color.rgb(0, 0, 0, 0.3));
shadow.setRadius(6);
shadow.setOffsetX(2);
shadow.setOffsetY(2);
node.setEffect(shadow);
node.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> {
shadow.setRadius(16);
shadow.setOffsetX(4);
shadow.setOffsetY(4);
shadow.setColor(Color.rgb(0, 0, 0, 0.5));
});
node.addEventHandler(MouseEvent.MOUSE_EXITED, e -> {
shadow.setRadius(6);
shadow.setOffsetX(2);
shadow.setOffsetY(2);
shadow.setColor(Color.rgb(0, 0, 0, 0.3));
});
}
// ==== ELEMENTS ====
/** Builds the side-panel card widget for {@code player} with totem, stats, and card-type icons. */
private VBox buildPlayerCard(Player player) {
VBox card = new VBox(3);
VBox.setMargin(card, new Insets(15, 15, 0, 15));
card.getStyleClass().add("player-card");
card.setPadding(new Insets(4));
HBox headerRow = new HBox(8);
headerRow.setAlignment(Pos.CENTER);
ImageView totem = new ImageView(loadImage("/GUIImages/Totems/totem_"
+ player.getTotem().toString().toLowerCase(Locale.ROOT) + ".png"));
totem.setFitHeight(20);
totem.setPreserveRatio(true);
Label usernameLabel = new Label(player.getUserName());
usernameLabel.getStyleClass().add("label-medium");
usernameLabel.setStyle("-fx-text-fill: #711423;");
if (player.getUserName().equals(controller.getMyUsername())) {
usernameLabel.setText(usernameLabel.getText() + " (you)");
}
if (controller.getMiniModel().disconnectedPlayers.contains(player.getUserName())) {
card.getStyleClass().add("player-card-crashed");
}
headerRow.getChildren().addAll(totem, usernameLabel);
HBox statsRow = new HBox(10);
statsRow.setAlignment(Pos.CENTER);
HBox foodBox = new HBox(4);
foodBox.setAlignment(Pos.CENTER);
ImageView foodIcon = new ImageView(loadImage("/GUIImages/Icons/Food.png"));
foodIcon.setFitHeight(20);
foodIcon.setPreserveRatio(true);
Label foodLabel = new Label(String.valueOf(player.getFoodValue()));
foodLabel.getStyleClass().add("label-small");
foodLabels.put(player.getUserName(), foodLabel);
foodBox.getChildren().addAll(foodIcon, foodLabel);
HBox prestigeBox = new HBox(1);
prestigeBox.setAlignment(Pos.CENTER);
ImageView ppIcon = new ImageView(loadImage("/GUIImages/Icons/PrestigePoint.png"));
ppIcon.setFitHeight(35);
ppIcon.setPreserveRatio(true);
Label prestigeLabel = new Label(String.valueOf(player.getPrestigeValue()));
prestigeLabel.getStyleClass().add("label-small");
prestigeLabels.put(player.getUserName(), prestigeLabel);
prestigeBox.getChildren().addAll(ppIcon, prestigeLabel);
ImageView buildingIcon = new ImageView(loadImage("/GUIImages/Icons/Building.png"));
buildingIcon.setFitWidth(28);
buildingIcon.setFitHeight(28);
buildingIcon.setPreserveRatio(true);
buildingIcon.setOnMouseClicked(e -> {
ArrayList<PlayableCard> cards = getPlayerCards(player.getUserName(), "building");
if (!cards.isEmpty()) openPopup(cards);
});
addHoverZoom(buildingIcon);
statsRow.getChildren().addAll(foodBox, prestigeBox, buildingIcon);
HBox iconsRow1 = new HBox(6);
iconsRow1.setSpacing(20);
iconsRow1.setAlignment(Pos.CENTER);
HBox iconsRow2 = new HBox(6);
iconsRow2.setAlignment(Pos.CENTER);
iconsRow2.setSpacing(20);
String[][] iconTypes = {
{"Artist", "artists"},
{"Gatherer", "gatherers"},
{"Inventor", "inventors"},
{"Builder", "builders"},
{"Shaman", "shamans"},
{"Hunter", "hunters"}
};
List<ImageView> icons = new ArrayList<>();
// building must be index 0; updateSidePanel iterates icons in ["building","artists",...] order
icons.add(buildingIcon);
for (int i = 0; i < iconTypes.length; i++) {
String iconName = iconTypes[i][0];
String fieldName = iconTypes[i][1];
ImageView icon = new ImageView(loadImage("/GUIImages/Icons/" + iconName + ".png"));
icon.setFitWidth(28);
icon.setFitHeight(28);
icon.setPreserveRatio(true);
icon.setOnMouseClicked(e -> {
ArrayList<PlayableCard> cards = getPlayerCards(player.getUserName(), fieldName);
if (!cards.isEmpty()) openPopup(cards);
});
addHoverZoom(icon);
icons.add(icon);
if (i < 3) iconsRow1.getChildren().add(icon);
else iconsRow2.getChildren().add(icon);
}
iconViews.put(player.getUserName(), icons);
card.getChildren().addAll(headerRow, statsRow, iconsRow1, iconsRow2);
return card;
}
/** Creates a {@link StackPane} containing the order card image for a game of {@code num} players. */
private StackPane createOrder(String num) {
ImageView img = new ImageView(loadImage("/GUIImages/Orders/order-" + num + ".png"));
img.fitHeightProperty().bind(board.sceneProperty().get().heightProperty().subtract(56).divide(4).multiply(0.94));
img.setPreserveRatio(true);
addClip(img);
return new StackPane(img);
}
/** Creates a {@link StackPane} for a board slot, optionally overlaying the occupying player's totem. */
private StackPane createSlot(Slot slot, boolean withZoom, boolean withShadow) {
ImageView img = new ImageView(loadImage("/GUIImages/Fronts/card-" + slot.getSlotId() + ".png"));
img.setPreserveRatio(true);
img.fitHeightProperty().bind(board.sceneProperty().get().heightProperty().subtract(56).divide(4).multiply(0.94));
addClip(img);
StackPane wrapper = new StackPane(img);
Player player = controller.getMiniModel().slotPlayerMap.get(slot);
if (player != null && controller.getMiniModel().getPositionByUsername(player.getUserName()) != -1) {
ImageView totem = new ImageView(loadImage("/GUIImages/Totems/totem_" + player.getTotem().toString().toLowerCase(Locale.ROOT) + ".png"));
totem.fitHeightProperty().bind(img.fitHeightProperty().multiply(0.332));
totem.setPreserveRatio(true);
StackPane.setAlignment(totem, Pos.TOP_LEFT);
img.fitHeightProperty().addListener((obs, ov, nv) -> {
StackPane.setMargin(totem, new Insets(0, 0, 0, 0.224 * nv.doubleValue()));
});
if (img.getFitHeight() > 0) {
StackPane.setMargin(totem, new Insets(0, 0, 0, 0.224 * img.getFitHeight()));
}
wrapper.getChildren().add(totem);
}
if (withShadow) addShadow(wrapper);
if (withZoom) addHoverZoom(wrapper);
return wrapper;
}
/** Creates a {@link StackPane} for a playable card, scaling its height to a fraction of {@code parent}'s scene. */
private StackPane createCard(PlayableCard card, boolean withZoom, boolean withShadow, Region parent) {
ImageView img = new ImageView(loadImage("/GUIImages/Fronts/card-" + card.getIdIMG() + ".png"));
img.setPreserveRatio(true);
if(controller.getMiniModel().players.size()==5)
{
img.fitHeightProperty().bind(parent.getScene().heightProperty().subtract(56).divide(4).multiply(0.85));
}
else
img.fitHeightProperty().bind(parent.getScene().heightProperty().subtract(56).divide(4).multiply(0.90));
addClip(img);
StackPane wrapper = new StackPane(img);
if (withShadow) addShadow(wrapper);
if (withZoom) addHoverZoom(wrapper);
return wrapper;
}
// ==== GROUPS ====
/** Builds the full side panel from scratch, creating a player card for each connected player. */
private void buildSidePanel() {
infoText.setText("Round: "+ controller.getMiniModel().currentState.getRound() + " • " + controller.getMiniModel().currentState.getGameStage().toString());
VBox playerList = new VBox();
playerList.setFillWidth(true);
for (Player p : controller.getMiniModel().players.values()) {
VBox card = buildPlayerCard(p);
playerCards.put(p.getUserName(), card);
playerList.getChildren().add(card);
}
playerSide.setContent(playerList);
}
/** Updates food/prestige labels, current-player highlight, card-icon opacity, and plays error shake if needed. */
private void updateSidePanel() {
infoText.setText("Round: "+ controller.getMiniModel().currentState.getRound() + " • " + controller.getMiniModel().currentState.getGameStage().toString());
if (controller.getMiniModel().currentState.getCurrentPlayer() == null) return;
String current = controller.getMiniModel().currentState.getCurrentPlayer().getUserName();
String[] fields = {"building", "artists", "gatherers", "inventors", "builders", "shamans", "hunters"};
for (Player p : controller.getMiniModel().players.values()) {
String u = p.getUserName();
foodLabels.get(u).setText(String.valueOf(p.getFoodValue()));
prestigeLabels.get(u).setText(String.valueOf(p.getPrestigeValue()));
List<ImageView> icons = iconViews.get(u);
for (int i = 0; i < icons.size(); i++) {
boolean empty = getPlayerCards(u, fields[i]).isEmpty();
icons.get(i).setOpacity(empty ? 0.3 : 1.0);
icons.get(i).setEffect(empty ? new ColorAdjust() : null);
}
VBox card = playerCards.get(u);
if(controller.getMiniModel().disconnectedPlayers.contains(u))
{
card.getStyleClass().clear();
card.getStyleClass().add("player-card-crashed");
}
else {
card.getStyleClass().clear();
card.getStyleClass().add("player-card");
}
if (u.equals(current)) {
card.setStyle("-fx-effect: dropshadow(gaussian, #fff8dc, 15, 0.10, 0, 0)");
ScaleTransition st = new ScaleTransition(Duration.millis(150), card);
st.setToX(1.1);
st.setToY(1.1);
st.play();
} else {
ScaleTransition st = new ScaleTransition(Duration.millis(150), card);
st.setToX(1.0);
st.setToY(1.0);
st.play();
card.setStyle("");
}
if (isError && u.equals(controller.getMyUsername())) {
TranslateTransition tt = new TranslateTransition(Duration.millis(56), card);
tt.setFromX(0);
tt.setByX(10);
tt.setCycleCount(6);
tt.setAutoReverse(true);
tt.play();
}
}
}
/** Renders the top card of the upper building stack with a count badge; clicking opens the selection popup. */
private void drawUpperBuilding() {
ArrayList<BuildingCard> cardList = new ArrayList<>(controller.getMiniModel().upperListBuildingCards);
if (!cardList.isEmpty()) {
StackPane img = createCard(cardList.getLast(), true, true, upperList);
Label label = new Label(String.valueOf(cardList.size()));
label.setTranslateY(-56);
label.setTranslateX(10);
label.getStyleClass().add("label-large");
StackPane.setAlignment(label, Pos.TOP_RIGHT);
img.getChildren().add(label);
img.setOnMouseClicked(e -> openPopupUpperBuilding(cardList));
upperList.getChildren().add(img);
}
}
/** Renders the top card of the lower building stack with a count badge; clicking opens the selection popup. */
private void drawLowerBuilding() {
ArrayList<BuildingCard> cardList = new ArrayList<>(controller.getMiniModel().lowerListBuildingCards);
if (!cardList.isEmpty()) {
StackPane img = createCard(cardList.getLast(), true, true, lowerList);
Label label = new Label(String.valueOf(cardList.size()));
label.setTranslateY(-56);
label.setTranslateX(10);
label.getStyleClass().add("label-large");
StackPane.setAlignment(label, Pos.TOP_RIGHT);
img.getChildren().add(label);
img.setOnMouseClicked(e -> openPopupLowerBuilding(cardList));
lowerList.getChildren().add(img);
}
}
/** Renders a single card-type pile in the hand area, or an invisible placeholder if the list is empty. */
private void drawMyHandList(ArrayList<? extends PlayableCard> cardList) {
if (!cardList.isEmpty()) {
StackPane img = createCard(cardList.getLast(), true, true, myHand);
Label label = new Label(String.valueOf(cardList.size()));
label.setTranslateY(-56);
label.setTranslateX(10);
label.getStyleClass().add("label-large");
StackPane.setAlignment(label, Pos.TOP_RIGHT);
img.getChildren().add(label);
img.setOnMouseClicked(e -> openPopup(new ArrayList<>(cardList)));
myHand.getChildren().add(img);
} else {
Region placeholder = new Region();
placeholder.prefHeightProperty().bind(myHand.getScene().heightProperty().subtract(56).divide(4).multiply(0.90));
placeholder.prefWidthProperty().bind(placeholder.prefHeightProperty().multiply(0.675));
placeholder.setStyle("-fx-background-color: transparent;");
myHand.getChildren().add(placeholder);
}
}
/** 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 "building" -> new ArrayList<>(p.getBuildingCards());
default -> new ArrayList<>();
};
}
// ==== ABSOLUTES ====
/** Clears and rebuilds the upper tribe row with click handlers for drawing, then appends the upper building stack. */
private void renderUpper() {
upperList.setSpacing(14);
upperList.setAlignment(Pos.CENTER);
upperList.getChildren().clear();
int i = 0;
for (TribeCard card : controller.getMiniModel().upperListTribeCards) {
final int index = i;
StackPane img;
if(!card.isEventCard()) {
img = createCard(card, true, true, upperList);
img.setOnMouseClicked(e -> controller.drawUpperTribeCard(index));
} else {
img = createCard(card, false, false, upperList);
}
upperList.getChildren().add(img);
i++;
}
drawUpperBuilding();
}
/** Clears and rebuilds the lower tribe row with click handlers for drawing, then appends the lower building stack. */
private void renderLower() {
lowerList.setSpacing(14);
lowerList.setAlignment(Pos.CENTER);
lowerList.getChildren().clear();
int i = 0;
for (TribeCard card : controller.getMiniModel().lowerListTribeCards) {
final int index = i;
StackPane img;
if(!card.isEventCard()) {
img = createCard(card, true, true, lowerList);
img.setOnMouseClicked(e -> controller.drawLowerTribeCard(index));
} else {
img = createCard(card, false, false, lowerList);
}
lowerList.getChildren().add(img);
i++;
}
drawLowerBuilding();
}
/** Sets the full-cover background image on the main HBox container. */
private void renderBackground() {
BackgroundSize size = new BackgroundSize(
BackgroundSize.AUTO, BackgroundSize.AUTO,
false, false, true, true
);
mainHBox.setBackground(new Background(new BackgroundImage(
loadImage("/GUIImages/Background.png"),
BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT,
BackgroundPosition.CENTER,
size
)));
}
/** Clears the board area and orchestrates deck, order card, and slot-map rendering. */
private void renderBoard() {
board.setSpacing(14);
board.setAlignment(Pos.CENTER);
board.getChildren().clear();
renderDeck();
renderOrder();
renderSlotMap();
}
/** Adds the era-specific deck back image to the board. */
private void renderDeck() {
String path = switch (controller.getMiniModel().currentState.getEra()) {
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.fitHeightProperty().bind(board.sceneProperty().get().heightProperty().subtract(56).divide(4).multiply(0.94));
back.setPreserveRatio(true);
addClip(back);
board.getChildren().add(back);
}
/** Renders the order card and overlays each player's totem at their proportional position. */
private void renderOrder() {
int numPlayers = controller.getMiniModel().players.size();
StackPane card = createOrder(Integer.toString(numPlayers));
Map<Integer, double[]> slotPositions = Map.of(
2, new double[]{0.229, 0.413},
3, new double[]{0.183, 0.367, 0.548},
4, new double[]{0.142, 0.316, 0.503, 0.690},
5, new double[]{0.066, 0.251, 0.433, 0.617, 0.802}
);
double[] positions = slotPositions.get(numPlayers);
if (positions == null) return;
Pane overlay = new Pane();
overlay.setPickOnBounds(false);
overlay.setMouseTransparent(true);
for (int i = 0; i < controller.getMiniModel().orderLogicCard.getPlayerList().size(); i++) {
OrderPlayer op = controller.getMiniModel().orderLogicCard.getPlayerList().get(i);
ImageView totem = new ImageView(loadImage(
"/GUIImages/Totems/totem_" + op.getPlayer().getTotem().toString().toLowerCase(Locale.ROOT) + ".png"
));
totem.setPreserveRatio(true);
if (op.isPlayed()) {
totem.setVisible(false);
}
final double yRatio = positions[i];
overlay.prefHeightProperty().bind(card.heightProperty());
overlay.prefWidthProperty().bind(card.widthProperty());
totem.fitHeightProperty().bind(card.heightProperty().multiply(0.323));
card.heightProperty().addListener((obs, ov, nv) -> {
totem.setLayoutY(nv.doubleValue() * (yRatio-0.196));
});
card.widthProperty().addListener((obs, ov, nv) -> {
totem.setLayoutX(nv.doubleValue() * 0.364);
});
if (card.getHeight() > 0) totem.setLayoutY(card.getHeight() *(yRatio-0.196));
if (card.getWidth() > 0) totem.setLayoutX(card.getWidth() * 0.364);
overlay.getChildren().add(totem);
}
card.getChildren().add(overlay);
board.getChildren().add(card);
}
/** Adds a clickable slot widget for each entry in the slot-player map. */
private void renderSlotMap() {
int i = 0;
for (Map.Entry<Slot, Player> entry : controller.getMiniModel().slotPlayerMap.entrySet()) {
Slot slot = entry.getKey();
final int index = i;
StackPane img = createSlot(slot, true, true);
img.setOnMouseClicked(e -> controller.slotChoice(index));
board.getChildren().add(img);
i++;
}
}
/** Clears and re-renders all seven card-type piles in the local player's hand area. */
private void renderMyHand() {
myHand.getChildren().clear();
myHand.setSpacing(14);
myHand.setAlignment(Pos.CENTER);
Player me = controller.getMiniModel().players.get(controller.getMyUsername());
drawMyHandList(me.getArtists());
drawMyHandList(me.getGatherers());
drawMyHandList(me.getInventors());
drawMyHandList(me.getBuilders());
drawMyHandList(me.getShamans());
drawMyHandList(me.getHunters());
drawMyHandList(me.getBuildingCards());
}
// ==== POPUP ====
/** Creates and configures the shared card-preview popup container. */
private void initPopup() {
popupCards = new HBox(10);
popupCards.setAlignment(Pos.CENTER);
popupCards.setPadding(new Insets(10));
popupCards.setStyle(
"-fx-background-color: #b42224;" +
"-fx-border-color: black;" +
"-fx-border-width: 2;" +
"-fx-border-radius: 10;" +
"-fx-background-radius: 10;"
);
popup.getContent().add(popupCards);
popup.setAutoHide(false);
popup.addEventHandler(Event.ANY, e -> {
if (popup.getScene() != null) {
popup.getScene().setFill(Color.TRANSPARENT);
}
});
}
/** Centers the popup over the current window and makes it visible. */
private void showPopup() {
Window window = myHand.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);
}
/** Populates the popup with non-clickable card images and shows it. */
private void openPopup(ArrayList<? extends PlayableCard> cardList) {
popupCards.getChildren().clear();
for (PlayableCard card : cardList) {
popupCards.getChildren().add(createCardPopup(card, false, false));
}
showPopup();
}
/** Populates the popup with clickable upper-building cards; clicking one draws it via the controller. */
private void openPopupUpperBuilding(ArrayList<BuildingCard> cardList) {
popupCards.getChildren().clear();
for (int i = 0; i < cardList.size(); i++) {
final int index = i;
StackPane img = createCardPopup(cardList.get(i), true, false);
img.setOnMouseClicked(e -> {
popup.hide();
controller.drawUpperBuildingCard(index);
});
popupCards.getChildren().add(img);
}
showPopup();
}
/** Populates the popup with clickable lower-building cards; clicking one draws it via the controller. */
private void openPopupLowerBuilding(ArrayList<BuildingCard> cardList) {
popupCards.getChildren().clear();
for (int i = 0; i < cardList.size(); i++) {
final int index = i;
StackPane img = createCardPopup(cardList.get(i), true, false);
img.setOnMouseClicked(e -> {
popup.hide();
controller.drawLowerBuildingCard(index);
});
popupCards.getChildren().add(img);
}
showPopup();
}
/** Shows the deck details/rules card in the popup. */
private void openDetailsPopup() {
popupCards.getChildren().clear();
ImageView img = new ImageView(loadImage("/GUIImages/Backs/back-118.png"));
img.setFitHeight(290);
img.setPreserveRatio(true);
addClip(img);
StackPane wrapper = new StackPane(img);
addShadow(wrapper);
popupCards.getChildren().add(wrapper);
showPopup();
}
/** Creates a fixed-height {@link StackPane} card widget suitable for use inside the popup. */
private StackPane createCardPopup(PlayableCard card, boolean withZoom, boolean withShadow) {
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);
return wrapper;
}
}