diff --git a/build.gradle b/build.gradle index 99005a1..62621a0 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ repositories { javafx { version = "25.0.2" - modules = ['javafx.controls', 'javafx.fxml', 'javafx.base', 'javafx.graphics', 'javafx.web'] + modules = ['javafx.controls', 'javafx.fxml', 'javafx.base', 'javafx.graphics', 'javafx.web', 'javafx.media'] } dependencies { diff --git a/documents/docs/networking/commands/protocol-document.md b/documents/docs/networking/commands/protocol-document.md index f267047..633dd16 100644 --- a/documents/docs/networking/commands/protocol-document.md +++ b/documents/docs/networking/commands/protocol-document.md @@ -102,6 +102,14 @@ This document describes the protocol for client-server communication in our appl - [Required pre-execution checks](#required-pre-execution-checks) - [Request Parameters](#request-parameters) - [Success Response](#success-response) + - [LEAVE_LOBBY command](#leave_lobby-command) + - [Required pre-execution checks](#required-pre-execution-checks-1) + - [Request Parameters](#request-parameters-1) + - [Implementation notes](#implementation-notes) + - [Success Response](#success-response-1) + - [Error Response](#error-response-1) + - [Example Request](#example-request-1) + - [Example Response](#example-response-1) - [START_GAME command](#start_game-command) - [GET_LOBBY_STATUS command](#get_lobby_status-command) - [Required pre-execution checks](#required-pre-execution-checks) @@ -671,6 +679,56 @@ END MSG=Lobby not found END +## LEAVE_LOBBY command + +The `LEAVE_LOBBY` command marks the currently logged-in user as absent in the specified lobby while a game is running. The lobby slot is kept reserved so the player can rejoin later. + +### Required pre-execution checks + +- [`UserLoggedInCheck`](#userloggedincheck) + +### Request Parameters + +| Parameter Name | Type | Optional | Description | +| :------------- | :--- | :------: | :---------- | +| `ID` | `int` | no | Numeric id of the target lobby | + +### Implementation notes + +- Parser: `LeaveLobbyParser` — reads the required `ID` parameter and builds `LeaveLobbyRequest`. +- Handler: `LeaveLobbyHandler` — resolves the lobby by id, verifies that a game is currently running, resolves the user from the session and marks the player as absent via `LobbyManager.leavePlayerFromLobby(...)`. + +### Success Response + +No additional response fields. The server replies with a simple `+OK` on success. + +### Error Response + +| Code | Description | +| :--- | :---------- | +| `MISSING_PARAMETER` | Required parameter `ID` missing (parser) | +| `USER_NOT_LOGGED_IN` | No user associated with the session (pre-execution check failed) | +| `LOBBY_NOT_FOUND` | The specified lobby id does not exist | +| `GAME_NOT_RUNNING` | The lobby does not currently have a running game | +| `NOT_IN_LOBBY` | The user is not actively present in the lobby | + +### Example Request + +``` + +LEAVE_LOBBY ID=1 + +``` + +### Example Response (success) + +``` + ++OK +END + +``` + ## START_GAME command The `START_GAME` command requests the server to start a new game in the specified lobby. The diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/Main.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/Main.java index 04046b2..9bbd40a 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/Main.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/Main.java @@ -52,7 +52,7 @@ public final class Main { } private static void printUsage() { - Logger logger = LogManager.getLogger(Main.class); + Logger logger = LogManager.getLogger(Main.class.getSimpleName()); logger.fatal( """ Usage: diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ClientApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ClientApp.java index 897b487..024775d 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ClientApp.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ClientApp.java @@ -15,7 +15,7 @@ import org.apache.logging.log4j.Logger; */ public class ClientApp { - private static final Logger LOGGER = LogManager.getLogger(ClientApp.class); + private static final Logger LOGGER = LogManager.getLogger(ClientApp.class.getSimpleName()); /** Shared client connection used when a username is provided at startup. */ private static volatile ClientService sharedClientService; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java index 58fb787..4d19dcd 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java @@ -88,7 +88,7 @@ public class ChatController { chatModelMap = new LinkedHashMap<>(); localUserList = new ArrayList<>(); this.chatBoxController = new ChatBoxController(username, this); - this.logger = LogManager.getLogger(ChatController.class); + this.logger = LogManager.getLogger(ChatController.class.getSimpleName()); this.serverEventListener = this::handleServerEvent; this.activeChatControllers = new HashMap<>(); @@ -124,7 +124,7 @@ public class ChatController { chatModelMap = new LinkedHashMap<>(); localUserList = new ArrayList<>(); this.chatBoxController = new ChatBoxController(username, this); - this.logger = LogManager.getLogger(ChatController.class); + this.logger = LogManager.getLogger(ChatController.class.getSimpleName()); this.serverEventListener = this::handleServerEvent; this.activeChatControllers = new HashMap<>(); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/game/GameState.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/game/GameState.java index b462ca5..433ccef 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/game/GameState.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/game/GameState.java @@ -18,4 +18,6 @@ public class GameState { public int dealer; public int activePlayer; public int winnerIndex = -1; + public List winnerNames = new ArrayList<>(); + public int potPerWinner; } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java index d46ce22..c20d244 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java @@ -26,7 +26,7 @@ public class ChatClient implements ChatClientInterface { */ public ChatClient(ClientService clientService) { this.clientService = clientService; - this.logger = LogManager.getLogger(ChatClient.class); + this.logger = LogManager.getLogger(ChatClient.class.getSimpleName()); } /** diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java index f2ace09..0ff4054 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java @@ -60,7 +60,7 @@ public class ClientService { public ClientService(String ip, int port) { this.idGenerator = new AtomicInteger(0); - this.logger = LogManager.getLogger(ClientService.class); + this.logger = LogManager.getLogger(ClientService.class.getSimpleName()); this.offlineMode = false; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java index 42b4073..2a6cbc1 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java @@ -111,6 +111,7 @@ public class GameClient { GameState s = new GameState(); s.players = new ArrayList<>(); s.communityCards = new ArrayList<>(); + s.winnerNames = new ArrayList<>(); ParseState state = new ParseState(); for (String raw : input.split("\n")) { @@ -155,6 +156,14 @@ public class GameClient { s.winnerIndex = intVal(l); yield true; } + case String l when l.startsWith("WINNER_NAME=") -> { + s.winnerNames.add(value(l)); + yield true; + } + case String l when l.startsWith("POT_PER_WINNER=") -> { + s.potPerWinner = intVal(l); + yield true; + } case String l when l.startsWith("HIGHSCORE=") -> { s.highscoreEntries.add(value(l)); yield true; @@ -224,6 +233,13 @@ public class GameClient { return true; } + if (line.startsWith("ID=")) { + if (state.currentPlayer.getId() == null) { + state.currentPlayer.setId(PlayerId.of(value(line))); + } + return true; + } + if (line.startsWith("CHIPS=")) { state.currentPlayer.setChips(intVal(line)); return true; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java index 1ca2274..23f4f1a 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java @@ -127,6 +127,15 @@ public class LobbyClient { client.processCommand("JOIN_LOBBY ID=" + lobbyId); } + /** + * Request the server to mark the current user as absent from the given lobby. + * + * @param lobbyId the lobby to leave + */ + public void leaveLobby(int lobbyId) { + client.processCommand("LEAVE_LOBBY ID=" + lobbyId); + } + /** * Logs in to the server with the given username by sending a "LOGIN" command. * @@ -244,6 +253,33 @@ public class LobbyClient { return result; } + /** + * Fetches the current lobby members from GET_LOBBY_STATUS and returns their usernames in order. + * + * @param lobbyId id of the lobby to inspect + * @return list of usernames currently known in the lobby + */ + public List fetchLobbyPlayerNames(int lobbyId) { + List lines = client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId); + List params = ClientService.convertToRequestParameters(lines); + + List names = new ArrayList<>(); + for (RequestParameter parameter : params) { + if (!"USERNAME".equalsIgnoreCase(parameter.key())) { + continue; + } + String name = parameter.value(); + if (name != null) { + String trimmed = name.trim(); + if (!trimmed.isEmpty() && !names.contains(trimmed)) { + names.add(trimmed); + } + } + } + + return names; + } + /** Simple data holder for lobby metadata returned by the server. */ public static final class LobbyInfo { public final int id; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/IntroVideoPlayer.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/IntroVideoPlayer.java new file mode 100644 index 0000000..d4d6fa7 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/IntroVideoPlayer.java @@ -0,0 +1,92 @@ +package ch.unibas.dmi.dbis.cs108.casono.client.ui; + +import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui; +import java.net.URL; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.geometry.Rectangle2D; +import javafx.scene.Scene; +import javafx.scene.layout.StackPane; +import javafx.scene.media.Media; +import javafx.scene.media.MediaPlayer; +import javafx.scene.media.MediaView; +import javafx.stage.Screen; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** Plays an intro video in fullscreen and then starts the main UI. */ +public class IntroVideoPlayer extends Application { + + private static final Logger LOGGER = LogManager.getLogger(IntroVideoPlayer.class); + + private Stage videoStage; + + @Override + public void start(Stage stage) { + this.videoStage = stage; + + URL videoUrl = getClass().getResource("/images/placeholder-animation.mp4"); + + if (videoUrl == null) { + LOGGER.error("Video file not found!"); + Platform.exit(); + return; + } + + Media media = new Media(videoUrl.toExternalForm()); + MediaPlayer mediaPlayer = new MediaPlayer(media); + MediaView mediaView = new MediaView(mediaPlayer); + + StackPane root = new StackPane(mediaView); + + Rectangle2D screenBounds = Screen.getPrimary().getBounds(); + + Scene scene = new Scene(root, screenBounds.getWidth(), screenBounds.getHeight()); + + // scale video automatically + mediaView.setPreserveRatio(true); + mediaView.setFitWidth(screenBounds.getWidth()); + mediaView.setFitHeight(screenBounds.getHeight()); + + stage.initStyle(StageStyle.UNDECORATED); + stage.setFullScreen(true); + stage.setFullScreenExitHint(""); + stage.setScene(scene); + stage.show(); + + mediaPlayer.setOnEndOfMedia(this::onVideoEnd); + mediaPlayer.play(); + } + + private void onVideoEnd() { + videoStage.close(); + setSystemProperties(); + startMainUI(); + } + + private void setSystemProperties() { + var params = getParameters().getRaw(); + if (params == null || params.isEmpty()) { + return; + } + + String arg = params.get(0); + String[] parts = arg.split(":", 2); + if (parts.length == 2) { + System.setProperty("casono.server.host", parts[0]); + System.setProperty("casono.server.port", parts[1]); + } + } + + private void startMainUI() { + Stage mainStage = new Stage(); + try { + new Casinomainui().start(mainStage); + } catch (Exception e) { + e.printStackTrace(); + Platform.exit(); + } + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/Launcher.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/Launcher.java index 85d42df..d1b6300 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/Launcher.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/Launcher.java @@ -1,13 +1,8 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui; -import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui; import javafx.application.Application; -/** - * Launcher for the Casono main UI. - * - *

Default constructor for the application. - */ +/** Launcher for the Casono intro animation. */ public class Launcher { /** Default constructor. */ @@ -21,6 +16,6 @@ public class Launcher { * @param args Command line arguments */ public static void main(String[] args) { - Application.launch(Casinomainui.class, args); + Application.launch(IntroVideoPlayer.class, args); } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java index 897bc04..ceae38c 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java @@ -5,6 +5,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController.ChatKey; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType; import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message; +import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager; import java.io.IOException; import java.net.URL; import java.util.ArrayList; @@ -70,7 +71,11 @@ public class ChatBoxController { ChatModel globalChatModel = new ChatModel(global, username, -1, null); chatController.getChatModelMap().put(new ChatController.ChatKey(global), globalChatModel); addChatTab("GLOBAL", globalChatModel, global); - addWhisperChatButton.setOnAction(_ -> addWhisperChatButton.show()); + addWhisperChatButton.setOnAction( + _ -> { + SoundManager.getInstance().playButtonClick(); + addWhisperChatButton.show(); + }); } /** @@ -90,14 +95,13 @@ public class ChatBoxController { MenuItem menuItem = new MenuItem(targetUserName); addWhisperChatButton.getItems().add(menuItem); menuItem.setOnAction( - _ -> - addWhisperChat( - targetUserName, - new ChatModel( - ChatType.WHISPER, - username, - -1, - targetUserName))); + _ -> { + SoundManager.getInstance().playButtonClick(); + addWhisperChat( + targetUserName, + new ChatModel( + ChatType.WHISPER, username, -1, targetUserName)); + }); }); } @@ -124,14 +128,16 @@ public class ChatBoxController { if (oldUsername.equals(item.getText())) { item.setText(newUsername); item.setOnAction( - _ -> - addWhisperChat( - newUsername, - new ChatModel( - ChatType.WHISPER, - username, - -1, - newUsername))); + _ -> { + SoundManager.getInstance().playButtonClick(); + addWhisperChat( + newUsername, + new ChatModel( + ChatType.WHISPER, + username, + -1, + newUsername)); + }); break; } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java index 7e3699c..d8fa0f8 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java @@ -3,6 +3,7 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel; import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message; +import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager; import java.net.URL; import java.util.ResourceBundle; import javafx.application.Platform; @@ -76,6 +77,7 @@ public class ChatViewController implements Initializable { * The input field is cleared after sending. */ public void sendMessage() { + SoundManager.getInstance().playButtonClick(); String message = inputField.getText().trim(); message = message.replace("'", " "); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java index 61d28bc..75d3910 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java @@ -10,8 +10,12 @@ import ch.unibas.dmi.dbis.cs108.casono.client.game.Player; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState; import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; +import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient; +import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.NotebookController; import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.PlayerStatusController; +import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.SettingsController; import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController; +import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager; import java.io.IOException; import java.net.URL; import java.util.HashSet; @@ -21,6 +25,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Logger; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; +import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.image.Image; @@ -60,11 +65,17 @@ public class CasinoGameController { private GameService gameService; private PlayerId myPlayerId; + private String myPlayerName; @FXML private TaskbarController taskbarIncludeController; private TaskbarController taskbarController; + @FXML private NotebookController notebookIncludeController; + private NotebookController notebookController; + @FXML private SettingsController settingsIncludeController; + // private SettingsController settingsController; private Image dealerImage; private javafx.animation.Timeline timeline; private boolean gameStarted = false; + private boolean firstCardUpdate = true; private java.util.List lastCommunityKeys = java.util.List.of(); private java.util.List lastMyCardKeys = java.util.List.of(); private int lastPot = Integer.MIN_VALUE; @@ -73,6 +84,7 @@ public class CasinoGameController { public static final double CHAT_CONTAINER_CONSTANT = 6.0; private static final int TOTAL_SLOTS = 5; private static final int PLAYER_SLOTS = 2; + private static final int MAX_OPPONENTS_TO_RENDER = 3; private int pot = 0; private static final String BACKSIDE = "/images/card-background-3.png"; private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-3.png"; @@ -115,6 +127,8 @@ public class CasinoGameController { private static final double COMMUNITY_CARD_WIDTH_RATIO = 0.11; private static final double PLAYER_CARD_HEIGHT_RATIO = 0.30; private static final double PLAYER_CARD_WIDTH_RATIO = 0.15; + private static final double DEALER_HEIGHT_RATIO = 0.13; + private static final double DEALER_WIDTH_RATIO = 0.13; private static final double PLAYER_CARDS_Y_OFFSET_FACTOR = 0.10; private static final int[] CHIP_VALUES = { 100000, 50000, 20000, 10000, @@ -139,6 +153,12 @@ public class CasinoGameController { private static final long CHIP_SCALE_DURATION_MS = 220; private static final long CHIP_DROP_DURATION_MS = 250; private static final long CHIP_STAGGER_DELAY_MULTIPLIER = 35L; + private static final int MAX_POT_ROWS = 2; + private static final double POT_CHIP_H_GAP = 6.0; + private static final double POT_CHIP_V_GAP = 6.0; + private static final double POT_WIDTH_FACTOR = 0.45; + private static final double FALLBACK_POT_WIDTH = 500.0; + private static final double FALLBACK_CHIP_WIDTH = 36.0; private static final double CHAT_WIDTH = 400; private static final double CHAT_HEIGHT = 600; private static final String ACTIVE_CARD_BOX_STYLE_CLASS = "player-cards-active-turn"; @@ -156,6 +176,8 @@ public class CasinoGameController { private static final int SMALL_BLIND = 100; private static final int BIG_BLIND = 200; private static final int NO_BET = 0; + private boolean myDealerIconSizeBound; + private java.util.List lobbyPlayerNames = java.util.List.of(); /** Standard constructor. Used by FXML. */ public CasinoGameController() { @@ -178,6 +200,38 @@ public class CasinoGameController { return suit + ":" + value; } + /** + * Count how many new non-null card keys were added when transitioning from old keys to new + * keys. + * + * @param oldKeys The previous list of card keys + * @param newKeys The new list of card keys + * @return The count of new (non-null) cards that were revealed + */ + private int countNewCards(java.util.List oldKeys, java.util.List newKeys) { + int count = 0; + int minSize = Math.min(oldKeys.size(), newKeys.size()); + + // Count cards that changed from "null" to actual card + for (int i = 0; i < minSize; i++) { + String oldKey = oldKeys.get(i); + String newKey = newKeys.get(i); + if ("null".equals(oldKey) && !"null".equals(newKey)) { + count++; + } + } + + // Count any new cards beyond the previous size (if applicable) + for (int i = minSize; i < newKeys.size(); i++) { + String newKey = newKeys.get(i); + if (!"null".equals(newKey)) { + count++; + } + } + + return Math.max(1, count); // At least play one sound if cards changed + } + /** * Generate a list of unique keys for a list of cards. * @@ -230,6 +284,16 @@ public class CasinoGameController { if (taskbarController != null && gameService != null && myPlayerId != null) { taskbarController.setGameService(gameService, myPlayerId); } + + notebookController = resolveNotebookController(); + if (taskbarController != null && notebookController != null) { + taskbarController.setNotebookController(notebookController); + } + + if (taskbarController != null && settingsIncludeController != null) { + taskbarController.setSettingsController(settingsIncludeController); + settingsIncludeController.setThemeChangeListener(this::applyTheme); + } configureTaskbarLobbyActionAnnouncements(); if (communityCardsBox == null) { @@ -251,6 +315,7 @@ public class CasinoGameController { } myDealerIcon.setVisible(false); + styleMyDealerIcon(); tableText.setWrapText(true); javafx.stage.Screen screen = javafx.stage.Screen.getPrimary(); @@ -267,6 +332,8 @@ public class CasinoGameController { renderPlayerCards(List.of()); chatContainer.toFront(); + + javafx.application.Platform.runLater(() -> applyTheme("standard")); } /** @@ -326,91 +393,92 @@ public class CasinoGameController { // Test method to demonstrate the UI functionality with sample data. // @FXML // public void uiTest() { - // if (communityCardsBox == null) { - // LOGGER.info("communityCardsBox is NULL"); - // return; - // } + // if (communityCardsBox == null) { + // LOGGER.info("communityCardsBox is NULL"); + // return; + // } // - // try { - // renderCommunityCards( - // List.of( - // new Card("ace", "hearts"), - // new Card("king", "spades"), - // new Card("10", "diamonds"))); + // try { + // renderCommunityCards( + // List.of( + // new Card("ace", "hearts"), + // new Card("king", "spades"), + // new Card("10", "diamonds"))); // - // renderPlayerCards(List.of(new Card("10", "spades"), new Card("king", "spades"))); - // } catch (Exception e) { - // e.printStackTrace(); - // } + // renderPlayerCards(List.of(new Card("10", "spades"), new Card("king", + // "spades"))); + // } catch (Exception e) { + // e.printStackTrace(); + // } // - // Player mathis = new Player(PlayerId.of("Mathis"), 20000); - // mathis.setState(PlayerState.ACTIVE); + // Player mathis = new Player(PlayerId.of("Mathis"), 20000); + // mathis.setState(PlayerState.ACTIVE); // - // Player jona = new Player(PlayerId.of("Jona"), 20000); - // jona.setState(PlayerState.FOLDED); + // Player jona = new Player(PlayerId.of("Jona"), 20000); + // jona.setState(PlayerState.FOLDED); // - // Player lars = new Player(PlayerId.of("Lars"), 20000); - // lars.setState(PlayerState.ACTIVE); + // Player lars = new Player(PlayerId.of("Lars"), 20000); + // lars.setState(PlayerState.ACTIVE); // - // player1Controller.setPlayer(mathis); - // player2Controller.setPlayer(jona); - // player3Controller.setPlayer(lars); + // player1Controller.setPlayer(mathis); + // player2Controller.setPlayer(jona); + // player3Controller.setPlayer(lars); // - // LOGGER.info( - // "Player 1: " - // + mathis.getName() - // + " | $" - // + mathis.getChips() - // + " | " - // + mathis.getState()); - // LOGGER.info( - // "Player 2: " - // + jona.getName() - // + " | $" - // + jona.getChips() - // + " | " - // + jona.getState()); - // LOGGER.info( - // "Player 3: " - // + lars.getName() - // + " | $" - // + lars.getChips() - // + " | " - // + lars.getState()); + // LOGGER.info( + // "Player 1: " + // + mathis.getName() + // + " | $" + // + mathis.getChips() + // + " | " + // + mathis.getState()); + // LOGGER.info( + // "Player 2: " + // + jona.getName() + // + " | $" + // + jona.getChips() + // + " | " + // + jona.getState()); + // LOGGER.info( + // "Player 3: " + // + lars.getName() + // + " | $" + // + lars.getChips() + // + " | " + // + lars.getState()); // - // if (playerStatusController != null) { - // playerStatusController.setPlayer(mathis); - // } else { - // LOGGER.warning("PlayerStatusController is NULL"); - // } + // if (playerStatusController != null) { + // playerStatusController.setPlayer(mathis); + // } else { + // LOGGER.warning("PlayerStatusController is NULL"); + // } // - // javafx.stage.Screen screen = javafx.stage.Screen.getPrimary(); - // double screenHeight = screen.getBounds().getHeight(); + // javafx.stage.Screen screen = javafx.stage.Screen.getPrimary(); + // double screenHeight = screen.getBounds().getHeight(); // - // casinoTableInnerBox.setTranslateY(-screenHeight * 0.08); - // playerCardsBox.setTranslateY(screenHeight * 0.35); + // casinoTableInnerBox.setTranslateY(-screenHeight * 0.08); + // playerCardsBox.setTranslateY(screenHeight * 0.35); // - // renderPot(500); + // renderPot(500); // - // lars.removeChips(500); - // LOGGER.info("Lars: $" + lars.getChips()); + // lars.removeChips(500); + // LOGGER.info("Lars: $" + lars.getChips()); // - // player3Controller.refresh(); + // player3Controller.refresh(); // - // mathis.fall(); - // player1Controller.refresh(); + // mathis.fall(); + // player1Controller.refresh(); // - // GameState s = new GameState(); - // s.dealer = 3; - // highlightDealer(s); + // GameState s = new GameState(); + // s.dealer = 3; + // highlightDealer(s); // - // s.phase = "FLOP"; + // s.phase = "FLOP"; // - // updateGameInfo(s); + // updateGameInfo(s); // - // s.winnerIndex = 1; + // s.winnerIndex = 1; // - // updateGameInfo(s); + // updateGameInfo(s); // } /** Start the UI update loop. */ @@ -475,6 +543,7 @@ public class CasinoGameController { .thenAccept( state -> { if (state == null) { + refreshLobbyOpponentsBeforeGameStart(); LOGGER.info("No state received yet."); return; } @@ -492,6 +561,36 @@ public class CasinoGameController { }); } + /** + * Refresh the list of opponents in the lobby before the game starts by fetching the latest + * player. + */ + private void refreshLobbyOpponentsBeforeGameStart() { + if (gameService != null && gameService.peekStateOrNull() != null) { + return; + } + if (chatClientService == null || chatLobbyId < 0) { + return; + } + + try { + LobbyClient lobbyClient = new LobbyClient(chatClientService); + List latestNames = lobbyClient.fetchLobbyPlayerNames(chatLobbyId); + if (latestNames == null) { + latestNames = List.of(); + } + if (latestNames.equals(lobbyPlayerNames)) { + return; + } + + List namesToRender = List.copyOf(latestNames); + javafx.application.Platform.runLater(() -> setLobbyPlayerNames(namesToRender)); + } catch (Exception e) { + String msg = "Could not refresh lobby player names: " + e.getMessage(); + LOGGER.fine(msg); + } + } + /** * Updates all visual UI components with the data from the provided GameState. This includes * community cards, pot, player information, and the taskbar. @@ -577,14 +676,28 @@ public class CasinoGameController { var newCommunityKeys = cardKeys(community, TOTAL_SLOTS); if (!newCommunityKeys.equals(lastCommunityKeys)) { + int newCardCount = countNewCards(lastCommunityKeys, newCommunityKeys); lastCommunityKeys = newCommunityKeys; renderCommunityCards(community); + if (!firstCardUpdate) { + SoundManager.getInstance().playCardRevealMultiple(newCardCount); + } } var newMyCardKeys = cardKeys(myCards, PLAYER_SLOTS); if (!newMyCardKeys.equals(lastMyCardKeys)) { + int newCardCount = countNewCards(lastMyCardKeys, newMyCardKeys); lastMyCardKeys = newMyCardKeys; renderPlayerCards(myCards); + // Don't play sound on first card update (initial animation) + if (!firstCardUpdate) { + SoundManager.getInstance().playCardRevealMultiple(newCardCount); + } + } + + // After first update, enable sounds for subsequent card reveals + if (firstCardUpdate) { + firstCardUpdate = false; } } @@ -621,12 +734,12 @@ public class CasinoGameController { */ private List getMyCards(List players) { - if (myPlayerId == null || players == null) { + if (players == null) { return List.of(); } for (Player p : players) { - if (p.getId() != null && p.getId().equals(myPlayerId)) { + if (isCurrentPlayer(p)) { return p.getCards() != null ? p.getCards() : List.of(); } } @@ -641,13 +754,13 @@ public class CasinoGameController { */ private void updatePlayerCards(GameState s) { - if (s.players == null || myPlayerId == null) { + if (s.players == null) { renderPlayerCards(List.of()); return; } for (Player p : s.players) { - if (p.getId().equals(myPlayerId)) { + if (isCurrentPlayer(p)) { renderPlayerCards(p.getCards()); return; } @@ -672,7 +785,18 @@ public class CasinoGameController { String winnerName = resolveWinnerName(s); if (winnerName != null && !winnerName.isBlank()) { - tableText.setText("Winner: " + winnerName); + StringBuilder winnerText = new StringBuilder("Winner"); + + if (s.winnerNames != null && s.winnerNames.size() > 1) { + winnerText.append("s: ").append(String.join(", ", s.winnerNames)); + if (s.potPerWinner > 0) { + winnerText.append(" (").append(s.potPerWinner).append(" each)"); + } + } else { + winnerText.append(": ").append(winnerName); + } + + tableText.setText(winnerText.toString()); return; } @@ -770,13 +894,13 @@ public class CasinoGameController { */ private void updateTaskbar(GameState s) { TaskbarController controller = resolveTaskbarController(); - if (controller == null || myPlayerId == null) { + if (controller == null) { return; } if (gameService != null) { controller.setGameService(gameService, myPlayerId); } - controller.update(s, myPlayerId); + controller.update(s, myPlayerName); } /** @@ -832,30 +956,51 @@ public class CasinoGameController { * opponents. */ private java.util.List buildOpponents(List players) { - java.util.List opponents = new java.util.ArrayList<>(); - boolean meFound = false; - - for (Player p : players) { - if (p == null) { - continue; - } - - PlayerId pid = p.getId(); - boolean isMe = myPlayerId != null && myPlayerId.equals(pid); - - if (isMe) { - meFound = true; - } else { - opponents.add(p); - } + if (players == null || players.isEmpty()) { + return java.util.List.of(); } - if (!meFound) { - opponents.clear(); - opponents.addAll(players); + int myIndex = findCurrentPlayerIndex(players); + if (myIndex < 0) { + java.util.List fallback = new java.util.ArrayList<>(); + for (Player player : players) { + if (player != null) { + fallback.add(player); + } + } + return fallback; } - return opponents; + java.util.List ordered = new java.util.ArrayList<>(Math.max(0, players.size() - 1)); + for (int offset = 1; offset < players.size(); offset++) { + int index = (myIndex + offset) % players.size(); + Player player = players.get(index); + if (player != null) { + ordered.add(player); + } + } + return ordered; + } + + /** + * Find the index of the current player in the provided list of players by comparing their + * PlayerId with myPlayerId. + * + * @param players The list of players to search through, which may include the current player + * and their opponents. + * @return The index of the current player in the list if found, or -1 if the current player is + * not present in the list or if the list is null or empty. + */ + private int findCurrentPlayerIndex(List players) { + if (players == null || players.isEmpty()) { + return -1; + } + for (int i = 0; i < players.size(); i++) { + if (isCurrentPlayer(players.get(i))) { + return i; + } + } + return -1; } /** @@ -970,12 +1115,12 @@ public class CasinoGameController { clearActiveTurnHighlights(); Player activePlayer = getPlayerAtIndex(players, activePlayerIndex); - if (activePlayer == null || activePlayer.getId() == null) { + if (activePlayer == null) { setTaskbarTurnHighlight(false); return; } - if (myPlayerId != null && myPlayerId.equals(activePlayer.getId())) { + if (isCurrentPlayer(activePlayer)) { setPlayerCardsTurnHighlighted(true); setTaskbarTurnHighlight(true); return; @@ -1104,11 +1249,7 @@ public class CasinoGameController { } Player winner = resolveWinner(s); - boolean isLocalWinner = - winner != null - && winner.getId() != null - && myPlayerId != null - && myPlayerId.equals(winner.getId()); + boolean isLocalWinner = winner != null && isCurrentPlayer(winner); if (!isLocalWinner && lastWinnerName != null && !lastWinnerName.isBlank()) { String localName = @@ -1255,11 +1396,11 @@ public class CasinoGameController { } Player dealerPlayer = s.players.get(s.dealer); - if (dealerPlayer == null || dealerPlayer.getId() == null) { + if (dealerPlayer == null) { return; } - if (myPlayerId != null && myPlayerId.equals(dealerPlayer.getId())) { + if (isCurrentPlayer(dealerPlayer)) { myDealerIcon.setVisible(true); return; } @@ -1292,10 +1433,17 @@ public class CasinoGameController { * otherwise. */ private boolean samePlayer(Player a, Player b) { - if (a == null || b == null || a.getId() == null || b.getId() == null) { + if (a == null || b == null) { return false; } - return a.getId().equals(b.getId()); + + String aName = normalizeIdentifier(a.getName()); + String bName = normalizeIdentifier(b.getName()); + if (aName != null && bName != null) { + return aName.equals(bName); + } + + return a.getId() != null && b.getId() != null && a.getId().equals(b.getId()); } /** @@ -1326,8 +1474,8 @@ public class CasinoGameController { view.setImage(image); - view.setOnMouseEntered(e -> animateCardHover(view, true)); - view.setOnMouseExited(e -> animateCardHover(view, false)); + // view.setOnMouseEntered(e -> animateCardHover(view, true)); + // view.setOnMouseExited(e -> animateCardHover(view, false)); view.setOpacity(CARD_START_OPACITY); @@ -1370,8 +1518,8 @@ public class CasinoGameController { view.setImage(image); - view.setOnMouseEntered(e -> animateCardHover(view, true)); - view.setOnMouseExited(e -> animateCardHover(view, false)); + // view.setOnMouseEntered(e -> animateCardHover(view, true)); + // view.setOnMouseExited(e -> animateCardHover(view, false)); view.setOpacity(CARD_START_OPACITY); @@ -1384,6 +1532,47 @@ public class CasinoGameController { } } + /** Apply styling to the myDealerIcon ImageView. */ + private void styleMyDealerIcon() { + if (myDealerIcon == null || playerCardsBox == null) { + return; + } + + myDealerIcon.setPreserveRatio(true); + myDealerIcon.setSmooth(true); + + javafx.scene.Scene scene = playerCardsBox.getScene(); + if (scene != null) { + bindMyDealerIconSize(scene); + } else { + playerCardsBox + .sceneProperty() + .addListener( + (obs, oldScene, newScene) -> { + if (newScene != null) { + bindMyDealerIconSize(newScene); + } + }); + } + } + + /** + * Bind the size of the myDealerIcon ImageView to the dimensions of the scene using predefined + * ratios. + * + * @param scene The JavaFX Scene to which the myDealerIcon belongs, used for binding the size + * properties. + */ + private void bindMyDealerIconSize(javafx.scene.Scene scene) { + if (myDealerIconSizeBound || scene == null || myDealerIcon == null) { + return; + } + + myDealerIcon.fitHeightProperty().bind(scene.heightProperty().multiply(DEALER_HEIGHT_RATIO)); + myDealerIcon.fitWidthProperty().bind(scene.widthProperty().multiply(DEALER_WIDTH_RATIO)); + myDealerIconSizeBound = true; + } + /** * Load an image from the given path safely, falling back to a default backside image if the * specified image cannot be found. @@ -1551,28 +1740,31 @@ public class CasinoGameController { * Animate a card hover effect by scaling and lifting the card when hovered and resetting it * when not hovered. * + *

However, we have removed the hover animations from the cards and the pot for improved + * handling and a more stable Game UI. + * * @param view The ImageView representing the card to animate on hover. * @param hover A boolean indicating whether the card is being hovered (true) or not (false). */ - private void animateCardHover(ImageView view, boolean hover) { - - double scale = hover ? HOVER_SCALE_ON : HOVER_SCALE_OFF; - double lift = hover ? HOVER_LIFT_ON : HOVER_LIFT_OFF; - - javafx.animation.ScaleTransition st = - new javafx.animation.ScaleTransition( - javafx.util.Duration.millis(HOVER_DURATION_MS), view); - st.setToX(scale); - st.setToY(scale); - - javafx.animation.TranslateTransition tt = - new javafx.animation.TranslateTransition( - javafx.util.Duration.millis(HOVER_DURATION_MS), view); - tt.setToY(lift); - - st.play(); - tt.play(); - } + // private void animateCardHover(ImageView view, boolean hover) { + // + // double scale = hover ? HOVER_SCALE_ON : HOVER_SCALE_OFF; + // double lift = hover ? HOVER_LIFT_ON : HOVER_LIFT_OFF; + // + // javafx.animation.ScaleTransition st = + // new javafx.animation.ScaleTransition( + // javafx.util.Duration.millis(HOVER_DURATION_MS), view); + // st.setToX(scale); + // st.setToY(scale); + // + // javafx.animation.TranslateTransition tt = + // new javafx.animation.TranslateTransition( + // javafx.util.Duration.millis(HOVER_DURATION_MS), view); + // tt.setToY(lift); + // + // st.play(); + // tt.play(); + // } /** * Position the player's hole cards box on the screen based on a predefined offset factor @@ -1640,13 +1832,26 @@ public class CasinoGameController { potBox.getChildren().clear(); + int chipsPerRow = resolveChipsPerPotRow(); + int maxVisibleChips = chipsPerRow * MAX_POT_ROWS; + HBox topRow = createPotRow(); + HBox bottomRow = createPotRow(); + VBox rows = new VBox(POT_CHIP_V_GAP, topRow, bottomRow); + rows.setAlignment(Pos.CENTER); + potBox.getChildren().add(rows); + int remaining = pot; int index = 0; + boolean limitReached = false; for (int chipValue : CHIP_VALUES) { while (remaining >= chipValue) { + if (index >= maxVisibleChips) { + limitReached = true; + break; + } ImageView chip = new ImageView(loadImageSafe("/images/chip-" + chipValue + "-5.png")); @@ -1658,18 +1863,67 @@ public class CasinoGameController { styleChip(chip); - potBox.getChildren().add(chip); + if (index < chipsPerRow) { + topRow.getChildren().add(chip); + } else { + bottomRow.getChildren().add(chip); + } animateChipAppear(chip, index); remaining -= chipValue; index++; } + + if (limitReached) { + break; + } } // LOGGER.info("POT RENDERED: " + pot + " -> remaining: " + remaining); } + /** + * Create a new HBox to represent a row of chips in the pot display, with predefined horizontal. + * + * @return A new HBox instance configured for displaying a row of chips in the pot, with + * appropriate spacing and alignment. + */ + private HBox createPotRow() { + HBox row = new HBox(POT_CHIP_H_GAP); + row.setAlignment(Pos.CENTER); + return row; + } + + /** + * Calculate the number of chips that can be displayed per row in the pot based on the available + * width. + * + * @return The calculated number of chips that can fit in a single row of the pot display. + */ + private int resolveChipsPerPotRow() { + double availableWidth = potBox != null ? potBox.getWidth() : 0.0; + if (availableWidth <= 0 && potBox != null && potBox.getScene() != null) { + availableWidth = potBox.getScene().getWidth() * POT_WIDTH_FACTOR; + } + if (availableWidth <= 0) { + availableWidth = FALLBACK_POT_WIDTH; + } + + double estimatedChipWidth = FALLBACK_CHIP_WIDTH; + if (potBox != null && potBox.getScene() != null) { + estimatedChipWidth = + Math.max(FALLBACK_CHIP_WIDTH, potBox.getScene().getWidth() * CHIP_WIDTH_RATIO); + } + + int chipsPerRow = + (int) + Math.floor( + (availableWidth + POT_CHIP_H_GAP) + / (estimatedChipWidth + POT_CHIP_H_GAP)); + return Math.max(1, chipsPerRow); + } + /** * Apply styling to the given ImageView for chips, including CSS classes and size bindings. * @@ -1769,13 +2023,131 @@ public class CasinoGameController { */ public void setMyPlayerId(PlayerId id) { this.myPlayerId = id; + this.myPlayerName = id != null ? normalizeIdentifier(id.value()) : null; TaskbarController controller = resolveTaskbarController(); if (controller != null && gameService != null && myPlayerId != null) { controller.setGameService(gameService, myPlayerId); } + renderLobbyOpponentsIfNeeded(); configureTaskbarLobbyActionAnnouncements(); } + /** + * Sets lobby player names to be shown before the first game-state update is available. + * + * @param names player names currently present in the lobby + */ + public void setLobbyPlayerNames(List names) { + if (names == null || names.isEmpty()) { + this.lobbyPlayerNames = java.util.List.of(); + } else { + this.lobbyPlayerNames = java.util.List.copyOf(names); + } + renderLobbyOpponentsIfNeeded(); + } + + /** + * Render the opponents in the lobby based on the list of lobby player names, but only if the + * game service is not yet initialized or if there is no game state available. + */ + private void renderLobbyOpponentsIfNeeded() { + if (gameService != null && gameService.peekStateOrNull() != null) { + return; + } + + if (lobbyPlayerNames == null || lobbyPlayerNames.isEmpty()) { + return; + } + + java.util.List opponents = new java.util.ArrayList<>(); + for (String rawName : lobbyPlayerNames) { + String normalized = normalizeIdentifier(rawName); + if (normalized == null) { + continue; + } + if (myPlayerName != null && myPlayerName.equals(normalized)) { + continue; + } + if (!opponents.contains(rawName.trim())) { + opponents.add(rawName.trim()); + } + if (opponents.size() >= MAX_OPPONENTS_TO_RENDER) { + break; + } + } + + setLobbyOpponent(player1Controller, opponents, 0); + setLobbyOpponent(player2Controller, opponents, 1); + setLobbyOpponent(player3Controller, opponents, 2); + } + + /** + * Set the lobby opponent information in the given PlayerStatusController slot based on the list + * of opponent names and the specified index. + * + * @param slot The PlayerStatusController instance representing the UI slot for an opponent, + * which will be updated with the opponent's name if available. + * @param opponents The list of opponent names currently in the lobby, which may be null or + * contain fewer entries than the index. + * @param index The index of the opponent in the opponents list to be displayed in the given + * slot, where 0 corresponds to the first opponent, 1 to the second, and so on. + */ + private void setLobbyOpponent(PlayerStatusController slot, List opponents, int index) { + if (slot == null) { + return; + } + + if (opponents == null || index >= opponents.size()) { + slot.setPlayer(null); + safeRefresh(slot); + return; + } + + slot.setPlayer(null); + slot.updatePlayer(opponents.get(index), 0); + slot.setDealer(false); + slot.setTurnHighlighted(false); + } + + /** + * Determine if the given Player object represents the current player based on the player's name + * and ID compared to the stored myPlayerName and myPlayerId values. + * + * @param player The Player object to check, which may be null or contain name and ID + * information. + * @return true if the given Player is identified as the current player based on name or ID + * matching, false otherwise. + */ + private boolean isCurrentPlayer(Player player) { + if (player == null) { + return false; + } + + String candidateName = normalizeIdentifier(player.getName()); + if (myPlayerName != null && candidateName != null && myPlayerName.equals(candidateName)) { + return true; + } + + return myPlayerId != null && player.getId() != null && myPlayerId.equals(player.getId()); + } + + /** + * Normalize a player name or identifier by trimming whitespace and converting to lowercase, + * returning null if the result is empty. + * + * @param value The raw player name or identifier to normalize, which may be null or contain + * leading/trailing whitespace. + * @return The normalized identifier in lowercase without leading/trailing whitespace, or null + * if the input is null or empty after trimming. + */ + private String normalizeIdentifier(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed.toLowerCase(); + } + /** * Configure the TaskbarController to announce lobby actions by sending chat messages when * certain actions occur in the game, such as winning a hand. @@ -1785,6 +2157,9 @@ public class CasinoGameController { if (controller == null) { return; } + if (chatClientService != null && chatLobbyId >= 0) { + controller.setLobbyContext(new LobbyClient(chatClientService), chatLobbyId); + } controller.setLobbyActionAnnouncer(this::sendLobbyActionMessage); } @@ -1833,4 +2208,86 @@ public class CasinoGameController { } return null; } + + /** + * Resolve the NotebookController for displaying tips and game information. + * + * @return The NotebookController instance, which may be the directly injected + * notebookController or the one obtained from the notebookIncludeController. + */ + private NotebookController resolveNotebookController() { + if (notebookController != null) { + return notebookController; + } + if (notebookIncludeController != null) { + notebookController = notebookIncludeController; + return notebookController; + } + return null; + } + + /** + * Resolve the SettingsController for theme switching. + * + * @return The SettingsController instance, which may be the directly injected + * settingsController or the one obtained from the settingsIncludeController. + */ + public SettingsController getSettingsController() { + return settingsIncludeController; + } + + /** + * Applies a UI theme by switching the application's stylesheet. + * + * @param theme The theme name: "standard", "blackwhite", or "glass" + */ + private void applyTheme(String theme) { + if (theme == null || theme.isBlank()) { + LOGGER.warning("Theme is null or empty -> ignoring theme change"); + return; + } + + javafx.scene.Scene scene = casinoTable != null ? casinoTable.getScene() : null; + + if (scene == null) { + LOGGER.warning("Scene not ready yet -> cannot apply theme: " + theme); + return; + } + + String cssPath = + switch (theme.toLowerCase()) { + case "mainuiparquet" -> "/ui-structure/Casinogameui-parquet.css"; + case "blackwhite" -> "/ui-structure/Casinogameui-blackwhite.css"; + case "glass" -> "/ui-structure/Casinogameui-glass.css"; + case "lightglass" -> "/ui-structure/Casinogameui-light-glass.css"; + default -> "/ui-structure/Casinogameui.css"; + }; + + java.net.URL cssUrl = getClass().getResource(cssPath); + + if (cssUrl == null) { + LOGGER.severe("CSS resource not found: " + cssPath); + return; + } + + try { + String cssExternalForm = cssUrl.toExternalForm(); + + scene.getStylesheets() + .removeIf( + s -> + s.contains("Casinogameui.css") + || s.contains("Casinogameui-parquet.css") + || s.contains("Casinogameui-blackwhite.css") + || s.contains("Casinogameui-glass.css") + || s.contains("Casinogameui-light-glass.css")); + + scene.getStylesheets().add(cssExternalForm); + + LOGGER.info("Theme switched successfully to: " + theme); + + } catch (Exception e) { + LOGGER.severe("Failed to apply theme '" + theme + "': " + e.getMessage()); + } + } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameUI.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameUI.java index c82dfe1..d9a7a1f 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameUI.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameUI.java @@ -6,6 +6,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId; import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; import ch.unibas.dmi.dbis.cs108.casono.client.network.GameClient; +import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient; import java.io.IOException; import java.util.UUID; import java.util.logging.Logger; @@ -130,6 +131,13 @@ public class CasinoGameUI extends Application { controller.setMyPlayerId(PlayerId.of(effectiveUsername)); controller.setChatContext(effectiveUsername, clientService, lobbyId); + try { + LobbyClient lobbyClient = new LobbyClient(clientService); + controller.setLobbyPlayerNames(lobbyClient.fetchLobbyPlayerNames(lobbyId)); + } catch (Exception e) { + LOG.fine("Could not preload lobby player names: " + e.getMessage()); + } + controller.startChat(chatController); Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT); @@ -140,6 +148,7 @@ public class CasinoGameUI extends Application { stage.setScene(scene); stage.setFullScreen(true); + stage.setFullScreenExitHint(""); stage.setOnHidden(e -> controller.stop()); stage.show(); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/CasinoBrowserController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/CasinoBrowserController.java index 6a360a3..c915ab2 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/CasinoBrowserController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/CasinoBrowserController.java @@ -68,7 +68,8 @@ public class CasinoBrowserController { private static final CookieManager COOKIE_MANAGER = new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER); - private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class); + private static final Logger LOGGER = + LogManager.getLogger(CasinoBrowserController.class.getSimpleName()); private static final ObservableList URL_SUGGESTIONS = FXCollections.observableArrayList(TRUSTED_DOMAINS); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/HighscoreViewController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/HighscoreViewController.java index e395b68..9045b81 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/HighscoreViewController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/HighscoreViewController.java @@ -14,7 +14,8 @@ import org.apache.logging.log4j.Logger; /** Controller for the highscore popup window. */ public class HighscoreViewController { - private static final Logger LOGGER = LogManager.getLogger(HighscoreViewController.class); + private static final Logger LOGGER = + LogManager.getLogger(HighscoreViewController.class.getSimpleName()); @FXML private ListView highscoreList; @FXML private Label statusLabel; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/NotebookController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/NotebookController.java new file mode 100644 index 0000000..b8da88a --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/NotebookController.java @@ -0,0 +1,269 @@ +package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents; + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Controller for the movable notebook (Tips) display within the Casono Game UI. + * + *

Responsible for: - Drag-and-drop movement of the notebook, - Display of general gaming tips, - + * Toggle visibility of the notebook. + */ +public class NotebookController { + + private static final Logger LOGGER = + LogManager.getLogger(NotebookController.class.getSimpleName()); + + @FXML private VBox notebook; + @FXML private ScrollPane tipsContent; + @FXML private VBox tipsList; + @FXML private Button toggleButton; + + private double xOffset = 0; + private double yOffset = 0; + private static final double NOTEBOOK_SCALE = 0.95; + private static final double SCALE_NORMAL = 1.0; + private boolean isVisible = true; + + /** Standard constructor. Used by FXML. */ + public NotebookController() { + // default constructor for FXML + } + + /** Initialize the notebook with default tips. */ + @FXML + private void initialize() { + if (tipsList != null) { + loadDefaultTips(); + } + } + + /** Load default poker tips into the notebook. */ + private void loadDefaultTips() { + tipsList.getChildren().clear(); + + String[] tips = { + "TEXAS HOLD'EM GRUNDLAGEN\n\nJeder Spieler erhält 2 verdeckte Karten. " + + "Zusätzlich gibt es 5 Gemeinschaftskarten.\n" + + "Ziel: Beste 5-Karten-Hand bilden oder alle Gegner zum Fold bringen.\n" + + "Startstack: 20000 Chips ($)", + "BLINDS & DEALER\n\nSmall Blind: 100 Chips | Big Blind: 200 Chips\n" + + "Die Blinds erzeugen den Startpot und " + + "müssen vor jeder Runde gezahlt werden.\n" + + "Der Dealer-Button bestimmt die Positionen " + + "sowie die Reihenfolge der Aktionen.", + "SPIELABLAUF\n\nPreflop → Flop (3 Karten) → Turn (4. Karte) → River (5. Karte)\n" + + "Preflop beginnt links vom Big Blind.\n" + + "Nach dem Flop beginnt die Action links vom Dealer (aktive Spieler).", + "AKTIONEN\n\nFold = aussteigen\nCall = mitgehen\nRaise = erhöhen\n" + + "Alle Aktionen müssen in korrekter Reihenfolge (Acting in Turn) erfolgen.", + "POSITION IST ENTSCHEIDEND\n\nEarly Position: nur starke Hände spielen\n" + + "Middle Position: etwas breiter spielen\n" + + "Late Position: größter Vorteil durch mehr Informationen", + "SHOWDOWN\n\nWenn nach der letzten Setzrunde mehrere Spieler übrig sind:\n" + + "Beste 5-Karten-Kombination aus Handkarten + Gemeinschaftskarten gewinnt.", + "STRATEGIE & DENKEN\n\nPoker ist Strategie + Psychologie + Mathematik\n" + + "Entscheidungen basieren auf Wahrscheinlichkeiten, " + + "Position und Gegnerverhalten.", + "WICHTIGE REGELN\n\n- Acting in Turn ist Pflicht\n" + + "- Jeder Spieler spielt nur seine eigene Hand (One Player One Hand)\n" + + "- Ungültige Einsätze werden blockiert oder korrigiert\n" + + "- Reihenfolge am Tisch muss immer eingehalten werden", + "HÄUFIGE FEHLER\n\n❌ Zu viele Hände spielen\n" + + "❌ Position ignorieren\n❌ Zu oft callen statt folden\n" + + "❌ Stärke der Gegner unterschätzen\n❌ Tilt (emotional spielen)", + "TOP 10 POKERHÄNDE\n\n" + + "1. 👑 Royal Flush\n" + + "A♠ K♠ Q♠ J♠ 10♠\n\n" + + "2. 🔥 Straight Flush\n" + + "9♥ 8♥ 7♥ 6♥ 5♥\n\n" + + "3. 💥 Four of a Kind (Poker)\n" + + "A♣ A♦ A♥ A♠ K♠\n\n" + + "4. 🏠 Full House\n" + + "K♣ K♦ K♥ 9♠ 9♦\n\n" + + "5. 🌊 Flush\n" + + "A♥ J♥ 8♥ 5♥ 2♥\n\n" + + "6. ➡ Straight\n" + + "10♣ 9♦ 8♠ 7♥ 6♣\n\n" + + "7. 🎯 Three of a Kind (Drilling)\n" + + "Q♣ Q♦ Q♥ 7♠ 2♦\n\n" + + "8. 👥 Two Pair\n" + + "J♣ J♦ 4♠ 4♥ A♣\n\n" + + "9. 👍 One Pair\n" + + "8♣ 8♦ K♠ J♥ 3♣\n\n" + + "10. 🃏 High Card\n" + + "A♠ J♦ 9♣ 5♥ 2♠" + }; + + for (String tip : tips) { + TextFlow tipBox = createTipElement(tip); + tipsList.getChildren().add(tipBox); + } + } + + private static final int LINE_SPACING = 4; + private static final int TIP_WIDTH = 280; + + /** + * Create a styled text element for a single tip. + * + * @param tipText The text of the tip to display. + * @return A TextFlow element containing the formatted tip. + */ + private TextFlow createTipElement(String tipText) { + Text text = new Text(tipText); + text.setStyle("-fx-font-size: 12; -fx-font-family: 'Segoe UI', Arial;"); + TextFlow flow = new TextFlow(text); + flow.setStyle( + "-fx-padding: 10; -fx-background-color: #f5f5f5; " + + "-fx-border-radius: 5; -fx-margin: 5;"); + flow.setLineSpacing(LINE_SPACING); + flow.setPrefWidth(TIP_WIDTH); + return flow; + } + + /** + * Called when the notebook header is pressed with the mouse. Saves the relative position for + * later correct repositioning. + * + * @param event The mouse event + */ + @FXML + private void onNotebookPressed(MouseEvent event) { + if (notebook == null || event == null) { + return; + } + xOffset = event.getSceneX() - notebook.getLayoutX(); + yOffset = event.getSceneY() - notebook.getLayoutY(); + } + + /** + * Called while dragging the notebook with the mouse. Updates the position and slightly scales + * the notebook for visual feedback. + * + * @param event The mouse event + */ + @FXML + private void onNotebookDragged(MouseEvent event) { + if (notebook == null || event == null || notebook.getScene() == null) { + return; + } + + notebook.setScaleX(NOTEBOOK_SCALE); + notebook.setScaleY(NOTEBOOK_SCALE); + + double targetX = event.getSceneX() - xOffset; + double targetY = event.getSceneY() - yOffset; + + double maxX = Math.max(0, notebook.getScene().getWidth() - scaledNodeWidth()); + double maxY = Math.max(0, notebook.getScene().getHeight() - scaledNodeHeight()); + + notebook.setLayoutX(clamp(targetX, 0, maxX)); + notebook.setLayoutY(clamp(targetY, 0, maxY)); + } + + /** + * Called when the mouse cursor is released over the notebook. Resets the notebook scaling to + * normal size. + * + * @param event The mouse event + */ + @FXML + private void onNotebookReleased(MouseEvent event) { + if (notebook == null) { + return; + } + notebook.setScaleX(SCALE_NORMAL); + notebook.setScaleY(SCALE_NORMAL); + + if (notebook.getScene() == null) { + return; + } + + double maxX = Math.max(0, notebook.getScene().getWidth() - scaledNodeWidth()); + double maxY = Math.max(0, notebook.getScene().getHeight() - scaledNodeHeight()); + + notebook.setLayoutX(clamp(notebook.getLayoutX(), 0, maxX)); + notebook.setLayoutY(clamp(notebook.getLayoutY(), 0, maxY)); + } + + /** Close the notebook window from the header minus button. */ + @FXML + private void onToggleNotebook() { + if (notebook == null) { + return; + } + isVisible = false; + notebook.setVisible(false); + notebook.setManaged(false); + } + + /** Shows the tips notebook (makes it visible if currently hidden). */ + public void showTips() { + if (notebook == null) { + return; + } + + if (!isVisible) { + isVisible = true; + notebook.setVisible(true); + notebook.setManaged(true); + } + + if (tipsContent != null) { + tipsContent.setVisible(true); + tipsContent.setManaged(true); + } + + if (toggleButton != null) { + toggleButton.setText("−"); + } + } + + /** + * Calculates the scaled width of the notebook node based on its current bounds and scale + * factor. + * + * @return The scaled width of the notebook node. + */ + private double scaledNodeWidth() { + double width = notebook.getBoundsInLocal().getWidth(); + if (width <= 0) { + width = notebook.prefWidth(-1); + } + return Math.max(0, width * notebook.getScaleX()); + } + + /** + * Calculates the scaled height of the notebook node based on its current bounds and scale + * factor. + * + * @return The scaled height of the notebook node. + */ + private double scaledNodeHeight() { + double height = notebook.getBoundsInLocal().getHeight(); + if (height <= 0) { + height = notebook.prefHeight(-1); + } + return Math.max(0, height * notebook.getScaleY()); + } + + /** + * Clamps a value between a minimum and maximum bound. + * + * @param value the value to clamp + * @param min the lower bound + * @param max the upper bound + * @return the clamped value in the range [min, max] + */ + private double clamp(double value, double min, double max) { + return Math.max(min, Math.min(value, max)); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/PlayerStatusController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/PlayerStatusController.java index 48738c2..0252763 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/PlayerStatusController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/PlayerStatusController.java @@ -20,15 +20,19 @@ public class PlayerStatusController { private static final Logger LOGGER = Logger.getLogger(PlayerStatusController.class.getName()); - @FXML private Label playerName; - @FXML private Label playerMoney; - @FXML private ImageView dealerIcon; - @FXML private Pane parent; - @FXML private VBox playerStatusBox; - @FXML private HBox statusInnerBoxTop; - @FXML private HBox statusInnerBoxBottom; + @FXML Label playerName; + @FXML Label playerMoney; + @FXML ImageView dealerIcon; + @FXML ImageView playerProfileImage; + @FXML Pane parent; + @FXML VBox playerStatusBox; + @FXML HBox statusInnerBoxTop; + @FXML HBox statusInnerBoxBottom; private Image dealerImage; + // Profile images (loaded from resources/images/profile-picture) + private Image profileUserImage; + private Image profileDealerImage; private Player player; private boolean turnHighlighted; private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-5.png"; @@ -53,6 +57,26 @@ public class PlayerStatusController { LOGGER.log(Level.SEVERE, "Error loading dealer image", e); } + try { + var base = "/images/profile-picture/"; + var uUser = getClass().getResource(base + "poker_user_491.png"); + var uDealer = getClass().getResource(base + "poker_dealer_471.png"); + + if (uUser != null) { + profileUserImage = new Image(uUser.toExternalForm(), true); + } + if (uDealer != null) { + profileDealerImage = new Image(uDealer.toExternalForm(), true); + } + + if (playerProfileImage != null && profileUserImage != null) { + playerProfileImage.setImage(profileUserImage); + } + + } catch (Exception e) { + LOGGER.log(Level.FINE, "Could not load profile images", e); + } + dealerIcon.layoutXProperty().bind(parent.widthProperty().multiply(DEALER_ICON_X_FACTOR)); dealerIcon.setVisible(false); } @@ -115,14 +139,27 @@ public class PlayerStatusController { * @return true if both represent the same player. */ public boolean hasPlayer(Player candidate) { - if (player == null - || candidate == null - || player.getId() == null - || candidate.getId() == null) { + if (player == null || candidate == null) { return false; } - return player.getId().equals(candidate.getId()); + String currentName = normalizeIdentifier(player.getName()); + String candidateName = normalizeIdentifier(candidate.getName()); + if (currentName != null && candidateName != null) { + return currentName.equals(candidateName); + } + + return player.getId() != null + && candidate.getId() != null + && player.getId().equals(candidate.getId()); + } + + private String normalizeIdentifier(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed.toLowerCase(); } /** @@ -147,6 +184,14 @@ public class PlayerStatusController { if (isDealer && dealerImage != null) { dealerIcon.setImage(dealerImage); } + + if (playerProfileImage != null) { + if (isDealer && profileDealerImage != null) { + playerProfileImage.setImage(profileDealerImage); + } else if (profileUserImage != null) { + playerProfileImage.setImage(profileUserImage); + } + } } /** diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/SettingsController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/SettingsController.java new file mode 100644 index 0000000..b62d5c3 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/SettingsController.java @@ -0,0 +1,209 @@ +package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents; + +import java.util.function.Consumer; +import javafx.fxml.FXML; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.VBox; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Controller for the settings display within the poker UI. Handles theme selection and drag + * movement. + */ +public class SettingsController { + + private static final Logger LOGGER = + LogManager.getLogger(SettingsController.class.getSimpleName()); + + @FXML private VBox settingsBox; + @FXML private RadioButton themeStandard; + @FXML private RadioButton themeStandardParquet; + @FXML private RadioButton themeBlackWhite; + @FXML private RadioButton themeGlass; + @FXML private RadioButton themeLightGlass; + private boolean visible = true; + + private double xOffset; + private double yOffset; + + private static final double SCALE_DRAG = 0.95; + private static final double SCALE_NORMAL = 1.0; + + private String currentTheme = "standard"; + private Consumer themeChangeListener; + + /** Initialize the settings controller, set up theme toggle group and listeners. */ + @FXML + private void initialize() { + setupThemeToggle(); + } + + /** Set up the theme toggle group and listeners for the theme selection radio buttons. */ + private void setupThemeToggle() { + if (themeStandard == null + || themeStandardParquet == null + || themeBlackWhite == null + || themeGlass == null + || themeLightGlass == null) { + LOGGER.warn("Theme buttons not injected from FXML"); + return; + } + + ToggleGroup group = new ToggleGroup(); + + themeStandard.setToggleGroup(group); + themeStandardParquet.setToggleGroup(group); + themeBlackWhite.setToggleGroup(group); + themeGlass.setToggleGroup(group); + themeLightGlass.setToggleGroup(group); + + themeStandard.setSelected(true); + + group.selectedToggleProperty() + .addListener( + (obs, oldVal, newVal) -> { + if (newVal == themeStandard) { + setTheme("standard"); + } else if (newVal == themeStandardParquet) { + setTheme("mainuiparquet"); + } else if (newVal == themeBlackWhite) { + setTheme("blackwhite"); + } else if (newVal == themeGlass) { + setTheme("glass"); + } else if (newVal == themeLightGlass) { + setTheme("lightglass"); + } + }); + } + + /** + * Set the current theme and notify the listener of the change. + * + * @param theme The new theme to set (e.g., "standard", "blackwhite", "glass"). + */ + private void setTheme(String theme) { + currentTheme = theme; + + if (themeChangeListener != null) { + themeChangeListener.accept(theme); + } + + LOGGER.info("Theme changed to: " + theme); + } + + /** + * Handle the mouse press event on the settings box to prepare for dragging. + * + * @param event The MouseEvent triggered when the user presses the mouse button on the settings + * box. + */ + @FXML + private void onSettingsPressed(MouseEvent event) { + if (settingsBox == null) { + return; + } + + xOffset = event.getSceneX() - settingsBox.getLayoutX(); + yOffset = event.getSceneY() - settingsBox.getLayoutY(); + } + + /** + * Handle the mouse drag event on the settings box to allow dragging it around the scene. + * + * @param event The MouseEvent triggered when the user drags the mouse while pressing on the + * settings box. + */ + @FXML + private void onSettingsDragged(MouseEvent event) { + if (settingsBox == null || settingsBox.getScene() == null) { + return; + } + + settingsBox.setScaleX(SCALE_DRAG); + settingsBox.setScaleY(SCALE_DRAG); + + double x = event.getSceneX() - xOffset; + double y = event.getSceneY() - yOffset; + + double maxX = settingsBox.getScene().getWidth() - settingsBox.getWidth(); + double maxY = settingsBox.getScene().getHeight() - settingsBox.getHeight(); + + settingsBox.setLayoutX(clamp(x, 0, maxX)); + settingsBox.setLayoutY(clamp(y, 0, maxY)); + } + + /** Close the settings box from the header minus button. */ + @FXML + private void onToggleSettings() { + if (settingsBox == null) { + return; + } + + visible = !visible; + + settingsBox.setVisible(visible); + settingsBox.setManaged(visible); + } + + /** + * Handle the mouse release event on the settings box to reset its scale after dragging. + * + * @param event The MouseEvent triggered when the user releases the mouse button after dragging + * the settings box. + */ + @FXML + private void onSettingsReleased(MouseEvent event) { + if (settingsBox == null || settingsBox.getScene() == null) { + return; + } + + settingsBox.setScaleX(SCALE_NORMAL); + settingsBox.setScaleY(SCALE_NORMAL); + } + + /** + * Set a listener to be notified when the theme changes. The listener will receive the new theme + * as a string. + * + * @param listener A Consumer that accepts a String representing the new theme (e.g., + * "standard", "blackwhite", "glass"). + */ + public void setThemeChangeListener(Consumer listener) { + this.themeChangeListener = listener; + } + + /** Show the settings box by setting its visibility and managed properties to true. */ + public void show() { + if (settingsBox == null) { + return; + } + + settingsBox.setVisible(true); + settingsBox.setManaged(true); + } + + /** Hide the settings box by setting its visibility and managed properties to false. */ + public void hide() { + if (settingsBox == null) { + return; + } + + settingsBox.setVisible(false); + settingsBox.setManaged(false); + } + + /** + * Utility method to clamp a value between a minimum and maximum range. + * + * @param value The value to clamp. + * @param min The minimum allowed value. + * @param max The maximum allowed value. + * @return The clamped value, guaranteed to be between min and max. + */ + private double clamp(double value, double min, double max) { + return Math.max(min, Math.min(value, max)); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java index ded5048..aee6ebf 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java @@ -8,6 +8,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState; import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient; import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui; +import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; @@ -39,7 +40,8 @@ import org.apache.logging.log4j.Logger; */ public class TaskbarController { - private static final Logger LOGGER = LogManager.getLogger(TaskbarController.class); + private static final Logger LOGGER = + LogManager.getLogger(TaskbarController.class.getSimpleName()); @FXML private HBox taskbar; @FXML private TextField taskbarInput; @@ -50,16 +52,22 @@ public class TaskbarController { @FXML private Label moneyLabel; private GameService gameService; + private LobbyClient lobbyClient; + private int lobbyId = -1; private PlayerId myPlayerId; + private String myPlayerName; private GameState lastState; private double xOffset = 0; private double yOffset = 0; + private NotebookController notebookController; + private SettingsController settingsController; private static final double TASKBAR_SCALE = 0.95; private static final double PREFLOP_BLOCK_RATIO = 0.50; private static final double FLOP_WARN_RATIO = 0.50; private static final double FLOP_BLOCK_RATIO = 0.80; private static final double TURN_RIVER_WARN_RATIO = 0.80; private static final double TURN_RIVER_BLOCK_RATIO = 1.00; + private static final int SMALLEST_VALUE = 5; private static final int SMALL_BLIND = 100; private static final int BIG_BLIND = 200; private static final String STYLE_YELLOW_BUTTON = "yellow-button"; @@ -80,6 +88,9 @@ public class TaskbarController { private static final int FIRST_PLAYER_MAX_BET = 3000; private static final int MAX_INPUT_LENGTH = 6; private static final double MAX_CHIP_PERCENT = 0.30; + private int lastPotSnapshot = 0; + private int lastObservedIncrease = 0; + private int lastRaiseIncrement = 0; /** Standard constructor. Used by FXML. */ public TaskbarController() { @@ -230,6 +241,23 @@ public class TaskbarController { public void setGameService(GameService gameService, PlayerId myPlayerId) { this.gameService = gameService; this.myPlayerId = myPlayerId; + this.myPlayerName = myPlayerId != null ? normalizeIdentifier(myPlayerId.value()) : null; + } + + /** Sets the lobby context so Exit can notify the server before closing. */ + public void setLobbyContext(LobbyClient lobbyClient, int lobbyId) { + this.lobbyClient = lobbyClient; + this.lobbyId = lobbyId; + } + + /** Sets the NotebookController reference for showing/hiding tips. */ + public void setNotebookController(NotebookController notebookController) { + this.notebookController = notebookController; + } + + /** Sets the SettingsController reference for theme switching. */ + public void setSettingsController(SettingsController settingsController) { + this.settingsController = settingsController; } /** Sets an optional callback that publishes simple action labels to lobby chat. */ @@ -319,7 +347,7 @@ public class TaskbarController { // Integer targetBet = resolveTargetBet(ActionType.RAISE_BUTTON, lastState); // if (targetBet == null) { - // return ""; + // return ""; // } // int totalContribution = Math.max(0, targetBet); @@ -340,6 +368,37 @@ public class TaskbarController { return "RAISE +" + raiseBy + " TO " + raiseamout; } + /** + * Resolves the typed input as the additional amount the player wants to contribute. + * + *

If the player has already invested chips in the current betting round, the typed value is + * interpreted as an additional amount on top of that investment. This lets the user enter the + * amount they still need to put in directly (for example: already invested 200, type 400 -> + * total target 600). + * + * @param state current game state + * @param rawInput text from the input field + * @return total target bet, or null if the input is invalid + */ + private Integer resolveTypedContributionTarget(GameState state, String rawInput) { + Integer input = parseInputTarget(rawInput); + if (input == null) { + return null; + } + + if (state == null || state.players == null) { + return input; + } + + Player me = findCurrentPlayer(state); + if (me == null) { + return input; + } + + int alreadyInvested = Math.max(0, me.getBet()); + return alreadyInvested > 0 ? alreadyInvested + input : input; + } + /** * Updates the taskbar based on the current game state and the player's status. * @@ -352,24 +411,44 @@ public class TaskbarController { * and whether they are out of the game. */ public void update(GameState state, PlayerId myPlayerId) { + this.myPlayerId = myPlayerId; + this.myPlayerName = myPlayerId != null ? normalizeIdentifier(myPlayerId.value()) : null; + update(state); + } - if (state == null || state.players == null || myPlayerId == null) { + /** + * Updates the taskbar based on the current game state and the player's name. + * + * @param state current game state + * @param myPlayerName local player name + */ + public void update(GameState state, String myPlayerName) { + this.myPlayerName = normalizeIdentifier(myPlayerName); + update(state); + } + + /** + * Updates the taskbar based on the current game state. + * + * @param state current game state + */ + private void update(GameState state) { + + if (state == null || state.players == null) { LOGGER.warn("Cannot update taskbar: invalid input"); return; } this.lastState = state; + Player me = findCurrentPlayer(state); + + synchronizeCurrentBetIfNeeded(state, me); + if (state.currentBet > 0) { lastReferenceBet = state.currentBet; } - Player me = - state.players.stream() - .filter(p -> myPlayerId.equals(p.getId())) - .findFirst() - .orElse(null); - if (me == null) { LOGGER.error("Player not found in GameState!"); return; @@ -393,6 +472,52 @@ public class TaskbarController { refreshBetInputUi(); } + /** + * Synchronizes the current bet in the game state if there has been a change in the pot size + * that is not yet reflected in the current bet. + * + * @param state The current GameState object representing the state of the game. + * @param me The Player object representing the current player, used to determine their bet + * status and whether they are the first to act in the current phase. + */ + private void synchronizeCurrentBetIfNeeded(GameState state, Player me) { + + if (state == null || me == null) { + return; + } + + int currentPot = Math.max(0, state.pot); + int potDiff = currentPot - lastPotSnapshot; + + lastPotSnapshot = currentPot; + + if (potDiff <= 0) { + return; + } + + LOGGER.debug("Pot changed: +" + potDiff); + + lastRaiseIncrement = Math.max(lastRaiseIncrement, potDiff); + + boolean isTurnOrRiver = isTurnOrRiver(state.phase); + + boolean isSpecialActor = isFirstPlayerOfPhase(state, me) && isTurnOrRiver; + + if (isSpecialActor) { + + LOGGER.info("SPECIAL TURN/RIVER LOGIC ACTIVE, potDiff=" + potDiff); + + if (potDiff > state.currentBet) { + + LOGGER.info("Updating currentBet -> potDiff: " + potDiff); + + state.currentBet = potDiff; + + lastReferenceBet = potDiff; + } + } + } + /** * Applies the availability of actions (Bet, Call, Raise, Fold) based on the current game state. * @@ -412,7 +537,7 @@ public class TaskbarController { boolean phaseChanged) { // boolean firstPlayerNewRound = - // phaseChanged && isFirstPreflopPlayer(state, me); + // phaseChanged && isFirstPreflopPlayer(state, me); inputActionAllowed = false; if (!isMyTurn || isOutOrFinished || state == null || me == null) { setBetButtonVisible(false); @@ -443,7 +568,7 @@ public class TaskbarController { return; } - if (isFirstPlayerOfPhase(state, me)) { + if (isFirstFlopPlayer(state, me)) { setActionEnabled(betButton, true); setActionEnabled(callButton, false); // setActionEnabled(foldButton, false); @@ -629,6 +754,53 @@ public class TaskbarController { return isInitialPreflopBlindLayout(state); } + /** + * Checks if the current player is the first to act on the flop. + * + * @param state The current GameState containing phase, dealer and active player information. + * @param me The current player. + * @return true if the player is the first player to act on the flop. + */ + private boolean isFirstFlopPlayer(GameState state, Player me) { + + if (state == null || me == null || state.players == null) { + return false; + } + + if (!isFlop(state.phase)) { + return false; + } + + int size = state.players.size(); + + if (size < 2) { + return false; + } + + int myIndex = state.players.indexOf(me); + + if (myIndex < 0) { + return false; + } + + int firstIndex = (state.dealer + 1) % size; + + for (int i = 0; i < size; i++) { + + int candidate = (firstIndex + i) % size; + + Player p = state.players.get(candidate); + + if (p != null && p.getState() != PlayerState.FOLDED && p.getChips() > 0) { + + firstIndex = candidate; + break; + } + } + + return state.activePlayer == firstIndex && myIndex == firstIndex; + } + /** * Checks if the current player is the first to act in the current phase of the game, based on * the dealer position and the active player index. @@ -876,7 +1048,8 @@ public class TaskbarController { return input != null ? input : callTarget + BIG_BLIND; } - return parseInputTarget(taskbarInput.getText()); + return resolveTypedContributionTarget( + state, taskbarInput != null ? taskbarInput.getText() : null); } /** @@ -1011,7 +1184,7 @@ public class TaskbarController { try { int value = Integer.parseInt(text.trim()); - if (value % SMALL_BLIND != 0) { + if (value % SMALLEST_VALUE != 0) { return null; } @@ -1033,7 +1206,7 @@ public class TaskbarController { * current game state. */ private ValidationResult validateTypedAmount(GameState state, String text) { - Integer target = parseInputTarget(text); + Integer target = resolveTypedContributionTarget(state, text); if (target == null || state == null) { return ValidationResult.blocked("Invalid input"); } @@ -1374,18 +1547,21 @@ public class TaskbarController { */ @FXML private void onInputSubmittedAction() { + SoundManager.getInstance().playButtonClick(); processBet(); } /** Called when the Call button is clicked. */ @FXML private void onInputPlayerCall() { + SoundManager.getInstance().playButtonClick(); submitAction(ActionType.CALL_BUTTON); } /** Called when the Fold button is clicked. */ @FXML private void onInputPlayerFold() { + SoundManager.getInstance().playButtonClick(); if (gameService == null) { LOGGER.error("GameService not initialized"); @@ -1409,6 +1585,7 @@ public class TaskbarController { /** Called when the Raise button is clicked. */ @FXML private void onInputPlayerRaise() { + SoundManager.getInstance().playButtonClick(); submitPresetInputAndProcess("raise"); } @@ -1450,8 +1627,19 @@ public class TaskbarController { */ @FXML private void onExitButtonClick() { + SoundManager.getInstance().playButtonClick(); javafx.application.Platform.runLater( () -> { + if (lobbyClient != null && lobbyId > 0) { + try { + lobbyClient.leaveLobby(lobbyId); + } catch (RuntimeException e) { + LOGGER.warn( + "Could not notify server about lobby leave: {}", + e.getMessage()); + } + } + // Close game stage javafx.stage.Stage currentStage = (javafx.stage.Stage) taskbar.getScene().getWindow(); @@ -1491,15 +1679,56 @@ public class TaskbarController { submitAction(ActionType.INPUT); } + /** + * Finds the current player in the given game state based on the player's name or ID. + * + * @param state The current GameState object representing the state of the game, which includes + * a list of players and their details. + * @return The Player object representing the current player if found in the game state. + */ private Player findCurrentPlayer(GameState state) { - if (state == null || state.players == null || myPlayerId == null) { + if (state == null || state.players == null) { return null; } - return state.players.stream() - .filter(player -> player != null && myPlayerId.equals(player.getId())) - .findFirst() - .orElse(null); + return state.players.stream().filter(this::isCurrentPlayer).findFirst().orElse(null); + } + + /** + * Checks if the given player matches the current player's identity based on name or ID. + * + * @param player The Player object to check against the current player's identity, which + * includes the player's name and ID. + * @return A boolean value indicating whether the given player matches the current player's + * identity. + */ + private boolean isCurrentPlayer(Player player) { + if (player == null) { + return false; + } + + String playerName = normalizeIdentifier(player.getName()); + if (myPlayerName != null && playerName != null && myPlayerName.equals(playerName)) { + return true; + } + + return myPlayerId != null && myPlayerId.equals(player.getId()); + } + + /** + * Normalizes a player identifier (name or ID) by trimming whitespace and converting to + * lowercase. + * + * @param value The string value representing a player identifier, such as a name or ID. + * @return A normalized version of the player identifier, where leading and trailing whitespace + * is removed. + */ + private String normalizeIdentifier(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed.toLowerCase(); } /** @@ -1534,6 +1763,7 @@ public class TaskbarController { */ @FXML private void onBrowserButtonClick() { + SoundManager.getInstance().playButtonClick(); try { Path path = Paths.get( @@ -1550,9 +1780,50 @@ public class TaskbarController { } } + /** Opens the integrated Casono Web Browser. */ + @FXML + private void onBrowserButtonClickWiki() { + try { + + CasinoBrowserController.open("https://www.wikipedia.org/"); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** Opens the integrated Casono Web Browser. */ + @FXML + private void onBrowserButtonClickBrave() { + try { + + CasinoBrowserController.open("https://search.brave.com"); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** Opens the tips notebook if it's currently closed. */ + @FXML + private void onShowTipsButtonClick() { + if (notebookController != null) { + notebookController.showTips(); + } + } + + /** Opens the settings box for theme selection. */ + @FXML + private void onSettingsButtonClick() { + if (settingsController != null) { + settingsController.show(); + } + } + /** Opens the highscore popup window from the taskbar. */ @FXML private void onHighscoreButtonClick() { + SoundManager.getInstance().playButtonClick(); try { var shared = ClientApp.getSharedClientService(); if (shared == null || shared.isOffline()) { diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java index 67415c5..36baee3 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java @@ -1,6 +1,7 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui; /** Main UI application for Casono. Loads the main FXML layout and sets up the stage. */ +import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager; import java.io.IOException; import javafx.application.Application; import javafx.fxml.FXMLLoader; @@ -30,17 +31,14 @@ public class Casinomainui extends Application { * @throws IOException If loading the FXML fails. */ public void start(Stage stage) throws IOException { + // Pre-load sounds to avoid delays on first play + SoundManager.getInstance().preloadSounds(); + // If the launcher passed an address argument (ip:port), expose it as // system properties so controllers can read it without embedding defaults. - var raw = getParameters().getRaw(); - if (raw != null && raw.size() > 0) { - String arg = raw.get(0); - String[] parts = arg.split(":", 2); - if (parts.length == 2) { - System.setProperty("casono.server.host", parts[0]); - System.setProperty("casono.server.port", parts[1]); - } - } + var params = getParameters(); + processServerParameters(params); + FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/ui-structure/Casinomainui.fxml")); Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT); @@ -51,9 +49,28 @@ public class Casinomainui extends Application { stage.getIcons().add(icon); stage.setScene(scene); stage.setFullScreen(true); + stage.setFullScreenExitHint(""); stage.show(); } + private void processServerParameters(Application.Parameters params) { + if (params == null) { + return; + } + + var raw = params.getRaw(); + if (raw == null || raw.isEmpty()) { + return; + } + + String arg = raw.get(0); + String[] parts = arg.split(":", 2); + if (parts.length == 2) { + System.setProperty("casono.server.host", parts[0]); + System.setProperty("casono.server.port", parts[1]); + } + } + /** * Main entry point for launching the application. * diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java index 518b8cf..c51722a 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java @@ -5,6 +5,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController; import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient; import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.HighscoreViewController; +import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager; import java.io.IOException; import java.net.URL; import javafx.application.Platform; @@ -30,7 +31,8 @@ import org.apache.logging.log4j.Logger; /** Controller for the Casono main UI lobby. Handles UI initialization and user actions. */ public class CasinomainuiController { - private static final Logger LOGGER = LogManager.getLogger(CasinomainuiController.class); + private static final Logger LOGGER = + LogManager.getLogger(CasinomainuiController.class.getSimpleName()); @FXML private AnchorPane rootPane; @FXML private Label titleLabel; @@ -169,6 +171,7 @@ public class CasinomainuiController { /** Handles the login button action. Validates input and calls LobbyClient.login(). */ @FXML public void handleLoginButton() { + SoundManager.getInstance().playButtonClick(); String username = usernameField == null ? null : usernameField.getText(); if (username == null || username.isBlank()) { showAlert("Please enter a username."); @@ -258,6 +261,7 @@ public class CasinomainuiController { /** Handles the exit button action to close the application. */ @FXML public void handleexitbutton() { + SoundManager.getInstance().playButtonClick(); if (chatController != null) { chatController.shutdown(); } @@ -271,6 +275,7 @@ public class CasinomainuiController { */ @FXML public void handleCreateLobbyButton() { + SoundManager.getInstance().playButtonClick(); if (translationManager.isFull()) { LOGGER.warn("Grid is full! No more lobbies available."); return; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java index c2c5163..b572975 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java @@ -6,6 +6,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient; import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; +import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -34,7 +35,8 @@ public class LobbyButtonGridManager { private static final int MAX_BUTTONS = 8; private static final int GUEST_ID_LENGTH = 8; - private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class); + private static final Logger LOGGER = + LogManager.getLogger(LobbyButtonGridManager.class.getSimpleName()); private static final String BUTTON_FALLBACK_IMAGE = "/images/lobbypictures/error.png"; @@ -363,6 +365,7 @@ public class LobbyButtonGridManager { btn.setOnAction( e -> { + SoundManager.getInstance().playButtonClick(); Integer targetLobbyId = translationManager.getLobbyIdForButton(buttonId); if (targetLobbyId != null) { diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java index a9a46c7..7855eee 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java @@ -48,6 +48,9 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_statu import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyHandler; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyParser; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyRequest; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_lobby.LeaveLobbyHandler; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_lobby.LeaveLobbyParser; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_lobby.LeaveLobbyRequest; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameHandler; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameParser; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameRequest; @@ -99,7 +102,7 @@ public class ServerApp { public static void start(String arg) { int port = Integer.parseInt(arg); - Logger logger = LogManager.getLogger(ServerApp.class); + Logger logger = LogManager.getLogger(ServerApp.class.getSimpleName()); logger.info("Starting server at port {}", port); EventBus eventBus = new EventBus(); @@ -297,6 +300,14 @@ public class ServerApp { new JoinLobbyHandler( responseDispatcher, context.lobbyManager(), userRegistry)); + // LEAVE_LOBBY registration + parserDispatcher.register("LEAVE_LOBBY", new LeaveLobbyParser()); + commandRouter.register( + LeaveLobbyRequest.class, + (CommandHandler) + new LeaveLobbyHandler( + responseDispatcher, context.lobbyManager(), userRegistry)); + // START_GAME registration parserDispatcher.register("START_GAME", new StartGameParser()); commandRouter.register( diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/change_username/ChangeUsernameHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/change_username/ChangeUsernameHandler.java index 97398af..86057b0 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/change_username/ChangeUsernameHandler.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/change_username/ChangeUsernameHandler.java @@ -1,5 +1,6 @@ package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GamePhase; import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager; import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User; import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry; @@ -42,33 +43,31 @@ public class ChangeUsernameHandler extends CommandHandler public void execute(ChangeUsernameRequest request) { Optional user = userRegistry.getBySessionId(request.getSessionId()); if (user.isEmpty()) { - responseDispatcher.dispatch( - new ErrorResponse( - request.getContext(), - "USER_NOT_LOGGED_IN", - "This session is not associated with an active user.")); + dispatchError( + request.getContext(), + "USER_NOT_LOGGED_IN", + "This session is not associated with an active user."); return; } - String newUsername = request.getUsername() == null ? "" : request.getUsername().trim(); - if (newUsername.isEmpty() || !VALID_USERNAME.matcher(newUsername).matches()) { - responseDispatcher.dispatch( - new ErrorResponse( - request.getContext(), - "INVALID_USERNAME", - "Only letters, numbers, '_' and '-' are allowed.")); + String newUsername = validateUsername(request); + if (newUsername == null) { return; } User currentUser = user.get(); String oldUsername = currentUser.getName(); + + if (isUsernameChangeBlocked(request.getContext(), oldUsername)) { + return; + } + boolean changed = userRegistry.changeUsername(currentUser.getId(), newUsername); if (!changed) { - responseDispatcher.dispatch( - new ErrorResponse( - request.getContext(), - "USERNAME_TAKEN", - "The requested username is already taken.")); + dispatchError( + request.getContext(), + "USERNAME_TAKEN", + "The requested username is already taken."); return; } @@ -76,11 +75,10 @@ public class ChangeUsernameHandler extends CommandHandler lobbyManager == null || lobbyManager.renamePlayer(oldUsername, newUsername); if (!lobbySynced) { userRegistry.changeUsername(currentUser.getId(), oldUsername); - responseDispatcher.dispatch( - new ErrorResponse( - request.getContext(), - "RENAME_CONFLICT", - "Could not update username in current lobby/game state.")); + dispatchError( + request.getContext(), + "RENAME_CONFLICT", + "Could not update username in current lobby/game state."); return; } @@ -109,4 +107,41 @@ public class ChangeUsernameHandler extends CommandHandler responseDispatcher.dispatch(ev); } } + + private String validateUsername(ChangeUsernameRequest request) { + String newUsername = request.getUsername() == null ? "" : request.getUsername().trim(); + if (newUsername.isEmpty() || !VALID_USERNAME.matcher(newUsername).matches()) { + dispatchError( + request.getContext(), + "INVALID_USERNAME", + "Only letters, numbers, '_' and '-' are allowed."); + return null; + } + return newUsername; + } + + private boolean isUsernameChangeBlocked(RequestContext context, String oldUsername) { + if (lobbyManager == null) { + return false; + } + + var lobby = lobbyManager.getLobbyByUsername(oldUsername); + boolean gameRunning = + lobby != null + && lobby.getGameController() != null + && lobby.getGameController().getState().getPhase() != GamePhase.FINISHED; + if (gameRunning && lobby.isPlayerActive(oldUsername)) { + dispatchError( + context, + "CANNOT_CHANGE_USERNAME_DURING_LOBBY", + "Cannot change username while actively playing in a lobby. " + + "Wait until the game ends."); + return true; + } + return false; + } + + private void dispatchError(RequestContext context, String code, String message) { + responseDispatcher.dispatch(new ErrorResponse(context, code, message)); + } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateResponse.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateResponse.java index f55fd36..7b6e33d 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateResponse.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateResponse.java @@ -6,6 +6,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Rank; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Suit; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.showdown.CardsSpeakRule; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GamePhase; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState; import ch.unibas.dmi.dbis.cs108.casono.server.domain.highscore.HighscoreService; @@ -13,8 +14,11 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestCo import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder; +import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; +import java.util.Set; /** * Response carrying a snapshot of the current game state using the project's wire format. @@ -51,10 +55,16 @@ public class GetGameStateResponse extends SuccessResponse { builder.param("CURRENT_BET", computeGlobalCurrentBet(state)); builder.param("DEALER", state.getDealerIndex()); builder.param("ACTIVE_PLAYER", state.getCurrentPlayerIndex()); + int winnerIndex = computeWinnerIndex(state, game); builder.param("WINNER", winnerIndex); - appendWinnerToHighscoresIfFinished(state, winnerIndex); + List winnerNames = computeWinnerNames(state, game); + for (String name : winnerNames) { + builder.param("WINNER_NAME", name); + } + + appendWinnerToHighscoresIfFinished(state, winnerNames); appendHighscoreEntries(builder); appendCommunityCards(builder, state); @@ -64,8 +74,14 @@ public class GetGameStateResponse extends SuccessResponse { return builder.build(); } - private static void appendWinnerToHighscoresIfFinished(GameState state, int winnerIndex) { - if (winnerIndex < 0 || state.getPhase() != GamePhase.FINISHED) { + private static void appendWinnerToHighscoresIfFinished( + GameState state, List winnerNames) { + if (winnerNames == null || winnerNames.isEmpty()) { + return; + } + + GamePhase phase = state.getPhase(); + if (phase != GamePhase.SHOWDOWN && phase != GamePhase.FINISHED) { return; } @@ -73,12 +89,22 @@ public class GetGameStateResponse extends SuccessResponse { return; } - Player winner = findPlayerByIndex(state, winnerIndex); - if (winner == null || winner.getId() == null || winner.getId().value() == null) { - return; + // Keep ordering stable and persist tied winners as a single shared highscore + // entry. Compute the per-winner share here (pot is split equally among + // winners) + int potPerWinner = computePotPerWinner(state, winnerNames.size()); + Set uniqueWinners = new LinkedHashSet<>(winnerNames); + List formattedWinners = new ArrayList<>(); + for (String name : uniqueWinners) { + if (name != null && !name.isBlank()) { + formattedWinners.add(name + " (" + Math.max(0, potPerWinner) + ")"); + } } - HighscoreService.getInstance().appendWinner(winner.getId().value()); + if (!formattedWinners.isEmpty()) { + HighscoreService.getInstance() + .appendFormattedEntry(String.join(", ", formattedWinners)); + } } private static Player findPlayerByIndex(GameState state, int winnerIndex) { @@ -120,6 +146,33 @@ public class GetGameStateResponse extends SuccessResponse { return -1; } + /** Computes the list of all winner names at showdown/finished phase. */ + private static List computeWinnerNames(GameState state, GameController game) { + GamePhase phase = state.getPhase(); + if (phase != GamePhase.SHOWDOWN && phase != GamePhase.FINISHED) { + return new ArrayList<>(); + } + + CardsSpeakRule showdown = new CardsSpeakRule(); + List winners = showdown.determineWinners(state); + + List winnerNames = new ArrayList<>(); + for (Player winner : winners) { + if (winner != null && winner.getId() != null && winner.getId().value() != null) { + winnerNames.add(winner.getId().value()); + } + } + return winnerNames; + } + + /** Computes the pot share per winner. */ + private static int computePotPerWinner(GameState state, int numWinners) { + if (numWinners <= 0 || state.getPot() == null) { + return 0; + } + return state.getPot().getAmount() / numWinners; + } + private static int computeGlobalCurrentBet(GameState state) { int globalCurrentBet = 0; for (Player p : state.getPlayers()) { diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/join_lobby/JoinLobbyHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/join_lobby/JoinLobbyHandler.java index 169b027..e557c3c 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/join_lobby/JoinLobbyHandler.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/join_lobby/JoinLobbyHandler.java @@ -23,7 +23,8 @@ import org.apache.logging.log4j.Logger; public class JoinLobbyHandler extends CommandHandler { private final LobbyManager lobbyManager; private final UserRegistry userRegistry; - private static final Logger LOGGER = LogManager.getLogger(JoinLobbyHandler.class); + private static final Logger LOGGER = + LogManager.getLogger(JoinLobbyHandler.class.getSimpleName()); /** * Create a new {@link JoinLobbyHandler}. diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyHandler.java new file mode 100644 index 0000000..2a4b16f --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyHandler.java @@ -0,0 +1,116 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_lobby; + +import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry; +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Handler for the `LEAVE_LOBBY` command. + * + *

Resolves the username from the session (requires {@link UserLoggedInCheck}), looks up the + * target lobby and marks the user as absent. The user can rejoin later without losing their slot. + * On success an `+OK` response is dispatched; on failure an appropriate {@link ErrorResponse} is + * returned. + */ +public class LeaveLobbyHandler extends CommandHandler { + private final LobbyManager lobbyManager; + private final UserRegistry userRegistry; + private static final Logger LOGGER = + LogManager.getLogger(LeaveLobbyHandler.class.getSimpleName()); + + /** + * Create a new {@link LeaveLobbyHandler}. + * + * @param responseDispatcher dispatcher used to send responses back to the client + * @param lobbyManager manager providing lobby state and operations + * @param userRegistry registry to resolve session -> user mappings + */ + public LeaveLobbyHandler( + ResponseDispatcher responseDispatcher, + LobbyManager lobbyManager, + UserRegistry userRegistry) { + super(responseDispatcher); + this.lobbyManager = lobbyManager; + this.userRegistry = userRegistry; + addCheck(new UserLoggedInCheck(userRegistry)); + } + + /** + * Execute the leave-lobby request. Marks the player as absent in the lobby. + * + * @param request the parsed {@link LeaveLobbyRequest} + */ + @Override + public void execute(LeaveLobbyRequest request) { + LOGGER.info( + "LEAVE_LOBBY request: session={}, lobbyId={}", + request.getContext().sessionId(), + request.getId()); + LobbyId lid = LobbyId.of(request.getId()); + var lobby = lobbyManager.getLobby(lid); + if (lobby == null) { + LOGGER.warn( + "LEAVE_LOBBY: Lobby {} not found (session={})", + request.getId(), + request.getContext().sessionId()); + responseDispatcher.dispatch( + new ErrorResponse(request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found")); + return; + } + + // Only allow leaving if game is running + if (lobby.getGameController() == null) { + LOGGER.warn( + "LEAVE_LOBBY: Game not running in lobby {} (session={})", + request.getId(), + request.getContext().sessionId()); + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), + "GAME_NOT_RUNNING", + "Can only leave a lobby while a game is running")); + return; + } + + var maybeUser = userRegistry.getBySessionId(request.getContext().sessionId()); + if (maybeUser.isEmpty()) { + LOGGER.warn( + "LEAVE_LOBBY: No user associated with session {}", + request.getContext().sessionId()); + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), + "USER_NOT_LOGGED_IN", + "No user associated with session")); + return; + } + + User user = maybeUser.get(); + String username = user.getName(); + + boolean ok = lobbyManager.leavePlayerFromLobby(username, lid); + if (!ok) { + LOGGER.warn( + "LEAVE_LOBBY: User '{}' failed to leave lobby {} (not in lobby or not active)", + username, + request.getId()); + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), + "NOT_IN_LOBBY", + "You are not in this lobby or not actively playing")); + return; + } + + LOGGER.info("User '{}' left lobby {} (marked absent)", username, request.getId()); + responseDispatcher.dispatch(new OkResponse(request.getContext())); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyParser.java new file mode 100644 index 0000000..fbbaa93 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyParser.java @@ -0,0 +1,33 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_lobby; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor; + +/** + * Parser for the `LEAVE_LOBBY` command. + * + *

Expected request parameters: + * + *

    + *
  • `ID` (int) — numeric lobby identifier (required) + *
+ * + *

The parser builds a {@link LeaveLobbyRequest} containing the parsed lobby id and the original + * request context. + */ +public class LeaveLobbyParser implements CommandParser { + /** + * Parse the incoming primitive request into a {@link LeaveLobbyRequest}. + * + * @param primitiveRequest the raw primitive request + * @return a {@link LeaveLobbyRequest} with the parsed lobby id and context + */ + @Override + public LeaveLobbyRequest parse(PrimitiveRequest primitiveRequest) { + RequestParameterAccessor accessor = + new RequestParameterAccessor(primitiveRequest.parameters()); + int id = accessor.require("ID", Integer::parseInt); + return new LeaveLobbyRequest(primitiveRequest.context(), id); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyRequest.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyRequest.java new file mode 100644 index 0000000..5faea90 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyRequest.java @@ -0,0 +1,34 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_lobby; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; + +/** + * Request data for the `LEAVE_LOBBY` command. + * + *

Contains the lobby id the client wants to leave and inherits the {@link Request} contextual + * information. + */ +public class LeaveLobbyRequest extends Request { + private final int id; + + /** + * Create a new {@link LeaveLobbyRequest}. + * + * @param context the request context + * @param id numeric lobby id to leave + */ + public LeaveLobbyRequest(RequestContext context, int id) { + super(context); + this.id = id; + } + + /** + * Returns the lobby id requested by the client. + * + * @return numeric lobby id + */ + public int getId() { + return id; + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java index a1c2017..73d092c 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java @@ -30,6 +30,8 @@ public class GameController { private int dealerIndex = 0; + private volatile Runnable onGameEndedCallback; + private static final int DEALER_OFFSET = 1; private static final int SMALL_BLIND_OFFSET = 1; private static final int BIG_BLIND_OFFSET = 2; @@ -45,6 +47,11 @@ public class GameController { this.engine = engine; } + /** Set a callback to be invoked when the game ends. */ + public void setOnGameEndedCallback(Runnable callback) { + this.onGameEndedCallback = callback; + } + /** * Adds a player to the game with the specified name and initial chip count. * @@ -304,5 +311,12 @@ public class GameController { */ public void endGame() { engine.getState().setPhase(GamePhase.FINISHED); + if (onGameEndedCallback != null) { + try { + onGameEndedCallback.run(); + } catch (RuntimeException e) { + // Log or handle callback errors + } + } } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/evaluator/HandEvaluator.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/evaluator/HandEvaluator.java index daf293e..62e6f98 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/evaluator/HandEvaluator.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/evaluator/HandEvaluator.java @@ -2,6 +2,7 @@ package ch.unibas.dmi.dbis.cs108.casono.server.domain.game.evaluator; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Card; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Suit; +import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; import java.util.List; @@ -40,6 +41,9 @@ public class HandEvaluator { private static final int START_INDEX = 1; private static final int CARD_DIFFERENCE = 1; private static final int PREVIOUS_INDEX_OFFSET = -1; + private static final int TWO_KICKERS = 2; + private static final int ONE_KICKER = 1; + private static final int THREE_KICKERS = 3; /** * Evaluates a list of cards and determines the best possible hand rank. @@ -62,7 +66,7 @@ public class HandEvaluator { return straightFlush; } - HandRank fourKind = checkFourOfAKind(rankCount); + HandRank fourKind = checkFourOfAKind(rankCount, ranks); if (fourKind != null) { return fourKind; } @@ -80,17 +84,17 @@ public class HandEvaluator { return new HandRank(HandRank.Type.STRAIGHT, ranks); } - HandRank threeKind = checkThreeOfAKind(rankCount); + HandRank threeKind = checkThreeOfAKind(rankCount, ranks); if (threeKind != null) { return threeKind; } - HandRank twoPair = checkTwoPair(rankCount); + HandRank twoPair = checkTwoPair(rankCount, ranks); if (twoPair != null) { return twoPair; } - HandRank onePair = checkOnePair(rankCount); + HandRank onePair = checkOnePair(rankCount, ranks); if (onePair != null) { return onePair; } @@ -200,20 +204,26 @@ public class HandEvaluator { } /** - * Helper method to check for a four of a kind hand rank. + * Helper method to check for a four of a kind hand rank. Stores the quad rank and 1 kicker for + * tie-breaking. * * @param rankCount A map where the key is the card rank and the value is the count of * occurrences. + * @param allRanks A sorted list of all card ranks in the hand. * @return A HandRank object representing the four of a kind hand rank, or null if not found. */ - private static HandRank checkFourOfAKind(Map rankCount) { + private static HandRank checkFourOfAKind(Map rankCount, List allRanks) { if (!rankCount.containsValue(FOUR_OF_A_KIND_COUNT)) { return null; } int quad = getRank(rankCount, FOUR_OF_A_KIND); + List kickers = getKickers(allRanks, List.of(quad), ONE_KICKER); - return new HandRank(HandRank.Type.FOUR_OF_A_KIND, List.of(quad)); + List result = new ArrayList<>(List.of(quad)); + result.addAll(kickers); + + return new HandRank(HandRank.Type.FOUR_OF_A_KIND, result); } /** @@ -255,30 +265,39 @@ public class HandEvaluator { } /** - * Helper method to check for a three of a kind hand rank. + * Helper method to check for a three of a kind hand rank. Stores the trips rank and 2 kickers + * for tie-breaking. * * @param rankCount A map where the key is the card rank and the value is the count of * occurrences. + * @param allRanks A sorted list of all card ranks in the hand. * @return A HandRank object representing the three of a kind hand rank, or null if not found. */ - private static HandRank checkThreeOfAKind(Map rankCount) { + private static HandRank checkThreeOfAKind( + Map rankCount, List allRanks) { if (!rankCount.containsValue(THREE_OF_A_KIND_COUNT)) { return null; } int tripsRank = getRank(rankCount, THREE_OF_A_KIND_RANK); + List kickers = getKickers(allRanks, List.of(tripsRank), TWO_KICKERS); - return new HandRank(HandRank.Type.THREE_OF_A_KIND, List.of(tripsRank)); + List result = new ArrayList<>(List.of(tripsRank)); + result.addAll(kickers); + + return new HandRank(HandRank.Type.THREE_OF_A_KIND, result); } /** - * Helper method to check for a two pair hand rank. + * Helper method to check for a two pair hand rank. Stores the two pair ranks and 1 kicker for + * tie-breaking. The kicker is the highest remaining card that is not part of either pair. * * @param rankCount A map where the key is the card rank and the value is the count of * occurrences. + * @param allRanks A sorted list of all card ranks in the hand. * @return A HandRank object representing the two pair hand rank, or null if not found. */ - private static HandRank checkTwoPair(Map rankCount) { + private static HandRank checkTwoPair(Map rankCount, List allRanks) { long pairCount = rankCount.values().stream().filter(v -> v == PAIR).count(); @@ -294,17 +313,24 @@ public class HandEvaluator { .limit(PAIR) .toList(); - return new HandRank(HandRank.Type.TWO_PAIR, pairRanks); + List kickers = getKickers(allRanks, pairRanks, ONE_KICKER); + + List result = new ArrayList<>(pairRanks); + result.addAll(kickers); + + return new HandRank(HandRank.Type.TWO_PAIR, result); } /** - * Helper method to check for a one pair hand rank. + * Helper method to check for a one pair hand rank. Stores the pair rank and 3 kickers for + * tie-breaking. * * @param rankCount A map where the key is the card rank and the value is the count of * occurrences. + * @param allRanks A sorted list of all card ranks in the hand. * @return A HandRank object representing the one pair hand rank, or null if not found. */ - private static HandRank checkOnePair(Map rankCount) { + private static HandRank checkOnePair(Map rankCount, List allRanks) { long pairCount = rankCount.values().stream().filter(v -> v == PAIR).count(); @@ -313,8 +339,12 @@ public class HandEvaluator { } int pair = getRank(rankCount, PAIR); + List kickers = getKickers(allRanks, List.of(pair), THREE_KICKERS); - return new HandRank(HandRank.Type.ONE_PAIR, List.of(pair)); + List result = new ArrayList<>(List.of(pair)); + result.addAll(kickers); + + return new HandRank(HandRank.Type.ONE_PAIR, result); } /** @@ -333,6 +363,26 @@ public class HandEvaluator { .orElse(DEFAULT_VALUE); } + /** + * Helper method to extract kickers from the list of all card ranks. Excludes the ranks already + * used in the main hand combination. + * + * @param allRanks A sorted list of all card ranks in the hand. + * @param excludeRanks A list of ranks to exclude (e.g., ranks used in pairs, trips, etc.). + * @param numKickers The number of kickers to extract. + * @return A list of kicker ranks, sorted in descending order. + */ + private static List getKickers( + List allRanks, List excludeRanks, int numKickers) { + + Set excludeSet = new HashSet<>(excludeRanks); + + return allRanks.stream() + .filter(rank -> !excludeSet.contains(rank)) + .limit(numKickers) + .toList(); + } + /** * Helper method to determine if a list of card ranks forms a straight. * diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/rules/showdown/CardsSpeakRule.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/rules/showdown/CardsSpeakRule.java index 2be2607..fa602c1 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/rules/showdown/CardsSpeakRule.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/rules/showdown/CardsSpeakRule.java @@ -5,7 +5,6 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Card; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.evaluator.HandEvaluator; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.evaluator.HandRank; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player; -import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerStatus; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.Rule; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState; import java.util.ArrayList; @@ -43,14 +42,28 @@ public class CardsSpeakRule implements Rule { */ public Player determineWinner(GameState state) { + List winners = determineWinners(state); + return winners.isEmpty() ? null : winners.get(0); + } + + /** + * Determines all players that share the best hand rank. + * + * @param state The current state of the game. + * @return The list of winners, ordered by appearance in the state. + */ + public List determineWinners(GameState state) { + List players = new ArrayList<>(state.getPlayers()); - Player bestPlayer = null; HandRank bestRank = null; + List winners = new ArrayList<>(); for (Player player : players) { - if (player.getStatus() == PlayerStatus.FOLDED) { + // Use folded flag from game flow to stay consistent with winner calculation in + // controller. + if (player.isFolded()) { continue; } @@ -62,13 +75,15 @@ public class CardsSpeakRule implements Rule { HandRank rank = HandEvaluator.evaluate(cards); if (bestRank == null || rank.compareTo(bestRank) > 0) { - bestRank = rank; - bestPlayer = player; + winners.clear(); + winners.add(player); + } else if (rank.compareTo(bestRank) == 0) { + winners.add(player); } } - return bestPlayer; + return winners; } /** @@ -81,15 +96,23 @@ public class CardsSpeakRule implements Rule { */ public void awardPot(GameState state) { - Player winner = determineWinner(state); + List winners = determineWinners(state); - if (winner == null) { + if (winners.isEmpty()) { return; } int pot = state.getPot().getAmount(); + int share = winners.isEmpty() ? 0 : pot / winners.size(); + int remainder = winners.isEmpty() ? 0 : pot % winners.size(); - winner.addChips(pot); + for (int i = 0; i < winners.size(); i++) { + Player winner = winners.get(i); + if (winner == null) { + continue; + } + winner.addChips(share + (i < remainder ? 1 : 0)); + } state.getPot().reset(); } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/highscore/HighscoreService.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/highscore/HighscoreService.java index 929b3e3..f47f5a5 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/highscore/HighscoreService.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/highscore/HighscoreService.java @@ -18,6 +18,7 @@ public final class HighscoreService { private static final DateTimeFormatter DISPLAY_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault()); private static final int MAX_RETURNED_ENTRIES = 100; + private static final int MIN_MONEY_COLUMN_COUNT = 3; private static final String LINE_SEPARATOR = "\t"; private final Path storagePath; @@ -32,6 +33,11 @@ public final class HighscoreService { } public synchronized void appendWinner(String winnerName) { + appendWinner(winnerName, 0); + } + + /** Append a winner with an optional money value (money may be 0). */ + public synchronized void appendWinner(String winnerName, int money) { if (winnerName == null) { return; } @@ -41,6 +47,44 @@ public final class HighscoreService { return; } + String line; + if (money > 0) { + line = + Instant.now() + + LINE_SEPARATOR + + sanitized + + LINE_SEPARATOR + + money + + System.lineSeparator(); + } else { + // Keep legacy two-column format for backward compatibility + line = Instant.now() + LINE_SEPARATOR + sanitized + System.lineSeparator(); + } + + try { + Files.createDirectories(storagePath.getParent()); + Files.writeString( + storagePath, + line, + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, + StandardOpenOption.APPEND); + } catch (IOException ignored) { + // Highscore persistence must never break the game state response path. + } + } + + /** Append a preformatted highscore entry such as "Lars (1000), Jona (1000)". */ + public synchronized void appendFormattedEntry(String entryText) { + if (entryText == null) { + return; + } + + String sanitized = sanitizeName(entryText); + if (sanitized.isEmpty()) { + return; + } + String line = Instant.now() + LINE_SEPARATOR + sanitized + System.lineSeparator(); try { @@ -74,15 +118,25 @@ public final class HighscoreService { if (raw == null || raw.isBlank()) { continue; } - - String[] parts = raw.split(LINE_SEPARATOR, 2); + // Support both legacy format (TIMESTAMP \t NAME) and new format (TIMESTAMP \t + // NAME \t MONEY) + String[] parts = raw.split(LINE_SEPARATOR); if (parts.length < 2) { continue; } try { Instant ts = Instant.parse(parts[0].trim()); - String display = DISPLAY_FORMATTER.format(ts) + " | " + parts[1].trim(); + String name = parts[1].trim(); + String display = DISPLAY_FORMATTER.format(ts) + " | " + name; + if (parts.length >= MIN_MONEY_COLUMN_COUNT) { + try { + int money = Integer.parseInt(parts[2].trim()); + display = display + " | $" + money; + } catch (NumberFormatException nfe) { + // ignore malformed money + } + } formatted.add(display); } catch (Exception ignored) { // Skip malformed lines and keep all valid entries. diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/Lobby.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/Lobby.java index 7aec356..8a0628b 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/Lobby.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/Lobby.java @@ -9,6 +9,8 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.RuleEngine; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState; import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; import java.util.logging.Logger; @@ -26,7 +28,9 @@ public class Lobby { private final LobbyId id; private final String name; private final List playerNames = new CopyOnWriteArrayList<>(); + private final Set absentPlayers = ConcurrentHashMap.newKeySet(); private volatile GameController gameController; + private volatile Runnable onGameEndedCallback; public Lobby(LobbyId id, String name) { this.id = id; @@ -96,12 +100,91 @@ public class Lobby { public void initGame(GameController controller) { this.gameController = controller; + if (controller != null && onGameEndedCallback != null) { + controller.setOnGameEndedCallback(onGameEndedCallback); + } + } + + /** + * Set a callback to be invoked when the game in this lobby ends. This will be propagated to the + * game controller when a game is started. + */ + public void setOnGameEndedCallback(Runnable callback) { + this.onGameEndedCallback = callback; + if (gameController != null) { + gameController.setOnGameEndedCallback(callback); + } } public GameController getGameController() { return gameController; } + /** + * Check if a player is currently active (not absent) in the lobby. + * + * @param playerName the player to check + * @return true if player is in the active player list + */ + public boolean isPlayerActive(String playerName) { + return playerNames.contains(playerName); + } + + /** + * Mark a player as absent (left the lobby) while the game is still running. The player remains + * in the lobby's mappings but is moved to the absent set. + * + * @param playerName the player to mark as absent + * @return true if the player was active and is now absent + */ + public boolean leavePlayer(String playerName) { + if (playerName == null) { + return false; + } + boolean removed = playerNames.remove(playerName); + if (removed) { + absentPlayers.add(playerName); + } + return removed; + } + + /** + * Try to restore an absent player back to active. Returns true if player was absent and is now + * active again. + * + * @param playerName the player to restore + * @return true if player was restored from absent + */ + public boolean restoreAbsentPlayer(String playerName) { + if (playerName == null) { + return false; + } + boolean wasAbsent = absentPlayers.remove(playerName); + if (wasAbsent && !playerNames.contains(playerName)) { + playerNames.add(playerName); + return true; + } + return false; + } + + /** + * Get the set of absent (gone but not removed) players in this lobby. + * + * @return copy of absent players set + */ + public Set getAbsentPlayers() { + return Set.copyOf(absentPlayers); + } + + /** + * Check if this lobby has any players (active or absent). + * + * @return true if playerNames or absentPlayers is non-empty + */ + public boolean hasAnyPlayers() { + return !playerNames.isEmpty() || !absentPlayers.isEmpty(); + } + /** * Atomically add a player and, if the lobby reached {@code autoStartPlayers} and no game exists * yet, create and start a game. The operation is synchronized on the internal player list to @@ -139,6 +222,11 @@ public class Lobby { game.addPlayer(PlayerId.of(p), defaultStartChips); } + // Set the callback before starting the game + if (onGameEndedCallback != null) { + game.setOnGameEndedCallback(onGameEndedCallback); + } + game.startGame(); this.gameController = game; LOGGER.info(() -> "Auto-started game in lobby " + id.value()); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyCleanupJob.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyCleanupJob.java index 47c0413..713993c 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyCleanupJob.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyCleanupJob.java @@ -14,7 +14,8 @@ import org.apache.logging.log4j.Logger; /** Periodically removes expired empty lobbies and notifies connected sessions. */ public class LobbyCleanupJob implements Runnable { - private static final Logger LOGGER = LogManager.getLogger(LobbyCleanupJob.class); + private static final Logger LOGGER = + LogManager.getLogger(LobbyCleanupJob.class.getSimpleName()); private final LobbyManager lobbyManager; private final SessionManager sessionManager; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyEventListener.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyEventListener.java index d6fee3a..84dab32 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyEventListener.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyEventListener.java @@ -4,4 +4,7 @@ package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby; public interface LobbyEventListener { /** Called when a game is automatically started in the given lobby. */ void onGameStarted(LobbyId lobbyId); + + /** Called when a game in the given lobby ends (phase set to FINISHED). */ + void onGameEnded(LobbyId lobbyId); } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyManager.java index 0e58adf..394e0d6 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyManager.java @@ -71,6 +71,22 @@ public class LobbyManager { if (lobby == null) { return false; } + + // Check if player was absent and try to restore + if (lobby.getAbsentPlayers().contains(username)) { + boolean restored = lobby.restoreAbsentPlayer(username); + if (restored) { + LOGGER.info( + () -> + "User '" + + username + + "' rejoined absent slot in lobby " + + lobbyId.value()); + playerToLobby.put(username, lobbyId); + return true; + } + } + AddResult result = lobby.addPlayerAndMaybeStart( username, maxPlayersPerLobby, AUTO_START_PLAYERS, DEFAULT_START_CHIPS); @@ -94,6 +110,8 @@ public class LobbyManager { + AUTO_START_PLAYERS + " players; game started."); notifyGameStarted(lobbyId); + // Set the game-ended callback + notifySetGameEndedCallback(lobbyId); } return true; } @@ -121,6 +139,29 @@ public class LobbyManager { } } + private void notifySetGameEndedCallback(LobbyId lobbyId) { + Lobby lobby = activeLobbies.get(lobbyId); + if (lobby != null) { + lobby.setOnGameEndedCallback( + () -> { + notifyGameEnded(lobbyId); + }); + } + } + + private void notifyGameEnded(LobbyId lobbyId) { + for (LobbyEventListener l : listeners) { + try { + l.onGameEnded(lobbyId); + } catch (RuntimeException e) { + LOGGER.warning( + () -> + "Listener threw while handling game-end for lobby " + + lobbyId.value()); + } + } + } + public boolean removePlayer(String username) { LobbyId id = playerToLobby.remove(username); if (id == null) { @@ -138,6 +179,33 @@ public class LobbyManager { return removed; } + /** + * Mark a player as absent (left the lobby during a running game). The player is not removed but + * moved to the absent set, allowing them to rejoin later without losing their slot. + * + * @param username the player to mark as absent + * @param lobbyId the lobby they're leaving + * @return true if the player was marked absent successfully + */ + public boolean leavePlayerFromLobby(String username, LobbyId lobbyId) { + Lobby lobby = getLobby(lobbyId); + if (lobby == null) { + return false; + } + boolean left = lobby.leavePlayer(username); + if (left) { + LOGGER.info( + () -> + "User '" + + username + + "' left lobby " + + lobbyId.value() + + " (marked absent)"); + // Keep the playerToLobby mapping intact for rejoin + } + return left; + } + public Collection getAllLobbies() { return activeLobbies.values(); } @@ -169,6 +237,9 @@ public class LobbyManager { for (String username : removed.getPlayerNames()) { playerToLobby.remove(username); } + for (String username : removed.getAbsentPlayers()) { + playerToLobby.remove(username); + } } /** diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserCleanupJob.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserCleanupJob.java index 32cfbb4..9b520ab 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserCleanupJob.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserCleanupJob.java @@ -15,7 +15,7 @@ public class UserCleanupJob implements Runnable { private final Duration reconnectThreshold; public UserCleanupJob(UserRegistry registry, Duration reconnectThreshold) { - this.logger = LogManager.getLogger(UserCleanupJob.class); + this.logger = LogManager.getLogger(UserCleanupJob.class.getSimpleName()); this.registry = registry; this.reconnectThreshold = reconnectThreshold; } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserFactory.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserFactory.java index 0a41b0c..30f232a 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserFactory.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserFactory.java @@ -9,7 +9,7 @@ import org.apache.logging.log4j.Logger; public class UserFactory { private final UserRegistry registry; private final AtomicInteger anonymousCounter = new AtomicInteger(1); - private static final Logger LOGGER = LogManager.getLogger(UserFactory.class); + private static final Logger LOGGER = LogManager.getLogger(UserFactory.class.getSimpleName()); /** * Creates a new UserFactory backed by the given registry. diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/NetworkManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/NetworkManager.java index 83a39d3..65e3780 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/NetworkManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/NetworkManager.java @@ -26,7 +26,7 @@ public class NetworkManager implements Runnable { */ public NetworkManager(Integer port, SessionManager sessionManager, CommandRouter router) { this.port = port; - this.logger = LogManager.getLogger(NetworkManager.class); + this.logger = LogManager.getLogger(NetworkManager.class.getSimpleName()); this.thread = new Thread(this, "networkManager"); this.running = true; this.sessionManager = sessionManager; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionDisconnectJob.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionDisconnectJob.java index 6fd481d..04e90cc 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionDisconnectJob.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionDisconnectJob.java @@ -15,7 +15,7 @@ public class SessionDisconnectJob implements Runnable { public SessionDisconnectJob( SessionManager sessionManager, EventBus eventBus, Duration timeoutThreshold) { - this.logger = LogManager.getLogger(SessionDisconnectJob.class); + this.logger = LogManager.getLogger(SessionDisconnectJob.class.getSimpleName()); this.sessionManager = sessionManager; this.eventBus = eventBus; this.timeoutThreshold = timeoutThreshold; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionManager.java index 569b828..1797668 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionManager.java @@ -24,7 +24,7 @@ public class SessionManager { public SessionManager(EventBus eventBus, CommandParserDispatcher dispatcher) { this.sessions = new ConcurrentHashMap<>(); this.eventBus = eventBus; - this.logger = LogManager.getLogger(SessionManager.class); + this.logger = LogManager.getLogger(SessionManager.class.getSimpleName()); this.dispatcher = dispatcher; } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionReader.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionReader.java index 4d5e353..585cd54 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionReader.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionReader.java @@ -41,7 +41,7 @@ public class SessionReader implements Runnable { this.router = session.getRouter(); this.logger = LogManager.getLogger( - SessionReader.class.toString() + "-" + session.getId().value()); + SessionReader.class.getSimpleName() + "-" + session.getId().value()); } @Override diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionWriter.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionWriter.java index c59abb3..20c11e0 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionWriter.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionWriter.java @@ -18,7 +18,7 @@ public class SessionWriter implements Runnable { this.queue = session.getResponseQueue(); this.logger = LogManager.getLogger( - SessionReader.class.toString() + "-" + session.getId().value()); + SessionReader.class.getSimpleName() + "-" + session.getId().value()); } public void run() { diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/ui/sound/SoundManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/ui/sound/SoundManager.java new file mode 100644 index 0000000..5e7f6e6 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/ui/sound/SoundManager.java @@ -0,0 +1,182 @@ +package ch.unibas.dmi.dbis.cs108.casono.ui.sound; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import javafx.animation.PauseTransition; +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.scene.media.AudioClip; +import javafx.util.Duration; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Manages sound effects for the Casono game. Provides methods to play sounds for card reveals and + * button clicks. + */ +public class SoundManager { + private static final Logger LOGGER = LogManager.getLogger(SoundManager.class); + private static final SoundManager INSTANCE = new SoundManager(); + private final Map soundCache = new HashMap<>(); + private static final float MIN_VOLUME = 0.0f; + private static final float MAX_VOLUME = 1.0f; + private static final float DEFAULT_VOLUME = 0.5f; + private float volume = DEFAULT_VOLUME; + private static final long CARD_REVEAL_STAGGER_MS = 120; // Delay between card reveal sounds + + private SoundManager() {} + + /** Returns the singleton instance of SoundManager. */ + public static SoundManager getInstance() { + return INSTANCE; + } + + /** + * Pre-loads critical sounds into cache to avoid delays on first play. Should be called at + * application startup. + */ + public void preloadSounds() { + try { + getOrLoadSound("button-click"); + getOrLoadSound("card-reveal"); + } catch (Exception e) { + LOGGER.error("Error preloading sounds", e); + } + } + + /** Plays a button click sound. */ + public void playButtonClick() { + playSound("button-click"); + } + + /** Plays a card reveal sound (for dealt or community cards). */ + public void playCardReveal() { + playSound("card-reveal"); + } + + /** + * Plays multiple card reveal sounds in sequence with slight stagger/overlap. Used when multiple + * cards are revealed at once (e.g., Flop reveals 3 cards, Turn reveals 1 card). Sounds play + * with a slight delay between them to simulate a person revealing cards one by one. + * + * @param count The number of cards being revealed. Each card gets its own sound with stagger. + */ + public void playCardRevealMultiple(int count) { + if (count <= 0) { + return; + } + if (count == 1) { + playCardReveal(); + return; + } + + for (int i = 0; i < count; i++) { + final int cardIndex = i; + long delayMs = cardIndex * CARD_REVEAL_STAGGER_MS; + + PauseTransition pause = new PauseTransition(Duration.millis(delayMs)); + pause.setOnFinished(this::onCardRevealDelayFinished); + pause.play(); + } + } + + private void onCardRevealDelayFinished(ActionEvent event) { + Platform.runLater(this::playCardRevealSafely); + } + + private void playCardRevealSafely() { + try { + playCardReveal(); + } catch (Exception e) { + LOGGER.error("Error playing card reveal sound", e); + } + } + + /** Plays a card shuffle sound. */ + public void playCardShuffle() { + playSound("card-shuffle"); + } + + /** Plays a pot/chip sound. */ + public void playChip() { + playSound("chip"); + } + + /** + * Plays a generic sound by key. The sound file should exist at: /sounds/{category}/{key}.mp3 or + * .wav + */ + public void playSound(String soundKey) { + try { + AudioClip audioClip = getOrLoadSound(soundKey); + if (audioClip != null) { + audioClip.setVolume(volume); + audioClip.play(); + } + } catch (Exception e) { + LOGGER.error("Error playing sound: {}", soundKey, e); + } + } + + /** Gets or loads a sound from cache. Categorizes sounds automatically based on key prefix. */ + private AudioClip getOrLoadSound(String soundKey) { + if (soundCache.containsKey(soundKey)) { + return soundCache.get(soundKey); + } + + String category = categorizeSound(soundKey); + String resourcePath = String.format("/sounds/%s/%s.wav", category, soundKey); + + try { + URL soundUrl = getClass().getResource(resourcePath); + if (soundUrl != null) { + AudioClip clip = new AudioClip(soundUrl.toString()); + soundCache.put(soundKey, clip); + return clip; + } else { + LOGGER.warn("Sound resource not found: {}", resourcePath); + return null; + } + } catch (Exception e) { + LOGGER.error("Failed to load sound: {}", resourcePath, e); + return null; + } + } + + /** Categorizes a sound based on its key prefix. */ + private String categorizeSound(String soundKey) { + if (soundKey.startsWith("button")) { + return "buttons"; + } else if (soundKey.startsWith("card")) { + return "cards"; + } else { + return "game"; + } + } + + /** Sets the volume for sound effects (0.0 - 1.0). */ + public void setVolume(float volume) { + this.volume = Math.max(MIN_VOLUME, Math.min(MAX_VOLUME, volume)); + } + + /** Gets the current volume level. */ + public float getVolume() { + return volume; + } + + /** Clears the sound cache. */ + public void clearCache() { + soundCache.clear(); + } + + /** Mutes all sounds by setting volume to 0. */ + public void mute() { + setVolume(0.0f); + } + + /** Unmutes sounds by restoring to default volume. */ + public void unmute() { + setVolume(DEFAULT_VOLUME); + } +} diff --git a/src/main/resources/images/background.png b/src/main/resources/images/background.png index 2b84fa7..6788c65 100644 Binary files a/src/main/resources/images/background.png and b/src/main/resources/images/background.png differ diff --git a/src/main/resources/images/glass-walpaper-casono.png b/src/main/resources/images/glass-walpaper-casono.png new file mode 100644 index 0000000..ff38a64 Binary files /dev/null and b/src/main/resources/images/glass-walpaper-casono.png differ diff --git a/src/main/resources/images/parquet-black.png b/src/main/resources/images/parquet-black.png new file mode 100644 index 0000000..23b4ed7 Binary files /dev/null and b/src/main/resources/images/parquet-black.png differ diff --git a/src/main/resources/images/parquet.png b/src/main/resources/images/parquet.png new file mode 100644 index 0000000..7ab9c8b Binary files /dev/null and b/src/main/resources/images/parquet.png differ diff --git a/src/main/resources/images/profile-picture/poker_dealer_471.png b/src/main/resources/images/profile-picture/poker_dealer_471.png new file mode 100644 index 0000000..6df5761 Binary files /dev/null and b/src/main/resources/images/profile-picture/poker_dealer_471.png differ diff --git a/src/main/resources/images/profile-picture/poker_user_491.png b/src/main/resources/images/profile-picture/poker_user_491.png new file mode 100644 index 0000000..8e0047c Binary files /dev/null and b/src/main/resources/images/profile-picture/poker_user_491.png differ diff --git a/src/main/resources/images/rug-red.png b/src/main/resources/images/rug-red.png new file mode 100644 index 0000000..815fcda Binary files /dev/null and b/src/main/resources/images/rug-red.png differ diff --git a/src/main/resources/sounds/buttons/button-click.wav b/src/main/resources/sounds/buttons/button-click.wav new file mode 100644 index 0000000..397cb37 Binary files /dev/null and b/src/main/resources/sounds/buttons/button-click.wav differ diff --git a/src/main/resources/sounds/cards/card-reveal.wav b/src/main/resources/sounds/cards/card-reveal.wav new file mode 100644 index 0000000..1d873eb Binary files /dev/null and b/src/main/resources/sounds/cards/card-reveal.wav differ diff --git a/src/main/resources/ui-structure/Casinogameui-blackwhite.css b/src/main/resources/ui-structure/Casinogameui-blackwhite.css new file mode 100644 index 0000000..779c2b6 --- /dev/null +++ b/src/main/resources/ui-structure/Casinogameui-blackwhite.css @@ -0,0 +1,726 @@ +/*main box css*/ + +.root { + -fx-background-color: #000000; + -fx-font-family: "Monospaced", "Courier New"; /* font */ +} + +.background { + -fx-background-image: url("/images/parquet-black.png"); + -fx-background-size: cover; /* Image fills the entire window */ + /* Centers the image and prevents it from repeating in a tile-like pattern. */ + -fx-background-position: center center; + -fx-background-repeat: no-repeat; +} + +.info-text { + -fx-text-fill: #ffffff; + -fx-font-size: 16px; +} + +.casino-table { + -fx-background-color: #000000; + -fx-background-radius: 210; + -fx-border-radius: 200; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 12; + /*-fx-min-height: 60vh;*/ + /*-fx-max-height: 60vh;*/ + /*-fx-min-width: 90vh;*/ + /*-fx-max-width: 90vh;*/ + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 15, 15); +} + +.dealer-box { + -fx-background-color: #ffffff; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-background-radius: 0; + -fx-border-radius: 0; + -fx-min-width: 70; + -fx-min-height: 100; + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 4, 4); +} + +.table-title { + -fx-text-fill: #ffffff; + -fx-font-size: 28px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255), 0, 0, 3, 3);*/ +} + +.taskbar { + -fx-background-color: #000000; + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-padding: 10 25; + -fx-cursor: move; + -fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5); +} + +.taskbar:hover { + -fx-border-color: #ffffff; +} + +.taskbar.player-active-turn { + /* Active turn should only change the shadow to avoid flicker/transparency jumps. */ + -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5); +} + +.black-input-field { + -fx-background-color: #000000 !important; + -fx-text-fill: #ffffff !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-background-radius: 8 !important; + -fx-border-radius: 8 !important; + -fx-border-color: #333333 !important; + -fx-border-width: 2 !important; + -fx-padding: 5 12 !important; +} + +.black-input-field:hover { + -fx-translate-y: -3 !important; + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3) !important; +} + +.black-input-field:focused { + -fx-border-color: #ffffff !important; + /*-fx-background-color: #000000;*/ +} + +.gray-input-field { + -fx-background-color: #000000 !important; + -fx-text-fill: #ffffff !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-background-radius: 8 !important; + -fx-border-radius: 8 !important; + -fx-border-color: #333333 !important; + /*-fx-color-color: #000000;*/ + -fx-border-width: 2 !important; + -fx-padding: 5 12 !important; +} + +.gray-input-field:hover { + -fx-translate-y: -3 !important; + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3) !important; +} + +.gray-input-field:focused { + -fx-border-color: #ffffff !important; + /*-fx-background-color: #333333;*/ +} + +.yellow-input-field { + -fx-background-color: #b8860b !important; + -fx-text-fill: #ffffff !important; + -fx-prompt-text-fill: #ffffff !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-background-radius: 8 !important; + -fx-border-radius: 8 !important; + -fx-border-color: #ffd700 !important; + -fx-border-width: 2 !important; + -fx-padding: 5 12 !important; +} + +.yellow-input-field:hover { + -fx-translate-y: -3 !important; + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3) !important; +} + +/*.yellow-input-field:focused {*/ +/* -fx-border-color: #ffd700;*/ +/* -fx-background-color: #b8860b;*/ +/*}*/ + +.red-input-field { + -fx-background-color: #8b0000 !important; + -fx-text-fill: #ffffff !important; + -fx-prompt-text-fill: #ffffff !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-background-radius: 8 !important; + -fx-border-radius: 8 !important; + -fx-border-color: #ff4444 !important; + -fx-border-width: 2 !important; + -fx-padding: 5 12 !important; +} + +.red-input-field:hover { + -fx-translate-y: -3 !important; + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3) !important; +} + +/*.red-input-field:focused {*/ +/* -fx-border-color: #ff4444;*/ +/* -fx-background-color: #8b0000;*/ +/*}*/ + +.yellow-button { + -fx-background-color: #b8860b !important; + -fx-border-color: #ffd700 !important; + -fx-text-fill: #ffffff !important; +} + +.red-button { + -fx-background-color: #8b0000 !important; + -fx-border-color: #ff4444 !important; + -fx-text-fill: #ffffff !important; +} + +.gray-button { + -fx-background-color: #000000 !important; + -fx-border-color: #333333 !important; + -fx-text-fill: #ffffff !important; +} + +.gray-button .label { + -fx-text-fill: #ffffff !important; +} + +.green-button { + -fx-background-color: #000000 !important; + -fx-border-color: #333333 !important; + -fx-text-fill: #ffffff !important; +} + +.gray-button, .yellow-button, .red-button, .green-button { + -fx-background-radius: 12 !important; + -fx-border-radius: 12 !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-border-width: 2 !important; + -fx-padding: 6 15 !important; + -fx-cursor: hand !important; +} + +.gray-button:hover, .yellow-button:hover, .red-button:hover, .green-button:hover { + -fx-translate-y: -3 !important; + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3) !important; +} + +.community-cards-box { + -fx-alignment: center !important; + -fx-spacing: 12 !important; + -fx-padding: 15 !important; + -fx-background-color: transparent !important; +} + +.community-card { + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3); + + -fx-alignment: center; + -fx-spacing: 12; + -fx-padding: 20; + -fx-background-color: linear-gradient(to bottom, #0b3d2e, #06261d); + -fx-background-radius: 16; + -fx-scale-x: 1; + -fx-scale-y: 1; + -fx-translate-y: 0; + -fx-opacity: 1; +} + +/*.community-card:hover {*/ +/* -fx-translate-y: -3;*/ +/*}*/ + +.player-card { + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3); + + -fx-alignment: center; + -fx-spacing: 12; + -fx-padding: 20; + -fx-background-color: linear-gradient(to bottom, #0b3d2e, #06261d); + -fx-background-radius: 16; + -fx-scale-x: 1; + -fx-scale-y: 1; + -fx-translate-y: 0; + -fx-opacity: 1; +} + +/*.player-card:hover {*/ +/* -fx-translate-y: -3;*/ +/*}*/ + +.player-card.player-cards-active-turn { + -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5); +} + +.dealer-box.player-cards-active-turn { + -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5); +} + +/*.dealer-box:hover {*/ +/* -fx-translate-y: -3;*/ +/*}*/ + +.chips-icon { + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3); +} + +/*.chips-icon:hover {*/ +/* -fx-translate-y: -3;*/ +/*}*/ + +.dealer-icon { + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3); +} + +.player-cards-box { + -fx-padding: 10; +} + +/*.player-cards-box.player-cards-active-turn {*/ +/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/ +/*}*/ + +.money-value { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/ +} + +.pot-title { + -fx-text-fill: #ffffff; + -fx-font-size: 14px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);*/ +} + +.pot-box { + -fx-padding: 5; +} + +.player-status-pane { + -fx-background-color: rgba(13, 158, 59, 0); + -fx-background-radius: 15; + -fx-border-radius: 15; + /* -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 2; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.5), 10, 0, 0, 4);*/ +} + +.player-status-pane:hover { + -fx-translate-y: -3; +} + +.status-inner-box-top { + -fx-background-color: #000000; + -fx-background-radius: 19; + -fx-border-radius: 15; + -fx-padding: 5 10; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); +} + +.status-inner-box-midle { + -fx-background-color: rgba(13, 158, 59, 0); +} + +.status-inner-box-bottom { + -fx-background-color: #000000; + -fx-background-radius: 19; + -fx-border-radius: 15; + -fx-padding: 5 10; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); +} + +.status-inner-box-top.player-status-active-turn { + -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5); +} + +.status-inner-box-bottom.player-status-active-turn { + -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5); +} + +.status-label-small { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/ +} + +.status-value-text { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/ +} + +.status-value-money { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/ +} + +.status-circle { + -fx-background-color: #000000; + -fx-background-radius: 50; + -fx-border-color: #ffffff; + -fx-border-width: 1; + -fx-border-radius: 50; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); +} + +.browser-root { + -fx-background-image: url("/images/background.png"); + -fx-background-size: cover; + -fx-background-position: center; + -fx-background-color: #0b2d15; +} + +.browser-web-view { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-padding: 0; + -fx-cursor: default; + -fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5); +} + +.web-view { + -fx-background-color: transparent; +} + +.taskbar-browser { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-padding: 10 25; + -fx-cursor: default; + -fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5); +} + +/*.taskbar-browser:hover {*/ +/* -fx-border-color: #ffffff;*/ +/*}*/ + +.security-label { + -fx-text-fill: #000000; + -fx-font-weight: bold; + -fx-font-size: 16px; +} + +.context-menu { + -fx-background-color: #333333 !important; + -fx-text-fill: #ffffff !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-background-radius: 8 !important; + -fx-border-radius: 8 !important; + -fx-border-color: #333333 !important; + -fx-color-color: #cccccc !important; + -fx-border-width: 2 !important; + -fx-padding: 5 12 !important; +} + +.context-menu .menu-item { + -fx-background-color: #333333 !important; +} + +.context-menu .menu-item .label { + -fx-text-fill: #ffffff !important; + -fx-font-size: 13px !important; +} + +/*.context-menu .menu-item:focused {*/ +/* -fx-background-color: #333333;*/ +/*}*/ + +.context-menu .menu-item:hover .label, +.context-menu .menu-item:focused .label { + /*-fx-text-fill: #b8860b;*/ + -fx-font-size: 15px !important; +} + +.notebook { + -fx-background-color: #000000; + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-effect: dropshadow(one-pass-box, rgb(255, 255, 255), 0, 0, 5, 5); + /*-fx-cursor: move;*/ + + -fx-padding: 0 0 15 0; +} + +.notebook:hover { + -fx-border-color: #ffffff; +} + +/*.notebook.player-active-turn {*/ +/* !* Active turn should only change the shadow to avoid flicker/transparency jumps. *!*/ +/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/ +/*}*/ + +.notebook-header { + -fx-background-color: #000000; + -fx-background-radius: 25 25 0 0; + -fx-padding: 8 10; + -fx-cursor: move; +} + +.notebook-title { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, rgb(255, 255, 255), 0, 0, 3, 3);*/ +} + +.notebook-content { + -fx-background-color: transparent; + -fx-padding: 0; +} + +.notebook-content > .viewport { + -fx-background-color: #000000; + -fx-background-radius: 0 0 25 25; +} + + +.scroll-pane { + -fx-background-color: transparent; + -fx-border-color: transparent; +} + +.scroll-pane .scroll-bar:vertical, +.scroll-pane .scroll-bar:horizontal { + -fx-background-color: transparent; +} + +.scroll-pane .scroll-bar .track { + -fx-background-color: #000000; + -fx-background-radius: 5; +} + +.scroll-pane .scroll-bar .thumb { + -fx-background-color: #333333; + -fx-background-radius: 5; +} + +.scroll-pane .scroll-bar .thumb:hover { + -fx-background-color: #666666; +} + +.notebook-tips-list { + -fx-padding: 10; + -fx-spacing: 10; + -fx-background-color: transparent; +} + +.notebook-tips-list .text-flow { + -fx-padding: 12; + -fx-background-color: #000000; + -fx-background-radius: 15; +} + +.settings-box { + -fx-background-color: #000000; + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-effect: dropshadow(one-pass-box, rgb(255, 255, 255), 0, 0, 5, 5); + /*-fx-cursor: move;*/ + -fx-padding: 0 0 15 0; +} + +.settings-box:hover { + -fx-border-color: #ffffff; +} + +/*.settings-box.player-active-turn {*/ +/* !* Active turn should only change the shadow to avoid flicker/transparency jumps. *!*/ +/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/ +/*}*/ + +.settings-header { + -fx-background-color: transparent; + -fx-background-radius: 20 20 0 0; + -fx-padding: 10 15; + -fx-cursor: move; + -fx-border-width: 0 0 1 0; +} + +.settings-label { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, rgb(255, 255, 255), 0, 0, 3, 3);*/ +} + +.settings-content { + -fx-padding: 15 20 20 20; + -fx-spacing: 12; +} + +.settings-content { + -fx-background-color: transparent; + -fx-text-fill: #ffffff; + -fx-font-size: 13px; +} + +.radio-button { + -fx-text-fill: #ffffff; + -fx-font-size: 13px; + + -fx-padding: 6 8; + -fx-background-radius: 10; +} + +.radio-button:hover { + -fx-font-size: 15px; +} + +.radio-button:selected { + -fx-text-fill: #b8860b; +} + +.radio-button .radio { + -fx-background-color: #b8860b; + /*-fx-border-width: 3;*/ + /*-fx-border-color: #b8860b;*/ + /*-fx-border-radius: 50%;*/ +} + +.radio-button:selected .radio { + -fx-background-color: #b8860b; + -fx-effect: dropshadow(gaussian, rgba(184,134,11,0.8), 6, 0.3, 0, 0); +} + +.settings-box { + -fx-background-insets: 0; +} + +.settings-content { + -fx-background-color: transparent; +} + +/*chat box css*/ + +.chat-box { + -fx-background-color: #000000 !important; + -fx-background-radius: 58; + -fx-border-radius: 50; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10 !important; + -fx-border-width: 12; + -fx-padding: 20; + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255), 0, 0, 15, 15) !important; +} + +.tab-chat-box { + -fx-background-color: #000000 !important; + -fx-background-radius: 23 !important; + -fx-border-radius: 20 !important; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10 !important; + -fx-border-width: 3 !important; + -fx-padding: 20; +} + +.chat-header { + -fx-text-fill: #ffffff !important; + -fx-font-size: 22px; + -fx-font-weight: bold; + -fx-padding: 0 0 10 0; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 0, 0) !important; +} + +.chat-separator { + -fx-background-color: #ffffff !important; + -fx-min-height: 4px; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 0, 0) !important; +} + +.tab-pane .tab-header-area .tab-header-background { + -fx-opacity: 0; + -fx-background-color: #000000 !important; + -fx-background-radius: 18 !important; + -fx-border-radius: 10 !important; + -fx-border-color: #000000 !important; + -fx-border-width: 5; + -fx-padding: 10; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0), 0, 0, 0, 0) !important; + -fx-min-height: 10; +} + +.tab-pane .tab { + -fx-background-color: #000000 !important; + -fx-background-radius: 18 !important; + -fx-border-radius: 10 !important; + -fx-border-color: #000000 !important; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0), 0, 0, 0, 0) !important; + -fx-border-width: 5; + -fx-padding: 6 15; +} + +.tab-pane .tab:selected { + -fx-background-color: #000000 !important; + -fx-background-radius: 18 !important; + -fx-border-radius: 10 !important; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10 !important; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0), 0, 0, 0, 0) !important; + -fx-border-width: 3 !important; + -fx-padding: 6 15; +} + +.tab-pane .tab .tab-close-button { + -fx-text-fill: #ffffff; +} + +.tab-pane .tab-header-area .headers-region { + -fx-background-color: #000000 !important; +} + +.tab .tab-label { + -fx-alignment: CENTER; + -fx-text-fill: #ffffff; + -fx-font-size: 12px; + -fx-font-weight: bold; +} + +.tab:selected .tab-label { + -fx-alignment: CENTER; + -fx-text-fill: #ffffff; +} + +.menu-button { + -fx-background-color: #000000 !important; + -fx-border-color: #333333 !important; + -fx-text-fill: #ffffff !important; + -fx-background-radius: 12 !important; + -fx-border-radius: 12 !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-border-width: 2 !important; + -fx-padding: 6 15 !important; + -fx-cursor: hand !important; +} + +.menu-button .label { + -fx-text-fill: #ffffff !important; +} + +.menu-button:hover { + -fx-translate-y: -3 !important; + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3) !important; +} + +.menu-button .menu-item { + -fx-background-color: #333333 !important; + -fx-border-color: #333333 !important; + -fx-text-fill: #ffffff; +} diff --git a/src/main/resources/ui-structure/Casinogameui-glass.css b/src/main/resources/ui-structure/Casinogameui-glass.css new file mode 100644 index 0000000..e36d99d --- /dev/null +++ b/src/main/resources/ui-structure/Casinogameui-glass.css @@ -0,0 +1,1123 @@ +/*main box css*/ + +.root { + -fx-background-color: #000000; + -fx-font-family: "Monospaced", "Courier New"; /* font */ +} + +.background { + -fx-background-color: #000000; + -fx-background-image: url("/images/glass-walpaper-casono.png"); + -fx-background-size: cover; /* Image fills the entire window */ + /* Centers the image and prevents it from repeating in a tile-like pattern. */ + -fx-background-position: center center; + -fx-background-repeat: no-repeat; +} + +.info-text { + -fx-text-fill: #ffffff; + -fx-font-size: 16px; +} + +.casino-table { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(255 ,255 ,255 ,0.18), + rgba(255 ,255 ,255 ,0.08) + ); + -fx-background-radius: 210; + -fx-border-radius: 200; + -fx-border-color: rgba(255, 255, 255, 0.25); + -fx-border-width: 12; + /*-fx-min-height: 60vh;*/ + /*-fx-max-height: 60vh;*/ + /*-fx-min-width: 90vh;*/ + /*-fx-max-width: 90vh;*/ + -fx-effect: + dropshadow( + gaussian, + rgba(0 ,0 ,0 , 0.25), + 40, + 0.2, + 0, + 12 + ); +} + +.dealer-box { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(255 ,255 ,255 ,0.38), + rgba(255 ,255 ,255 ,0.18) + ); + + -fx-border-color: rgba(255, 255, 255, 0.40); + -fx-border-width: 3; + -fx-background-radius: 28; + -fx-border-radius: 0; + -fx-min-width: 70; + -fx-min-height: 100; + -fx-effect: + dropshadow( + gaussian, + rgba(0 ,0 ,0 , 0.45), + 40, + 0.2, + 0, + 12 + ); +} + +.table-title { + -fx-text-fill: #ffffff; + -fx-font-size: 28px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255), 0, 0, 3, 3);*/ +} + +.taskbar { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(255, 255, 255, 0.38), + rgba(255, 255, 255, 0.18) + ); + + -fx-background-radius: 28; + -fx-border-radius: 28; + -fx-border-color: rgba(255, 255, 255, 0.40); + -fx-border-width: 3; + -fx-padding: 14 30; + -fx-effect: + dropshadow( + gaussian, + rgba(0, 0, 0, 0.45), + 40, + 0.2, + 0, + 12 + ); + + -fx-cursor: move; +} + +.taskbar:hover { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(255, 255, 255, 0.44), + rgba(255, 255, 255, 0.22) + ); + + -fx-border-color: + rgba(255, 255, 255, 0.55); + +} + +.taskbar.player-active-turn { + /* Active turn should only change the shadow to avoid flicker/transparency jumps. */ + -fx-border-color: rgba(184, 134, 11, 0.40); + -fx-effect: + dropshadow( + gaussian, + rgba(184, 134, 11, 0.45), + 40, + 0.2, + 0, + 12 + ); +} + +.black-input-field { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18) + ) !important; + -fx-text-fill: #ffffff !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-background-radius: 8 !important; + -fx-border-radius: 8 !important; + -fx-border-color: rgba(255, 255, 255, 0.40) !important; + -fx-border-width: 2 !important; + -fx-padding: 5 12 !important; +} + +.black-input-field:hover { + -fx-translate-y: -3 !important; + -fx-background-color: + linear-gradient( + to bottom right, + rgba(0, 0, 0, 0.44), + rgba(0, 0, 0, 0.22) + ) !important; +} + +.black-input-field:focused { + -fx-border-color: rgba(255, 255, 255, 0.55) !important; + /*-fx-background-color: #000000;*/ +} + +.gray-input-field { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18) + ) !important; + -fx-text-fill: #ffffff !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-background-radius: 8 !important; + -fx-border-radius: 8 !important; + -fx-border-color: rgba(255, 255, 255, 0.40) !important; + /*-fx-color-color: #000000;*/ + -fx-border-width: 2 !important; + -fx-padding: 5 12 !important; +} + +.gray-input-field:hover { + -fx-translate-y: -3 !important; + -fx-effect: + dropshadow( + gaussian, + rgba(255, 255, 255, 0.45), + 40, + 0.2, + 0, + 12 + ) !important; +} + +.gray-input-field:focused { + -fx-border-color: rgba(255, 255, 255, 0.55) !important; + /*-fx-background-color: #333333;*/ +} + +.yellow-input-field { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(184, 134, 11, 0.38), + rgba(184, 134, 11, 0.18) + ) !important; + -fx-text-fill: #ffffff !important; + -fx-prompt-text-fill: #ffffff !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-background-radius: 8 !important; + -fx-border-radius: 8 !important; + -fx-border-color: rgba(255, 215, 0, 0.40) !important; + -fx-border-width: 2 !important; + -fx-padding: 5 12 !important; +} + +.yellow-input-field:hover { + -fx-translate-y: -3 !important; + -fx-effect: + dropshadow( + gaussian, + rgba(255, 255, 255, 0.55), + 40, + 0.2, + 0, + 12 + ) !important; +} + +/*.yellow-input-field:focused {*/ +/* -fx-border-color: #ffd700;*/ +/* -fx-background-color: #b8860b;*/ +/*}*/ + +.red-input-field { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(139, 0, 0, 0.38), + rgba(139, 0, 0, 0.18) + ) !important; + -fx-text-fill: #ffffff !important; + -fx-prompt-text-fill: #ffffff !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-background-radius: 8 !important; + -fx-border-radius: 8 !important; + -fx-border-color: rgba(255, 68, 68, 0.40) !important; + -fx-border-width: 2 !important; + -fx-padding: 5 12 !important; +} + +.red-input-field:hover { + -fx-translate-y: -3 !important; + -fx-effect: + dropshadow( + gaussian, + rgba(255, 255, 255, 0.55), + 40, + 0.2, + 0, + 12 + ) !important; +} + +/*.red-input-field:focused {*/ +/* -fx-border-color: #ff4444;*/ +/* -fx-background-color: #8b0000;*/ +/*}*/ + +.yellow-button { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(184, 134, 11, 0.38), + rgba(184, 134, 11, 0.18) + ) !important; + -fx-border-color: rgba(255, 215, 0, 0.40) !important; + -fx-text-fill: #ffffff !important; +} + +.red-button { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(139, 0, 0, 0.38), + rgba(139, 0, 0, 0.18) + ) !important; + -fx-border-color: rgba(255, 68, 68, 0.40) !important; + -fx-text-fill: #ffffff !important; +} + +.gray-button { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18) + ) !important; + -fx-border-color: rgba(51, 51, 51, 0.40) !important; + -fx-text-fill: #ffffff !important; +} + +.gray-button .label { + -fx-text-fill: #ffffff !important; +} + +.green-button { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18) + ) !important; + -fx-border-color: rgba(51, 51, 51, 0.40) !important; + -fx-text-fill: #ffffff !important; +} + +.gray-button, .yellow-button, .red-button, .green-button { + -fx-background-radius: 12 !important; + -fx-border-radius: 12 !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-border-width: 2 !important; + -fx-padding: 6 15 !important; + -fx-cursor: hand !important; +} + +.gray-button:hover, .yellow-button:hover, .red-button:hover, .green-button:hover { + -fx-translate-y: -3 !important; + -fx-effect: + dropshadow( + gaussian, + rgba(255, 255, 255, 0.45), + 40, + 0.2, + 0, + 12 + ) !important; +} + +.community-cards-box { + -fx-alignment: center !important; + -fx-spacing: 12 !important; + -fx-padding: 15 !important; + -fx-background-color: transparent !important; +} + +.community-card { + -fx-effect: + dropshadow( + gaussian, + rgba(255, 255, 255, 0.45), + 40, + 0.2, + 0, + 12 + ) !important; + + -fx-alignment: center; + -fx-spacing: 12; + -fx-padding: 20; + -fx-background-color: linear-gradient(to bottom, #0b3d2e, #06261d); + -fx-background-radius: 16; + -fx-scale-x: 1; + -fx-scale-y: 1; + -fx-translate-y: 0; + -fx-opacity: 1; +} + +.community-card:hover { + -fx-translate-y: -3; +} + +.player-card { + -fx-effect: + dropshadow( + gaussian, + rgba(255, 255, 255, 0.45), + 40, + 0.2, + 0, + 12 + ) !important; + + -fx-alignment: center; + -fx-spacing: 12; + -fx-padding: 20; + -fx-background-color: linear-gradient(to bottom, #0b3d2e, #06261d); + -fx-background-radius: 16; + -fx-scale-x: 1; + -fx-scale-y: 1; + -fx-translate-y: 0; + -fx-opacity: 1; +} + +.player-card:hover { + -fx-translate-y: -3; +} + +.player-card.player-cards-active-turn { + -fx-effect: + dropshadow( + gaussian, + rgba(184, 134, 11, 0.45), + 40, + 0.2, + 0, + 12 + ); +} + +.dealer-box.player-cards-active-turn { + -fx-effect: + dropshadow( + gaussian, + rgba(184, 134, 11, 0.45), + 40, + 0.2, + 0, + 12 + ); +} + +/*.dealer-box:hover {*/ +/* -fx-translate-y: -3;*/ +/*}*/ + +.chips-icon { + -fx-effect: + dropshadow( + gaussian, + rgba(255, 255, 255, 0.45), + 40, + 0.2, + 0, + 12 + ) !important; +} + +/*.chips-icon:hover {*/ +/* -fx-translate-y: -3;*/ +/*}*/ + +.dealer-icon { + -fx-effect: + dropshadow( + gaussian, + rgba(255, 255, 255, 0.45), + 40, + 0.2, + 0, + 12 + ) !important; +} + +.player-cards-box { + -fx-padding: 10; +} + +/*.player-cards-box.player-cards-active-turn {*/ +/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/ +/*}*/ + +.money-value { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/ +} + +.pot-title { + -fx-text-fill: #ffffff; + -fx-font-size: 14px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);*/ +} + +.pot-box { + -fx-padding: 5; +} + +.player-status-pane { + -fx-background-color: transparent; + -fx-background-radius: 15; + -fx-border-radius: 15; + /* -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 2; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.5), 10, 0, 0, 4);*/ +} + +.player-status-pane:hover { + -fx-translate-y: -3; +} + +.status-inner-box-top { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18) + ) !important; + -fx-background-radius: 19; + -fx-border-radius: 15; + -fx-padding: 5 10; + -fx-border-color: rgba(255, 255, 255, 0.40); + -fx-border-width: 3; + -fx-effect: + dropshadow( + gaussian, + rgba(0, 0, 0, 0.45), + 40, + 0.2, + 0, + 12 + ); +} + +.status-inner-box-midle { + -fx-background-color: rgba(13, 158, 59, 0); +} + +.status-inner-box-bottom { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18) + ) !important; + -fx-background-radius: 19; + -fx-border-radius: 15; + -fx-padding: 5 10; + -fx-border-color: rgba(255, 255, 255, 0.40); + -fx-border-width: 3; + -fx-effect: + dropshadow( + gaussian, + rgba(0, 0, 0, 0.45), + 40, + 0.2, + 0, + 12 + ); +} + +.status-inner-box-top.player-status-active-turn { + -fx-effect: + dropshadow( + gaussian, + rgba(184, 134, 11, 0.45), + 40, + 0.2, + 0, + 12 + ); +} + +.status-inner-box-bottom.player-status-active-turn { + -fx-effect: + dropshadow( + gaussian, + rgba(184, 134, 11, 0.45), + 40, + 0.2, + 0, + 12 + ); +} + +.status-label-small { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/ +} + +.status-value-text { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/ +} + +.status-value-money { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/ +} + +.status-circle { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18) + ) !important; + -fx-background-radius: 50; + -fx-border-color: rgba(255, 255, 255, 0.40); + -fx-border-width: 1; + -fx-border-radius: 50; + -fx-effect: + dropshadow( + gaussian, + rgba(0, 0, 0, 0.45), + 40, + 0.2, + 0, + 12 + ); +} + +.browser-root { + -fx-background-image: url("/images/background.png"); + -fx-background-size: cover; + -fx-background-position: center; + -fx-background-color: #0b2d15; +} + +.browser-web-view { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-padding: 0; + -fx-cursor: default; + -fx-effect: + dropshadow( + gaussian, + rgba(0, 0, 0, 0.45), + 40, + 0.2, + 0, + 12 + ); +} + +.web-view { + -fx-background-color: transparent; +} + +.taskbar-browser { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-padding: 10 25; + -fx-cursor: default; + -fx-effect: + dropshadow( + gaussian, + rgba(0, 0, 0, 0.45), + 40, + 0.2, + 0, + 12 + ); +} + +/*.taskbar-browser:hover {*/ +/* -fx-border-color: #ffffff;*/ +/*}*/ + +.security-label { + -fx-text-fill: #000000; + -fx-font-weight: bold; + -fx-font-size: 16px; +} + +.context-menu { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(51, 51, 51, 0.38), + rgba(51, 51, 51, 0.18) + ) !important; + -fx-text-fill: #ffffff !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-background-radius: 8 !important; + -fx-border-radius: 8 !important; + -fx-border-color: rgba(51, 51, 51, 0.40) !important; + -fx-color-color: #cccccc !important; + -fx-border-width: 2 !important; + -fx-padding: 5 12 !important; +} + +.context-menu .menu-item { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(51, 51, 51, 0.38), + rgba(51, 51, 51, 0.18) + ) !important; +} + +.context-menu .menu-item .label { + -fx-text-fill: #ffffff !important; + -fx-font-size: 13px !important; +} + +/*.context-menu .menu-item:focused {*/ +/* -fx-background-color: #333333;*/ +/*}*/ + +.context-menu .menu-item:hover .label, +.context-menu .menu-item:focused .label { + /*-fx-text-fill: #b8860b;*/ + -fx-font-size: 15px !important; +} + +.notebook { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18) + ); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: rgba(255, 255, 255, 0.40); + -fx-border-width: 3; + -fx-effect: + dropshadow( + gaussian, + rgba(255, 255, 255, 0.45), + 40, + 0.2, + 0, + 12 + ) !important; + /*-fx-cursor: move;*/ + -fx-padding: 0 0 15 0; +} + +.notebook:hover { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(0, 0, 0, 0.44), + rgba(0, 0, 0, 0.22) + ); + + -fx-border-color: + rgba(255, 255, 255, 0.55); +} + +/*.notebook.player-active-turn {*/ +/* !* active turn should only change the shadow to avoid flicker/transparency jumps. *!*/ +/* -fx-border-color:*/ +/* rgba(184, 134, 11, 0.55);*/ +/* -fx-effect:*/ +/* dropshadow(*/ +/* gaussian,*/ +/* rgba(184, 134, 11, 0.45),*/ +/* 40,*/ +/* 0.2,*/ +/* 0,*/ +/* 12*/ +/* );*/ +/*}*/ + +.notebook-header { + -fx-background-color: transparent !important; + -fx-background-radius: 25 25 0 0; + -fx-padding: 8 10; + -fx-cursor: move; +} + +.notebook-title { + -fx-text-fill: #ffffff; + -fx-background-color: transparent !important; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, rgb(255, 255, 255), 0, 0, 3, 3);*/ +} + +.notebook-content { + -fx-background-color: transparent; + -fx-padding: 0; +} + +.notebook-content > .viewport { + -fx-background-color: transparent !important; + -fx-background-radius: 0 0 25 25; +} + + +.scroll-pane { + -fx-background-color: transparent; + -fx-border-color: transparent; +} + +.scroll-pane .scroll-bar:vertical, +.scroll-pane .scroll-bar:horizontal { + -fx-background-color: transparent; +} + +.scroll-pane .scroll-bar .track { + -fx-background-color: transparent !important; + -fx-background-radius: 5; +} + +.scroll-pane .scroll-bar .thumb { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(200, 200, 200, 0.38), + rgba(200, 200, 200, 0.18) + ) !important; + -fx-background-radius: 5; +} + +.scroll-pane .scroll-bar .thumb:hover { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(255, 255, 255, 0.38), + rgba(255, 255, 255, 0.18) + ) !important; +} + +.notebook-tips-list { + -fx-padding: 10; + -fx-spacing: 10; + -fx-background-color: transparent; +} + +.notebook-tips-list .text-flow { + -fx-padding: 12; + -fx-background-color: + linear-gradient( + to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18) + ); + -fx-background-radius: 15; +} + +.settings-box { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18) + ); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: rgba(255, 255, 255, 0.40); + -fx-border-width: 3; + -fx-effect: + dropshadow( + gaussian, + rgba(255, 255, 255, 0.45), + 40, + 0.2, + 0, + 12 + ) !important; + /*-fx-cursor: move;*/ + -fx-padding: 0 0 15 0; +} + +.settings-box:hover { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(0, 0, 0, 0.44), + rgba(0, 0, 0, 0.22) + ); + + -fx-border-color: + rgba(255, 255, 255, 0.55); +} + +/*.settings-box.player-active-turn {*/ +/* !* Active turn should only change the shadow to avoid flicker/transparency jumps. *!*/ +/* -fx-border-color:*/ +/* rgba(184, 134, 11, 0.55);*/ +/* -fx-effect:*/ +/* dropshadow(*/ +/* gaussian,*/ +/* rgba(184, 134, 11, 0.45),*/ +/* 40,*/ +/* 0.2,*/ +/* 0,*/ +/* 12*/ +/* );*/ +/*}*/ + +.settings-header { + -fx-background-color: transparent; + -fx-background-radius: 20 20 0 0; + -fx-padding: 10 15; + -fx-cursor: move; + -fx-border-width: 0 0 1 0; +} + +.settings-label { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, rgb(255, 255, 255), 0, 0, 3, 3);*/ +} + +.settings-content { + -fx-padding: 15 20 20 20; + -fx-spacing: 12; +} + +.settings-content { + -fx-background-color: transparent; + -fx-text-fill: #ffffff; + -fx-font-size: 13px; +} + +.radio-button { + -fx-text-fill: #ffffff; + -fx-font-size: 13px; + + -fx-padding: 6 8; + -fx-background-radius: 10; +} + +.radio-button:hover { + -fx-font-size: 15px; +} + +.radio-button:selected { + -fx-text-fill: #ffffff; +} + +.radio-button .radio { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(184, 134, 11, 0.38), + rgba(184, 134, 11, 0.18) + ); + /*-fx-border-width: 3;*/ + /*-fx-border-color: #b8860b;*/ + /*-fx-border-radius: 50%;*/ +} + +.radio-button:selected .radio { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(184, 134, 11, 0.38), + rgba(184, 134, 11, 0.18) + ); + -fx-effect: + dropshadow( + gaussian, + rgba(184, 134, 11, 0.45), + 40, + 0.2, + 0, + 12 + ); +} + +.settings-box { + -fx-background-insets: 0; +} + +.settings-content { + -fx-background-color: transparent; +} + +/*chat box css*/ + +.chat-box { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(255, 255, 255, 0.18), + rgba(255, 255, 255, 0.08) + ) !important; + -fx-background-radius: 58; + -fx-border-radius: 50; + -fx-border-color: rgba(255, 255, 255, 0.25) !important; + -fx-border-width: 12; + -fx-padding: 20; + -fx-effect: + dropshadow( + gaussian, + rgba(255, 255, 255, 0.25), + 40, + 0.2, + 0, + 12 + ) !important; +} + +.tab-chat-box { + -fx-background-color: transparent !important; + -fx-background-radius: 23 !important; + -fx-border-radius: 20 !important; + -fx-border-color: rgba(255, 255, 255, 0.40) !important; + -fx-border-width: 3 !important; + -fx-padding: 20; +} + +.chat-header { + -fx-text-fill: #ffffff !important; + -fx-font-size: 22px; + -fx-font-weight: bold; + -fx-padding: 0 0 10 0; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 0, 0) !important; +} + +.chat-separator { + -fx-background-color: transparent !important; + -fx-min-height: 4px; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 0, 0) !important; +} + +.tab-header-area .tab-header-background { + -fx-background-color: transparent !important; + -fx-background-radius: 18 !important; + -fx-border-radius: 10 !important; + -fx-border-color: rgba(0, 0, 0, 0) !important; + -fx-border-width: 5; + -fx-padding: 10; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0), 0, 0, 0, 0) !important; + -fx-min-height: 10; +} + +.tab-pane .tab { + -fx-background-color: transparent !important; + -fx-background-radius: 18 !important; + -fx-border-radius: 10 !important; + -fx-border-color: rgba(255, 255, 255, 0) !important; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0), 0, 0, 0, 0) !important; + -fx-border-width: 5; + -fx-padding: 6 15; +} + +.tab-pane .tab:selected { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18) + ) !important; + -fx-background-radius: 18 !important; + -fx-border-radius: 10 !important; + -fx-border-color: rgba(255, 255, 255, 0.40) !important; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0), 0, 0, 0, 0) !important; + -fx-border-width: 3 !important; + -fx-padding: 6 15; +} + +.tab-pane .tab .tab-close-button { + -fx-text-fill: #ffffff !important; +} + +.tab-pane .tab-header-area .headers-region { + -fx-background-color: transparent !important; +} + +.tab .tab-label { + -fx-alignment: CENTER; + -fx-text-fill: #ffffff; + -fx-font-size: 12px; + -fx-font-weight: bold; +} + +.tab:selected .tab-label { + -fx-alignment: CENTER; + -fx-text-fill: #ffffff; +} + +.menu-button { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18) + ) !important; + -fx-border-color: rgba(51, 51, 51, 0.40) !important; + -fx-text-fill: #ffffff !important; + -fx-background-radius: 12 !important; + -fx-border-radius: 12 !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-border-width: 2 !important; + -fx-padding: 6 15 !important; + -fx-cursor: hand !important; +} + +.menu-button .label { + -fx-text-fill: #ffffff !important; +} + +.menu-button:hover { + -fx-translate-y: -3 !important; + -fx-effect: + dropshadow( + gaussian, + rgba(255, 255, 255, 0.45), + 40, + 0.2, + 0, + 12 + ) !important; +} + +.menu-button .menu-item { + -fx-background-color: + linear-gradient( + to bottom right, + rgba(51, 51, 51, 0.38), + rgba(51, 51, 51, 0.18) + ) !important; + -fx-border-color: rgba(51, 51, 51, 0.40) !important; + -fx-text-fill: #ffffff; +} diff --git a/src/main/resources/ui-structure/Casinogameui-light-glass.css b/src/main/resources/ui-structure/Casinogameui-light-glass.css new file mode 100644 index 0000000..4a32bbf --- /dev/null +++ b/src/main/resources/ui-structure/Casinogameui-light-glass.css @@ -0,0 +1,846 @@ +/*main box css*/ + +.root { + -fx-background-color: #000000; + -fx-font-family: "Monospaced", "Courier New"; +} + +.background { + -fx-background-color: #000000; + -fx-background-image: url("/images/glass-walpaper-casono.png"); + -fx-background-size: cover; + /* Centers the image and prevents it from repeating in a tile-like pattern. */ + -fx-background-position: center center; + -fx-background-repeat: no-repeat; +} + +.info-text { + -fx-text-fill: #ffffff; + -fx-font-size: 16px; +} + +.casino-table { + -fx-background-color: + linear-gradient(to bottom right, + rgba(255, 255, 255, 0.18), + rgba(255, 255, 255, 0.08)); + -fx-background-radius: 210; + -fx-border-radius: 200; + -fx-border-color: rgba(255, 255, 255, 0.25); + -fx-border-width: 12; + /*-fx-min-height: 60vh;*/ + /*-fx-max-height: 60vh;*/ + /*-fx-min-width: 90vh;*/ + /*-fx-max-width: 90vh;*/ + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.30), 12, 0, 0, 4); +} + +.dealer-box { + -fx-background-color: + linear-gradient(to bottom right, + rgba(255, 255, 255, 0.38), + rgba(255, 255, 255, 0.18)); + + -fx-border-color: rgba(255, 255, 255, 0.40); + -fx-border-width: 3; + -fx-background-radius: 28; + -fx-border-radius: 0; + -fx-min-width: 70; + -fx-min-height: 100; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.35), 10, 0, 0, 3); +} + +.table-title { + -fx-text-fill: #ffffff; + -fx-font-size: 28px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255), 0, 0, 3, 3);*/ +} + +.taskbar { + -fx-background-color: + linear-gradient(to bottom right, + rgba(255, 255, 255, 0.38), + rgba(255, 255, 255, 0.18)); + + -fx-background-radius: 28; + -fx-border-radius: 28; + -fx-border-color: rgba(255, 255, 255, 0.40); + -fx-border-width: 3; + -fx-padding: 14 30; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.30), 10, 0, 0, 3); + + -fx-cursor: move; +} + +.taskbar:hover { + -fx-background-color: + linear-gradient(to bottom right, + rgba(255, 255, 255, 0.44), + rgba(255, 255, 255, 0.22)); + + -fx-border-color: + rgba(255, 255, 255, 0.55); +} + +.taskbar.player-active-turn { + /* Active turn should only change the shadow to avoid flicker/transparency jumps. */ + -fx-border-color: rgba(184, 134, 11, 0.40); + -fx-effect: dropshadow(one-pass-box, rgba(184, 134, 11, 0.30), 10, 0, 0, 3); +} + +.black-input-field { + -fx-background-color: + linear-gradient(to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18)) !important; + -fx-text-fill: #ffffff !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-background-radius: 8 !important; + -fx-border-radius: 8 !important; + -fx-border-color: rgba(255, 255, 255, 0.40) !important; + -fx-border-width: 2 !important; + -fx-padding: 5 12 !important; +} + +.black-input-field:hover { + -fx-background-color: + linear-gradient(to bottom right, + rgba(0, 0, 0, 0.44), + rgba(0, 0, 0, 0.22)) !important; +} + +.black-input-field:focused { + -fx-border-color: rgba(255, 255, 255, 0.55) !important; + /*-fx-background-color: #000000;*/ +} + +.gray-input-field { + -fx-background-color: + linear-gradient(to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18)) !important; + -fx-text-fill: #ffffff !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-background-radius: 8 !important; + -fx-border-radius: 8 !important; + -fx-border-color: rgba(255, 255, 255, 0.40) !important; + /*-fx-color-color: #000000;*/ + -fx-border-width: 2 !important; + -fx-padding: 5 12 !important; +} + +.gray-input-field:hover { + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important; +} + +.gray-input-field:focused { + -fx-border-color: rgba(255, 255, 255, 0.55) !important; + /*-fx-background-color: #333333;*/ +} + +.yellow-input-field { + -fx-background-color: + linear-gradient(to bottom right, + rgba(184, 134, 11, 0.38), + rgba(184, 134, 11, 0.18)) !important; + -fx-text-fill: #ffffff !important; + -fx-prompt-text-fill: #ffffff !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-background-radius: 8 !important; + -fx-border-radius: 8 !important; + -fx-border-color: rgba(255, 215, 0, 0.40) !important; + -fx-border-width: 2 !important; + -fx-padding: 5 12 !important; +} + +.yellow-input-field:hover { + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important; +} + +/*.yellow-input-field:focused {*/ +/* -fx-border-color: #ffd700;*/ +/* -fx-background-color: #b8860b;*/ +/*}*/ + +.red-input-field { + -fx-background-color: + linear-gradient(to bottom right, + rgba(139, 0, 0, 0.38), + rgba(139, 0, 0, 0.18)) !important; + -fx-text-fill: #ffffff !important; + -fx-prompt-text-fill: #ffffff !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-background-radius: 8 !important; + -fx-border-radius: 8 !important; + -fx-border-color: rgba(255, 68, 68, 0.40) !important; + -fx-border-width: 2 !important; + -fx-padding: 5 12 !important; +} + +.red-input-field:hover { + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important; +} + +/*.red-input-field:focused {*/ +/* -fx-border-color: #ff4444;*/ +/* -fx-background-color: #8b0000;*/ +/*}*/ + +.yellow-button { + -fx-background-color: + linear-gradient(to bottom right, + rgba(184, 134, 11, 0.38), + rgba(184, 134, 11, 0.18)) !important; + -fx-border-color: rgba(255, 215, 0, 0.40) !important; + -fx-text-fill: #ffffff !important; +} + +.red-button { + -fx-background-color: + linear-gradient(to bottom right, + rgba(139, 0, 0, 0.38), + rgba(139, 0, 0, 0.18)) !important; + -fx-border-color: rgba(255, 68, 68, 0.40) !important; + -fx-text-fill: #ffffff !important; +} + +.gray-button { + -fx-background-color: + linear-gradient(to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18)) !important; + -fx-border-color: rgba(51, 51, 51, 0.40) !important; + -fx-text-fill: #ffffff !important; +} + +.gray-button .label { + -fx-text-fill: #ffffff !important; +} + +.green-button { + -fx-background-color: + linear-gradient(to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18)) !important; + -fx-border-color: rgba(51, 51, 51, 0.40) !important; + -fx-text-fill: #ffffff !important; +} + +.gray-button, +.yellow-button, +.red-button, +.green-button { + -fx-background-radius: 12 !important; + -fx-border-radius: 12 !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-border-width: 2 !important; + -fx-padding: 6 15 !important; + -fx-cursor: hand !important; +} + +.gray-button:hover, +.yellow-button:hover, +.red-button:hover, +.green-button:hover { + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important; +} + +.community-cards-box { + -fx-alignment: center !important; + -fx-spacing: 12 !important; + -fx-padding: 15 !important; + -fx-background-color: transparent !important; +} + +.community-card { + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important; + + -fx-alignment: center; + -fx-spacing: 12; + -fx-padding: 20; + -fx-background-color: linear-gradient(to bottom, #0b3d2e, #06261d); + -fx-background-radius: 16; + -fx-scale-x: 1; + -fx-scale-y: 1; + -fx-translate-y: 0; + -fx-opacity: 1; +} + +.community-card:hover {} + +.player-card { + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important; + + -fx-alignment: center; + -fx-spacing: 12; + -fx-padding: 20; + -fx-background-color: linear-gradient(to bottom, #0b3d2e, #06261d); + -fx-background-radius: 16; + -fx-scale-x: 1; + -fx-scale-y: 1; + -fx-translate-y: 0; + -fx-opacity: 1; +} + +.player-card:hover {} + +.player-card.player-cards-active-turn { + -fx-effect: dropshadow(one-pass-box, rgba(184, 134, 11, 0.30), 10, 0, 0, 3); +} + +.dealer-box.player-cards-active-turn { + -fx-effect: dropshadow(one-pass-box, rgba(184, 134, 11, 0.30), 10, 0, 0, 3); +} + +/*.dealer-box:hover {*/ +/* -fx-translate-y: -3;*/ +/*}*/ + +.chips-icon { + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important; +} + +/*.chips-icon:hover {*/ +/* -fx-translate-y: -3;*/ +/*}*/ + +.dealer-icon { + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important; +} + +.player-cards-box { + -fx-padding: 10; +} + +/*.player-cards-box.player-cards-active-turn {*/ +/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/ +/*}*/ + +.money-value { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/ +} + +.pot-title { + -fx-text-fill: #ffffff; + -fx-font-size: 14px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);*/ +} + +.pot-box { + -fx-padding: 5; +} + +.player-status-pane { + -fx-background-color: transparent; + -fx-background-radius: 15; + -fx-border-radius: 15; + /* -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; +-fx-border-width: 2; +-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.5), 10, 0, 0, 4);*/ +} + +.player-status-pane:hover {} + +.status-inner-box-top { + -fx-background-color: + linear-gradient(to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18)) !important; + -fx-background-radius: 19; + -fx-border-radius: 15; + -fx-padding: 5 10; + -fx-border-color: rgba(255, 255, 255, 0.40); + -fx-border-width: 3; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.30), 10, 0, 0, 3); +} + +.status-inner-box-midle { + -fx-background-color: rgba(13, 158, 59, 0); +} + +.status-inner-box-bottom { + -fx-background-color: + linear-gradient(to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18)) !important; + -fx-background-radius: 19; + -fx-border-radius: 15; + -fx-padding: 5 10; + -fx-border-color: rgba(255, 255, 255, 0.40); + -fx-border-width: 3; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.30), 10, 0, 0, 3); +} + +.status-inner-box-top.player-status-active-turn { + -fx-effect: dropshadow(one-pass-box, rgba(184, 134, 11, 0.30), 10, 0, 0, 3); +} + +.status-inner-box-bottom.player-status-active-turn { + -fx-effect: dropshadow(one-pass-box, rgba(184, 134, 11, 0.30), 10, 0, 0, 3); +} + +.status-label-small { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/ +} + +.status-value-text { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/ +} + +.status-value-money { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/ +} + +.status-circle { + -fx-background-color: + linear-gradient(to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18)) !important; + -fx-background-radius: 50; + -fx-border-color: rgba(255, 255, 255, 0.40); + -fx-border-width: 1; + -fx-border-radius: 50; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.30), 10, 0, 0, 3); +} + +.browser-root { + -fx-background-image: url("/images/background.png"); + -fx-background-size: cover; + -fx-background-position: center; + -fx-background-color: #0b2d15; +} + +.browser-web-view { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-padding: 0; + -fx-cursor: default; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.30), 10, 0, 0, 3); +} + +.web-view { + -fx-background-color: transparent; +} + +.taskbar-browser { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-padding: 10 25; + -fx-cursor: default; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.30), 10, 0, 0, 3); +} + +/*.taskbar-browser:hover {*/ +/* -fx-border-color: #ffffff;*/ +/*}*/ + +.security-label { + -fx-text-fill: #000000; + -fx-font-weight: bold; + -fx-font-size: 16px; +} + +.context-menu { + -fx-background-color: + linear-gradient(to bottom right, + rgba(51, 51, 51, 0.38), + rgba(51, 51, 51, 0.18)) !important; + -fx-text-fill: #ffffff !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-background-radius: 8 !important; + -fx-border-radius: 8 !important; + -fx-border-color: rgba(51, 51, 51, 0.40) !important; + -fx-color-color: #cccccc !important; + -fx-border-width: 2 !important; + -fx-padding: 5 12 !important; +} + +.context-menu .menu-item { + -fx-background-color: + linear-gradient(to bottom right, + rgba(51, 51, 51, 0.38), + rgba(51, 51, 51, 0.18)) !important; +} + +.context-menu .menu-item .label { + -fx-text-fill: #ffffff !important; + -fx-font-size: 13px !important; +} + +/*.context-menu .menu-item:focused {*/ +/* -fx-background-color: #333333;*/ +/*}*/ + +.context-menu .menu-item:hover .label, +.context-menu .menu-item:focused .label { + /*-fx-text-fill: #b8860b;*/ + -fx-font-size: 15px !important; +} + +.notebook { + -fx-background-color: + linear-gradient(to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18)); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: rgba(255, 255, 255, 0.40); + -fx-border-width: 3; + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important; + /*-fx-cursor: move;*/ + -fx-padding: 0 0 15 0; +} + +.notebook:hover { + -fx-background-color: + linear-gradient(to bottom right, + rgba(0, 0, 0, 0.44), + rgba(0, 0, 0, 0.22)); + + -fx-border-color: + rgba(255, 255, 255, 0.55); +} + +/*.notebook.player-active-turn {*/ +/* !* active turn should only change the shadow to avoid flicker/transparency jumps. *!*/ +/* -fx-border-color:*/ +/* rgba(184, 134, 11, 0.55);*/ +/* -fx-effect:*/ +/* dropshadow(*/ +/* gaussian,*/ +/* rgba(184, 134, 11, 0.45),*/ +/* 40,*/ +/* 0.2,*/ +/* 0,*/ +/* 12*/ +/* );*/ +/*}*/ + +.notebook-header { + -fx-background-color: transparent !important; + -fx-background-radius: 25 25 0 0; + -fx-padding: 8 10; + -fx-cursor: move; +} + +.notebook-title { + -fx-text-fill: #ffffff; + -fx-background-color: transparent !important; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, rgb(255, 255, 255), 0, 0, 3, 3);*/ +} + +.notebook-content { + -fx-background-color: transparent; + -fx-padding: 0; +} + +.notebook-content>.viewport { + -fx-background-color: transparent !important; + -fx-background-radius: 0 0 25 25; +} + + +.scroll-pane { + -fx-background-color: transparent; + -fx-border-color: transparent; +} + +.scroll-pane .scroll-bar:vertical, +.scroll-pane .scroll-bar:horizontal { + -fx-background-color: transparent; +} + +.scroll-pane .scroll-bar .track { + -fx-background-color: transparent !important; + -fx-background-radius: 5; +} + +.scroll-pane .scroll-bar .thumb { + -fx-background-color: + linear-gradient(to bottom right, + rgba(200, 200, 200, 0.38), + rgba(200, 200, 200, 0.18)) !important; + -fx-background-radius: 5; +} + +.scroll-pane .scroll-bar .thumb:hover { + -fx-background-color: + linear-gradient(to bottom right, + rgba(255, 255, 255, 0.38), + rgba(255, 255, 255, 0.18)) !important; +} + +.notebook-tips-list { + -fx-padding: 10; + -fx-spacing: 10; + -fx-background-color: transparent; +} + +.notebook-tips-list .text-flow { + -fx-padding: 12; + -fx-background-color: + linear-gradient(to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18)); + -fx-background-radius: 15; +} + +.settings-box { + -fx-background-color: + linear-gradient(to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18)); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: rgba(255, 255, 255, 0.40); + -fx-border-width: 3; + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important; + /*-fx-cursor: move;*/ + -fx-padding: 0 0 15 0; +} + +.settings-box:hover { + -fx-background-color: + linear-gradient(to bottom right, + rgba(0, 0, 0, 0.44), + rgba(0, 0, 0, 0.22)); + + -fx-border-color: + rgba(255, 255, 255, 0.55); +} + +/*.settings-box.player-active-turn {*/ +/* !* Active turn should only change the shadow to avoid flicker/transparency jumps. *!*/ +/* -fx-border-color:*/ +/* rgba(184, 134, 11, 0.55);*/ +/* -fx-effect:*/ +/* dropshadow(*/ +/* gaussian,*/ +/* rgba(184, 134, 11, 0.45),*/ +/* 40,*/ +/* 0.2,*/ +/* 0,*/ +/* 12*/ +/* );*/ +/*}*/ + +.settings-header { + -fx-background-color: transparent; + -fx-background-radius: 20 20 0 0; + -fx-padding: 10 15; + -fx-cursor: move; + -fx-border-width: 0 0 1 0; +} + +.settings-label { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + /*-fx-effect: dropshadow(one-pass-box, rgb(255, 255, 255), 0, 0, 3, 3);*/ +} + +.settings-content { + -fx-padding: 15 20 20 20; + -fx-spacing: 12; +} + +.settings-content { + -fx-background-color: transparent; + -fx-text-fill: #ffffff; + -fx-font-size: 13px; +} + +.radio-button { + -fx-text-fill: #ffffff; + -fx-font-size: 13px; + + -fx-padding: 6 8; + -fx-background-radius: 10; +} + +.radio-button:hover { + -fx-font-size: 15px; +} + +.radio-button:selected { + -fx-text-fill: #ffffff; +} + +.radio-button .radio { + -fx-background-color: + linear-gradient(to bottom right, + rgba(184, 134, 11, 0.38), + rgba(184, 134, 11, 0.18)); + /*-fx-border-width: 3;*/ + /*-fx-border-color: #b8860b;*/ + /*-fx-border-radius: 50%;*/ +} + +.radio-button:selected .radio { + -fx-background-color: + linear-gradient(to bottom right, + rgba(184, 134, 11, 0.38), + rgba(184, 134, 11, 0.18)); + -fx-effect: dropshadow(one-pass-box, rgba(184, 134, 11, 0.30), 10, 0, 0, 3); +} + +.settings-box { + -fx-background-insets: 0; +} + +.settings-content { + -fx-background-color: transparent; +} + +/*chat box css*/ + +.chat-box { + -fx-background-color: + linear-gradient(to bottom right, + rgba(255, 255, 255, 0.18), + rgba(255, 255, 255, 0.08)) !important; + -fx-background-radius: 58; + -fx-border-radius: 50; + -fx-border-color: rgba(255, 255, 255, 0.25) !important; + -fx-border-width: 12; + -fx-padding: 20; + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.20), 10, 0, 0, 3) !important; +} + +.tab-chat-box { + -fx-background-color: transparent !important; + -fx-background-radius: 23 !important; + -fx-border-radius: 20 !important; + -fx-border-color: rgba(255, 255, 255, 0.40) !important; + -fx-border-width: 3 !important; + -fx-padding: 20; +} + +.chat-header { + -fx-text-fill: #ffffff !important; + -fx-font-size: 22px; + -fx-font-weight: bold; + -fx-padding: 0 0 10 0; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 0, 0) !important; +} + +.chat-separator { + -fx-background-color: transparent !important; + -fx-min-height: 4px; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 0, 0) !important; +} + +.tab-header-area .tab-header-background { + -fx-background-color: transparent !important; + -fx-background-radius: 18 !important; + -fx-border-radius: 10 !important; + -fx-border-color: rgba(0, 0, 0, 0) !important; + -fx-border-width: 5; + -fx-padding: 10; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0), 0, 0, 0, 0) !important; + -fx-min-height: 10; +} + +.tab-pane .tab { + -fx-background-color: transparent !important; + -fx-background-radius: 18 !important; + -fx-border-radius: 10 !important; + -fx-border-color: rgba(255, 255, 255, 0) !important; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0), 0, 0, 0, 0) !important; + -fx-border-width: 5; + -fx-padding: 6 15; +} + +.tab-pane .tab:selected { + -fx-background-color: + linear-gradient(to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18)) !important; + -fx-background-radius: 18 !important; + -fx-border-radius: 10 !important; + -fx-border-color: rgba(255, 255, 255, 0.40) !important; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0), 0, 0, 0, 0) !important; + -fx-border-width: 3 !important; + -fx-padding: 6 15; +} + +.tab-pane .tab .tab-close-button { + -fx-text-fill: #ffffff !important; +} + +.tab-pane .tab-header-area .headers-region { + -fx-background-color: transparent !important; +} + +.tab .tab-label { + -fx-alignment: CENTER; + -fx-text-fill: #ffffff; + -fx-font-size: 12px; + -fx-font-weight: bold; +} + +.tab:selected .tab-label { + -fx-alignment: CENTER; + -fx-text-fill: #ffffff; +} + +.menu-button { + -fx-background-color: + linear-gradient(to bottom right, + rgba(0, 0, 0, 0.38), + rgba(0, 0, 0, 0.18)) !important; + -fx-border-color: rgba(51, 51, 51, 0.40) !important; + -fx-text-fill: #ffffff !important; + -fx-background-radius: 12 !important; + -fx-border-radius: 12 !important; + -fx-font-family: "Courier New" !important; + -fx-font-weight: bold !important; + -fx-border-width: 2 !important; + -fx-padding: 6 15 !important; + -fx-cursor: hand !important; +} + +.menu-button .label { + -fx-text-fill: #ffffff !important; +} + +.menu-button:hover { + -fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important; +} + +.menu-button .menu-item { + -fx-background-color: + linear-gradient(to bottom right, + rgba(51, 51, 51, 0.38), + rgba(51, 51, 51, 0.18)) !important; + -fx-border-color: rgba(51, 51, 51, 0.40) !important; + -fx-text-fill: #ffffff; +} diff --git a/src/main/resources/ui-structure/Casinogameui-parquet.css b/src/main/resources/ui-structure/Casinogameui-parquet.css new file mode 100644 index 0000000..43ee157 --- /dev/null +++ b/src/main/resources/ui-structure/Casinogameui-parquet.css @@ -0,0 +1,613 @@ +.root { + -fx-background-color: #000000; + -fx-font-family: "Monospaced", "Courier New"; /* font */ +} + +.background { + -fx-background-image: url("/images/parquet.png"); + -fx-background-size: cover; /* Image fills the entire window */ + /* Centers the image and prevents it from repeating in a tile-like pattern. */ + -fx-background-position: center center; + -fx-background-repeat: no-repeat; +} + +.info-text { + -fx-text-fill: #ffffff; + -fx-font-size: 16px; +} + +.casino-table { + -fx-background-color: #0d9e3b; + -fx-background-radius: 210; + -fx-border-radius: 200; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 12; + /*-fx-min-height: 60vh;*/ + /*-fx-max-height: 60vh;*/ + /*-fx-min-width: 90vh;*/ + /*-fx-max-width: 90vh;*/ + -fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 15, 15); +} + +.dealer-box { + -fx-background-color: #ffffff; + -fx-border-color: #000000; + -fx-border-width: 3; + -fx-background-radius: 0; + -fx-border-radius: 0; + -fx-min-width: 70; + -fx-min-height: 100; + -fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 4, 4); +} + +.table-title { + -fx-text-fill: #ffffff; + -fx-font-size: 28px; + -fx-font-weight: bold; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3); +} + +.taskbar { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-padding: 10 25; + -fx-cursor: move; + -fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5); +} + +.taskbar:hover { + -fx-border-color: #ffffff; +} + +.taskbar.player-active-turn { + /* Active turn should only change the shadow to avoid flicker/transparency jumps. */ + -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5); +} + +.black-input-field { + -fx-background-color: #1a1a1a; + -fx-text-fill: #ffffff; + -fx-font-family: "Courier New"; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-border-radius: 8; + -fx-border-color: #444444; + -fx-border-width: 2; + -fx-padding: 5 12; +} + +.black-input-field:hover { + -fx-translate-y: -3; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); +} + + +.black-input-field:focused { + -fx-border-color: #ffffff; + -fx-background-color: #000000; +} + +.gray-input-field { + -fx-background-color: #333333; + -fx-text-fill: #ffffff; + -fx-font-family: "Courier New"; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-border-radius: 8; + -fx-border-color: #666666; + -fx-color-color: #cccccc; + -fx-border-width: 2; + -fx-padding: 5 12; +} + +.gray-input-field:hover { + -fx-translate-y: -3; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); +} + +.gray-input-field:focused { + -fx-border-color: #ffffff; + -fx-background-color: #333333; +} + +.yellow-input-field { + -fx-background-color: #b8860b; + -fx-text-fill: #ffffff; + -fx-prompt-text-fill: #ffffff; + -fx-font-family: "Courier New"; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-border-radius: 8; + -fx-border-color: #ffd700; + -fx-border-width: 2; + -fx-padding: 5 12; +} + +.yellow-input-field:hover { + -fx-translate-y: -3; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); +} + +.yellow-input-field:focused { + -fx-border-color: #ffffff; + -fx-background-color: #b8860b; +} + +.red-input-field { + -fx-background-color: #8b0000; + -fx-text-fill: #ffffff; + -fx-prompt-text-fill: #ffffff; + -fx-font-family: "Courier New"; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-border-radius: 8; + -fx-border-color: #ff4444; + -fx-border-width: 2; + -fx-padding: 5 12; +} + +.red-input-field:hover { + -fx-translate-y: -3; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); +} + +.red-input-field:focused { + -fx-border-color: #ffffff; + -fx-background-color: #8b0000; +} + +.yellow-button { + -fx-background-color: #b8860b; + -fx-border-color: #ffd700; + -fx-text-fill: #ffffff; +} + +.red-button { + -fx-background-color: #8b0000; + -fx-border-color: #ff4444; + -fx-text-fill: #ffffff; +} + +.gray-button { + -fx-background-color: #333333; + -fx-border-color: #666666; + /*-fx-text-fill: #cccccc;*/ + -fx-text-fill: #ffffff; +} + +.gray-button .label { + -fx-text-fill: #ffffff; +} + +.green-button { + -fx-background-color: #144523; + -fx-border-color: #0d9e3b; + -fx-text-fill: #ffffff; +} + +.gray-button, .yellow-button, .red-button, .green-button { + -fx-background-radius: 12; + -fx-border-radius: 12; + -fx-font-family: "Courier New"; + -fx-font-weight: bold; + -fx-border-width: 2; + -fx-padding: 6 15; + -fx-cursor: hand; +} + +.gray-button:hover, .yellow-button:hover, .red-button:hover, .green-button:hover { + -fx-translate-y: -3; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); +} + +.community-cards-box { + -fx-alignment: center; + -fx-spacing: 12; + -fx-padding: 15; + -fx-background-color: transparent; +} + +.community-card { + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); + + -fx-alignment: center; + -fx-spacing: 12; + -fx-padding: 20; + -fx-background-color: linear-gradient(to bottom, #0b3d2e, #06261d); + -fx-background-radius: 16; + -fx-scale-x: 1; + -fx-scale-y: 1; + -fx-translate-y: 0; + -fx-opacity: 1; +} + +/*.community-card:hover {*/ +/* -fx-translate-y: -3;*/ +/*}*/ + +.player-card { + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); + + -fx-alignment: center; + -fx-spacing: 12; + -fx-padding: 20; + -fx-background-color: linear-gradient(to bottom, #0b3d2e, #06261d); + -fx-background-radius: 16; + -fx-scale-x: 1; + -fx-scale-y: 1; + -fx-translate-y: 0; + -fx-opacity: 1; +} + +/*.player-card:hover {*/ +/* -fx-translate-y: -3;*/ +/*}*/ + +.player-card.player-cards-active-turn { + -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5); +} + +.dealer-box.player-cards-active-turn { + -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5); +} + +/*.dealer-box:hover {*/ +/* -fx-translate-y: -3;*/ +/*}*/ + +.chips-icon { + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); +} + +/*.chips-icon:hover {*/ +/* -fx-translate-y: -3;*/ +/*}*/ + +.dealer-icon { + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); +} + +.player-cards-box { + -fx-padding: 10; +} + +/*.player-cards-box.player-cards-active-turn {*/ +/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/ +/*}*/ + +.money-value { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2); +} + +.pot-title { + -fx-text-fill: #ffffff; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3); +} + +.pot-box { + -fx-padding: 5; +} + +.player-status-pane { + -fx-background-color: rgba(13, 158, 59, 0); + -fx-background-radius: 15; + -fx-border-radius: 15; + /* -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 2; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.5), 10, 0, 0, 4);*/ +} + +.player-status-pane:hover { + -fx-translate-y: -3; +} + +.status-inner-box-top { + -fx-background-color: rgba(13, 158, 59); + -fx-background-radius: 19; + -fx-border-radius: 15; + -fx-padding: 5 10; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); +} + +.status-inner-box-midle { + -fx-background-color: rgba(13, 158, 59, 0); +} + +.status-inner-box-bottom { + -fx-background-color: rgba(13, 158, 59); + -fx-background-radius: 19; + -fx-border-radius: 15; + -fx-padding: 5 10; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); +} + +.status-inner-box-top.player-status-active-turn { + -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5); +} + +.status-inner-box-bottom.player-status-active-turn { + -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5); +} + +.status-label-small { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2); +} + +.status-value-text { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2); +} + +.status-value-money { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2); +} + +.status-circle { + -fx-background-color: #000000; + -fx-background-radius: 50; + -fx-border-color: #ffffff; + -fx-border-width: 1; + -fx-border-radius: 50; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); +} + +.browser-root { + -fx-background-image: url("/images/background.png"); + -fx-background-size: cover; + -fx-background-position: center; + -fx-background-color: #0b2d15; +} + +.browser-web-view { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-padding: 0; + -fx-cursor: default; + -fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5); +} + +.web-view { + -fx-background-color: transparent; +} + +.taskbar-browser { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-padding: 10 25; + -fx-cursor: default; + -fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5); +} + +/*.taskbar-browser:hover {*/ +/* -fx-border-color: #ffffff;*/ +/*}*/ + +.security-label { + -fx-text-fill: #000000; + -fx-font-weight: bold; + -fx-font-size: 16px; +} + +.context-menu { + -fx-background-color: #333333; + -fx-text-fill: #ffffff; + -fx-font-family: "Courier New"; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-border-radius: 8; + -fx-border-color: #333333; + -fx-color-color: #cccccc; + -fx-border-width: 2; + -fx-padding: 5 12; +} + +.context-menu .menu-item { + -fx-background-color: #333333; +} + +.context-menu .menu-item .label { + -fx-text-fill: #ffffff; + -fx-font-size: 13px; +} + +/*.context-menu .menu-item:focused {*/ +/* -fx-background-color: #333333;*/ +/*}*/ + +.context-menu .menu-item:hover .label, +.context-menu .menu-item:focused .label { + /*-fx-text-fill: #b8860b;*/ + -fx-font-size: 15px; +} + +.notebook { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 5, 5); + /*-fx-cursor: move;*/ + + -fx-padding: 0 0 15 0; +} + +.notebook:hover { + -fx-border-color: #ffffff; +} + +/*.notebook.player-active-turn {*/ +/* !* Active turn should only change the shadow to avoid flicker/transparency jumps. *!*/ +/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/ +/*}*/ + +.notebook-header { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 25 25 0 0; + -fx-padding: 8 10; + -fx-cursor: move; +} + +.notebook-title { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3); +} + +.notebook-content { + -fx-background-color: transparent; + -fx-padding: 0; +} + +.notebook-content > .viewport { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 0 0 25 25; +} + + +.scroll-pane { + -fx-background-color: transparent; + -fx-border-color: transparent; +} + +.scroll-pane .scroll-bar:vertical, +.scroll-pane .scroll-bar:horizontal { + -fx-background-color: transparent; +} + +.scroll-pane .scroll-bar .track { + /*-fx-background-color: #5c3d10;*/ + -fx-background-color: transparent; + -fx-background-radius: 5; +} + +.scroll-pane .scroll-bar .thumb { + -fx-background-color: #3d260a; + -fx-background-radius: 5; +} + +.scroll-pane .scroll-bar .thumb:hover { + -fx-background-color: #2a1b07; +} + +.notebook-tips-list { + -fx-padding: 10; + -fx-spacing: 10; + -fx-background-color: transparent; +} + +.notebook-tips-list .text-flow { + -fx-padding: 12; + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 15; +} + +.settings-box { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 5, 5); + /*-fx-cursor: move;*/ + -fx-padding: 0 0 15 0; +} + +.settings-box:hover { + -fx-border-color: #ffffff; +} + +/*.settings-box.player-active-turn {*/ +/* !* Active turn should only change the shadow to avoid flicker/transparency jumps. *!*/ +/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/ +/*}*/ + +.settings-header { + -fx-background-color: transparent; + -fx-background-radius: 20 20 0 0; + -fx-padding: 10 15; + -fx-cursor: move; + -fx-border-width: 0 0 1 0; +} + +.settings-label { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3); +} + +.settings-content { + -fx-padding: 15 20 20 20; + -fx-spacing: 12; +} + +.settings-content { + -fx-background-color: transparent; + -fx-text-fill: #e8e8e8; + -fx-font-size: 13px; +} + +.radio-button { + -fx-text-fill: #ffffff; + -fx-font-size: 13px; + + -fx-padding: 6 8; + -fx-background-radius: 10; +} + +.radio-button:hover { + -fx-font-size: 15px; +} + +.radio-button:selected { + /*-fx-text-fill: #b8860b;*/ + -fx-text-fill: #ffffff; +} + +.radio-button .radio { + -fx-background-color: #b8860b; + /*-fx-border-width: 3;*/ + /*-fx-border-color: #b8860b;*/ + /*-fx-border-radius: 50%;*/ +} + +.radio-button:selected .radio { + -fx-background-color: #b8860b; + -fx-effect: dropshadow(gaussian, rgba(184,134,11,0.8), 6, 0.3, 0, 0); +} + +.settings-box { + -fx-background-insets: 0; +} + +.settings-content { + -fx-background-color: transparent; +} diff --git a/src/main/resources/ui-structure/Casinogameui.css b/src/main/resources/ui-structure/Casinogameui.css index 4ac89cc..5d133bf 100644 --- a/src/main/resources/ui-structure/Casinogameui.css +++ b/src/main/resources/ui-structure/Casinogameui.css @@ -174,7 +174,12 @@ .gray-button { -fx-background-color: #333333; -fx-border-color: #666666; - -fx-text-fill: #cccccc; + /*-fx-text-fill: #cccccc;*/ + -fx-text-fill: #ffffff; +} + +.gray-button .label { + -fx-text-fill: #ffffff; } .green-button { @@ -219,6 +224,10 @@ -fx-opacity: 1; } +/*.community-card:hover {*/ +/* -fx-translate-y: -3;*/ +/*}*/ + .player-card { -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); @@ -233,6 +242,10 @@ -fx-opacity: 1; } +/*.player-card:hover {*/ +/* -fx-translate-y: -3;*/ +/*}*/ + .player-card.player-cards-active-turn { -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5); } @@ -241,10 +254,17 @@ -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5); } +/*.dealer-box:hover {*/ +/* -fx-translate-y: -3;*/ +/*}*/ + .chips-icon { -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); } +/*.chips-icon:hover {*/ +/* -fx-translate-y: -3;*/ +/*}*/ .dealer-icon { -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); @@ -266,9 +286,10 @@ } .pot-title { - -fx-text-fill: gold; + -fx-text-fill: #ffffff; -fx-font-size: 14px; -fx-font-weight: bold; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3); } .pot-box { @@ -410,6 +431,183 @@ -fx-background-color: #333333; } -.context-menu .menu-item:focused { - -fx-background-color: #333333; +.context-menu .menu-item .label { + -fx-text-fill: #ffffff; + -fx-font-size: 13px; +} + +/*.context-menu .menu-item:focused {*/ +/* -fx-background-color: #333333;*/ +/*}*/ + +.context-menu .menu-item:hover .label, +.context-menu .menu-item:focused .label { + /*-fx-text-fill: #b8860b;*/ + -fx-font-size: 15px; +} + +.notebook { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 5, 5); + /*-fx-cursor: move;*/ + + -fx-padding: 0 0 15 0; +} + +.notebook:hover { + -fx-border-color: #ffffff; +} + +/*.notebook.player-active-turn {*/ +/* !* Active turn should only change the shadow to avoid flicker/transparency jumps. *!*/ +/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/ +/*}*/ + +.notebook-header { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 25 25 0 0; + -fx-padding: 8 10; + -fx-cursor: move; +} + +.notebook-title { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3); +} + +.notebook-content { + -fx-background-color: transparent; + -fx-padding: 0; +} + +.notebook-content > .viewport { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 0 0 25 25; +} + + +.scroll-pane { + -fx-background-color: transparent; + -fx-border-color: transparent; +} + +.scroll-pane .scroll-bar:vertical, +.scroll-pane .scroll-bar:horizontal { + -fx-background-color: transparent; +} + +.scroll-pane .scroll-bar .track { + /*-fx-background-color: #5c3d10;*/ + -fx-background-color: transparent; + -fx-background-radius: 5; +} + +.scroll-pane .scroll-bar .thumb { + -fx-background-color: #3d260a; + -fx-background-radius: 5; +} + +.scroll-pane .scroll-bar .thumb:hover { + -fx-background-color: #2a1b07; +} + +.notebook-tips-list { + -fx-padding: 10; + -fx-spacing: 10; + -fx-background-color: transparent; +} + +.notebook-tips-list .text-flow { + -fx-padding: 12; + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 15; +} + +.settings-box { + -fx-background-color: rgb(13, 158, 59); + -fx-background-radius: 23; + -fx-border-radius: 20; + -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; + -fx-border-width: 3; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 5, 5); + /*-fx-cursor: move;*/ + -fx-padding: 0 0 15 0; +} + +.settings-box:hover { + -fx-border-color: #ffffff; +} + +/*.settings-box.player-active-turn {*/ +/* !* Active turn should only change the shadow to avoid flicker/transparency jumps. *!*/ +/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/ +/*}*/ + +.settings-header { + -fx-background-color: transparent; + -fx-background-radius: 20 20 0 0; + -fx-padding: 10 15; + -fx-cursor: move; + -fx-border-width: 0 0 1 0; +} + +.settings-label { + -fx-text-fill: #ffffff; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3); +} + +.settings-content { + -fx-padding: 15 20 20 20; + -fx-spacing: 12; +} + +.settings-content { + -fx-background-color: transparent; + -fx-text-fill: #e8e8e8; + -fx-font-size: 13px; +} + +.radio-button { + -fx-text-fill: #ffffff; + -fx-font-size: 13px; + + -fx-padding: 6 8; + -fx-background-radius: 10; +} + +.radio-button:hover { + -fx-font-size: 15px; +} + +.radio-button:selected { + /*-fx-text-fill: #b8860b;*/ + -fx-text-fill: #ffffff; +} + +.radio-button .radio { + -fx-background-color: #b8860b; + /*-fx-border-width: 3;*/ + /*-fx-border-color: #b8860b;*/ + /*-fx-border-radius: 50%;*/ +} + +.radio-button:selected .radio { + -fx-background-color: #b8860b; + -fx-effect: dropshadow(gaussian, rgba(184,134,11,0.8), 6, 0.3, 0, 0); +} + +.settings-box { + -fx-background-insets: 0; +} + +.settings-content { + -fx-background-color: transparent; } diff --git a/src/main/resources/ui-structure/Casinogameui.fxml b/src/main/resources/ui-structure/Casinogameui.fxml index e225aa1..f8269ec 100644 --- a/src/main/resources/ui-structure/Casinogameui.fxml +++ b/src/main/resources/ui-structure/Casinogameui.fxml @@ -7,11 +7,11 @@ - + styleClass="background"> @@ -41,6 +41,8 @@ alignment="CENTER" styleClass="casino-table" spacing="20" + mouseTransparent="false" + pickOnBounds="false" maxWidth="Infinity" maxHeight="Infinity"> @@ -67,7 +69,7 @@ - + + + + + + + + + + + diff --git a/src/main/resources/ui-structure/Casinomainui.fxml b/src/main/resources/ui-structure/Casinomainui.fxml index b5df10b..0b734ee 100644 --- a/src/main/resources/ui-structure/Casinomainui.fxml +++ b/src/main/resources/ui-structure/Casinomainui.fxml @@ -4,7 +4,7 @@ - + + + + + + + + + +