Merge branch 'main' into feat/140-chat-messages
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
|
||||
@@ -18,4 +18,6 @@ public class GameState {
|
||||
public int dealer;
|
||||
public int activePlayer;
|
||||
public int winnerIndex = -1;
|
||||
public List<String> winnerNames = new ArrayList<>();
|
||||
public int potPerWinner;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String> fetchLobbyPlayerNames(int lobbyId) {
|
||||
List<String> lines = client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId);
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
|
||||
List<String> 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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("'", " ");
|
||||
|
||||
@@ -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<String> lastCommunityKeys = java.util.List.of();
|
||||
private java.util.List<String> 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<String> 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<String> oldKeys, java.util.List<String> 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<String> latestNames = lobbyClient.fetchLobbyPlayerNames(chatLobbyId);
|
||||
if (latestNames == null) {
|
||||
latestNames = List.of();
|
||||
}
|
||||
if (latestNames.equals(lobbyPlayerNames)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> 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<Card> getMyCards(List<Player> 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<Player> buildOpponents(List<Player> players) {
|
||||
java.util.List<Player> 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<Player> fallback = new java.util.ArrayList<>();
|
||||
for (Player player : players) {
|
||||
if (player != null) {
|
||||
fallback.add(player);
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return opponents;
|
||||
java.util.List<Player> 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<Player> 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.
|
||||
*
|
||||
* <p>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<String> 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<String> 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<String> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<String> URL_SUGGESTIONS =
|
||||
FXCollections.observableArrayList(TRUSTED_DOMAINS);
|
||||
|
||||
@@ -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<String> highscoreList;
|
||||
@FXML private Label statusLabel;
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<String> 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<String> 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));
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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()) {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<LeaveLobbyRequest>)
|
||||
new LeaveLobbyHandler(
|
||||
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||
|
||||
// START_GAME registration
|
||||
parserDispatcher.register("START_GAME", new StartGameParser());
|
||||
commandRouter.register(
|
||||
|
||||
@@ -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<ChangeUsernameRequest>
|
||||
public void execute(ChangeUsernameRequest request) {
|
||||
Optional<User> 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<ChangeUsernameRequest>
|
||||
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<ChangeUsernameRequest>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> 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<String> 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<String> uniqueWinners = new LinkedHashSet<>(winnerNames);
|
||||
List<String> 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<String> computeWinnerNames(GameState state, GameController game) {
|
||||
GamePhase phase = state.getPhase();
|
||||
if (phase != GamePhase.SHOWDOWN && phase != GamePhase.FINISHED) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
CardsSpeakRule showdown = new CardsSpeakRule();
|
||||
List<Player> winners = showdown.determineWinners(state);
|
||||
|
||||
List<String> 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()) {
|
||||
|
||||
@@ -23,7 +23,8 @@ import org.apache.logging.log4j.Logger;
|
||||
public class JoinLobbyHandler extends CommandHandler<JoinLobbyRequest> {
|
||||
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}.
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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<LeaveLobbyRequest> {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Expected request parameters:
|
||||
*
|
||||
* <ul>
|
||||
* <li>`ID` (int) — numeric lobby identifier (required)
|
||||
* </ul>
|
||||
*
|
||||
* <p>The parser builds a {@link LeaveLobbyRequest} containing the parsed lobby id and the original
|
||||
* request context.
|
||||
*/
|
||||
public class LeaveLobbyParser implements CommandParser<LeaveLobbyRequest> {
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Integer, Long> rankCount) {
|
||||
private static HandRank checkFourOfAKind(Map<Integer, Long> rankCount, List<Integer> allRanks) {
|
||||
if (!rankCount.containsValue(FOUR_OF_A_KIND_COUNT)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int quad = getRank(rankCount, FOUR_OF_A_KIND);
|
||||
List<Integer> kickers = getKickers(allRanks, List.of(quad), ONE_KICKER);
|
||||
|
||||
return new HandRank(HandRank.Type.FOUR_OF_A_KIND, List.of(quad));
|
||||
List<Integer> 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<Integer, Long> rankCount) {
|
||||
private static HandRank checkThreeOfAKind(
|
||||
Map<Integer, Long> rankCount, List<Integer> allRanks) {
|
||||
if (!rankCount.containsValue(THREE_OF_A_KIND_COUNT)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int tripsRank = getRank(rankCount, THREE_OF_A_KIND_RANK);
|
||||
List<Integer> kickers = getKickers(allRanks, List.of(tripsRank), TWO_KICKERS);
|
||||
|
||||
return new HandRank(HandRank.Type.THREE_OF_A_KIND, List.of(tripsRank));
|
||||
List<Integer> 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<Integer, Long> rankCount) {
|
||||
private static HandRank checkTwoPair(Map<Integer, Long> rankCount, List<Integer> 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<Integer> kickers = getKickers(allRanks, pairRanks, ONE_KICKER);
|
||||
|
||||
List<Integer> 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<Integer, Long> rankCount) {
|
||||
private static HandRank checkOnePair(Map<Integer, Long> rankCount, List<Integer> allRanks) {
|
||||
|
||||
long pairCount = rankCount.values().stream().filter(v -> v == PAIR).count();
|
||||
|
||||
@@ -313,8 +339,12 @@ public class HandEvaluator {
|
||||
}
|
||||
|
||||
int pair = getRank(rankCount, PAIR);
|
||||
List<Integer> kickers = getKickers(allRanks, List.of(pair), THREE_KICKERS);
|
||||
|
||||
return new HandRank(HandRank.Type.ONE_PAIR, List.of(pair));
|
||||
List<Integer> 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<Integer> getKickers(
|
||||
List<Integer> allRanks, List<Integer> excludeRanks, int numKickers) {
|
||||
|
||||
Set<Integer> 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.
|
||||
*
|
||||
|
||||
@@ -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<Player> 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<Player> determineWinners(GameState state) {
|
||||
|
||||
List<Player> players = new ArrayList<>(state.getPlayers());
|
||||
|
||||
Player bestPlayer = null;
|
||||
HandRank bestRank = null;
|
||||
List<Player> 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<Player> 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();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<String> playerNames = new CopyOnWriteArrayList<>();
|
||||
private final Set<String> 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<String> 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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<Lobby> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<String, AudioClip> 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);
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 4.4 MiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 5.8 MiB |
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
|
||||
|
||||
<!-- Main container: Links the UI to the CasinoGameController and loads Casinogameui.css -->
|
||||
<AnchorPane xmlns="http://javafx.com/javafx/21"
|
||||
<AnchorPane fx:id="rootPane"
|
||||
xmlns="http://javafx.com/javafx/21"
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameController"
|
||||
styleClass="background"
|
||||
stylesheets="@Casinogameui.css">
|
||||
styleClass="background">
|
||||
|
||||
<GridPane prefWidth="1200" prefHeight="800" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0">
|
||||
<columnConstraints>
|
||||
@@ -41,6 +41,8 @@
|
||||
alignment="CENTER"
|
||||
styleClass="casino-table"
|
||||
spacing="20"
|
||||
mouseTransparent="false"
|
||||
pickOnBounds="false"
|
||||
maxWidth="Infinity"
|
||||
maxHeight="Infinity">
|
||||
|
||||
@@ -67,7 +69,7 @@
|
||||
</Label>
|
||||
|
||||
<Label fx:id="tableText"
|
||||
text="Welcome! The round is about to begin."
|
||||
text="Welcome! The game starts when 4 players are present..."
|
||||
styleClass="table-title" />
|
||||
|
||||
</VBox>
|
||||
@@ -161,8 +163,18 @@
|
||||
</GridPane>
|
||||
|
||||
<!-- movable taskbar -->
|
||||
<AnchorPane>
|
||||
<AnchorPane pickOnBounds="false">
|
||||
<fx:include fx:id="taskbarInclude" source="gameuicomponents/Taskbar.fxml" GridPane.columnIndex="1"/>
|
||||
</AnchorPane>
|
||||
|
||||
<!-- movable notebook (Tips Box) -->
|
||||
<AnchorPane pickOnBounds="false">
|
||||
<fx:include fx:id="notebookInclude" source="gameuicomponents/Notebook.fxml" GridPane.columnIndex="1"/>
|
||||
</AnchorPane>
|
||||
|
||||
<!-- movable settings -->
|
||||
<AnchorPane pickOnBounds="false">
|
||||
<fx:include fx:id="settingsInclude" source="gameuicomponents/Settings.fxml" GridPane.columnIndex="1"/>
|
||||
</AnchorPane>
|
||||
|
||||
</AnchorPane>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
|
||||
<AnchorPane xmlns="http://javafx.com"
|
||||
<AnchorPane fx:id="rootPane" xmlns="http://javafx.com"
|
||||
xmlns:fx="http://javafx.com"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.CasinomainuiController"
|
||||
styleClass="anchor-pane-bg"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx/21"
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.NotebookController"
|
||||
fx:id="notebook"
|
||||
styleClass="notebook"
|
||||
spacing="0"
|
||||
layoutX="900.0" layoutY="50.0"
|
||||
prefWidth="350" prefHeight="500">
|
||||
|
||||
<HBox styleClass="notebook-header"
|
||||
onMousePressed="#onNotebookPressed"
|
||||
onMouseDragged="#onNotebookDragged"
|
||||
onMouseReleased="#onNotebookReleased"
|
||||
spacing="10"
|
||||
alignment="CENTER_LEFT">
|
||||
|
||||
<Label text="📓 TIPS"
|
||||
styleClass="notebook-title"
|
||||
HBox.hgrow="ALWAYS"/>
|
||||
|
||||
<Button fx:id="toggleButton"
|
||||
text="−"
|
||||
onAction="#onToggleNotebook"
|
||||
styleClass="red-button"
|
||||
prefWidth="40"
|
||||
prefHeight="30"/>
|
||||
|
||||
</HBox>
|
||||
|
||||
<ScrollPane fx:id="tipsContent"
|
||||
styleClass="notebook-content"
|
||||
fitToWidth="true"
|
||||
vbarPolicy="ALWAYS"
|
||||
hbarPolicy="NEVER"
|
||||
VBox.vgrow="ALWAYS">
|
||||
|
||||
<VBox fx:id="tipsList"
|
||||
styleClass="notebook-tips-list"
|
||||
spacing="8"
|
||||
prefWidth="330">
|
||||
</VBox>
|
||||
|
||||
</ScrollPane>
|
||||
|
||||
</VBox>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.shape.Circle?>
|
||||
|
||||
<!--
|
||||
This layout currently serves as a visual structure. As soon as the game engine and
|
||||
@@ -14,7 +15,7 @@ and status displays will be dynamically populated with real-time data from the s
|
||||
<VBox fx:id="playerStatusBox"
|
||||
xmlns="http://javafx.com/javafx/21"
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.PlayerStatusController"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.PlayerStatusController"
|
||||
styleClass="player-status-pane"
|
||||
prefWidth="200"
|
||||
spacing="5">
|
||||
@@ -25,7 +26,7 @@ and status displays will be dynamically populated with real-time data from the s
|
||||
<HBox fx:id="statusInnerBoxTop"
|
||||
styleClass="status-inner-box-top"
|
||||
alignment="CENTER_LEFT"
|
||||
prefHeight="40">
|
||||
prefHeight="45">
|
||||
|
||||
<padding>
|
||||
<Insets left="10"/>
|
||||
@@ -33,6 +34,7 @@ and status displays will be dynamically populated with real-time data from the s
|
||||
|
||||
<Label styleClass="status-label-small"/>
|
||||
<Label fx:id="playerName" text="-" styleClass="status-value-text"/>
|
||||
|
||||
</HBox>
|
||||
|
||||
<!-- spacer -->
|
||||
@@ -50,6 +52,7 @@ and status displays will be dynamically populated with real-time data from the s
|
||||
|
||||
<Label styleClass="status-label-small"/>
|
||||
<Label fx:id="playerMoney" text="0 $" styleClass="status-value-money"/>
|
||||
|
||||
</HBox>
|
||||
|
||||
<!-- ICON AREA -->
|
||||
@@ -61,12 +64,28 @@ and status displays will be dynamically populated with real-time data from the s
|
||||
prefWidth="40"
|
||||
prefHeight="40"/>
|
||||
|
||||
<ImageView fx:id="playerProfileImage"
|
||||
fitWidth="40"
|
||||
fitHeight="40"
|
||||
preserveRatio="true"
|
||||
smooth="true"
|
||||
layoutX="-25"
|
||||
layoutY="-125"
|
||||
styleClass="status-profile-image">
|
||||
|
||||
<clip>
|
||||
<Circle centerX="20" centerY="20" radius="20" />
|
||||
</clip>
|
||||
|
||||
</ImageView>
|
||||
|
||||
<ImageView fx:id="dealerIcon"
|
||||
fitWidth="43"
|
||||
fitHeight="43"
|
||||
layoutY="-120"
|
||||
visible="false"
|
||||
styleClass="dealer-icon"/>
|
||||
|
||||
</Pane>
|
||||
|
||||
</children>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx/21"
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.SettingsController"
|
||||
fx:id="settingsBox"
|
||||
styleClass="settings-box"
|
||||
spacing="0"
|
||||
layoutX="100.0" layoutY="100.0"
|
||||
prefWidth="300" prefHeight="250">
|
||||
|
||||
<HBox styleClass="settings-header"
|
||||
onMousePressed="#onSettingsPressed"
|
||||
onMouseDragged="#onSettingsDragged"
|
||||
onMouseReleased="#onSettingsReleased"
|
||||
spacing="10"
|
||||
alignment="CENTER_LEFT">
|
||||
|
||||
<Label text="⚙ SETTINGS"
|
||||
styleClass="settings-label"
|
||||
HBox.hgrow="ALWAYS"/>
|
||||
|
||||
<Button fx:id="toggleButton"
|
||||
text="−"
|
||||
onAction="#onToggleSettings"
|
||||
styleClass="red-button"
|
||||
prefWidth="40"
|
||||
prefHeight="30"/>
|
||||
|
||||
</HBox>
|
||||
|
||||
<VBox styleClass="settings-content"
|
||||
spacing="15"
|
||||
VBox.vgrow="ALWAYS">
|
||||
|
||||
<Label text="Theme:"
|
||||
styleClass="settings-label"/>
|
||||
|
||||
<RadioButton fx:id="themeStandard"
|
||||
text="STANDARD (DEFAULT)"
|
||||
styleClass="radio-button"/>
|
||||
|
||||
<RadioButton fx:id="themeStandardParquet"
|
||||
text="STANDARD WOODEN"
|
||||
styleClass="radio-button"/>
|
||||
|
||||
<RadioButton fx:id="themeBlackWhite"
|
||||
text="BLACK AND WHITE"
|
||||
styleClass="radio-button"/>
|
||||
|
||||
<RadioButton fx:id="themeGlass"
|
||||
text="GLASS"
|
||||
styleClass="radio-button"/>
|
||||
|
||||
<RadioButton fx:id="themeLightGlass"
|
||||
text="LIGHT GLASS"
|
||||
styleClass="radio-button"/>
|
||||
|
||||
</VBox>
|
||||
|
||||
</VBox>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<!-- movable taskbar -->
|
||||
<HBox xmlns="http://javafx.com/javafx/21"
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController"
|
||||
fx:id="taskbar"
|
||||
styleClass="taskbar"
|
||||
spacing="15"
|
||||
@@ -17,10 +17,18 @@
|
||||
layoutX="50.0" layoutY="710.0">
|
||||
|
||||
<!-- Placeholder buttons -->
|
||||
<Button text="SETTINGS" styleClass="gray-button" />
|
||||
<Button text="HIGHSCORE" onAction="#onHighscoreButtonClick" styleClass="gray-button" />
|
||||
<!-- Buten for the Casono Browser -->
|
||||
<Button text="HELP" onAction="#onBrowserButtonClick" styleClass="gray-button" />
|
||||
<Button text="SETTINGS" onAction="#onSettingsButtonClick" styleClass="gray-button" />
|
||||
<Button text="HIGHSCORE" onAction="#onHighscoreButtonClick" styleClass="gray-button" />
|
||||
|
||||
<!-- Help Button with Dropdown Menu -->
|
||||
<MenuButton text="HELP" styleClass="gray-button">
|
||||
<items>
|
||||
<MenuItem text="SHOW TIPS" onAction="#onShowTipsButtonClick" />
|
||||
<MenuItem text="CASONO BROWSER: Manual" onAction="#onBrowserButtonClick" />
|
||||
<MenuItem text="CASONO BROWSER: Wikipedia" onAction="#onBrowserButtonClickWiki" />
|
||||
<MenuItem text="CASONO BROWSER: Brave Search" onAction="#onBrowserButtonClickBrave" />
|
||||
</items>
|
||||
</MenuButton>
|
||||
|
||||
<!-- Vertical dividing line: Creates a visual separation between two button areas -->
|
||||
<Separator orientation="VERTICAL" prefHeight="20" opacity="0.3" />
|
||||
|
||||
@@ -14,10 +14,10 @@ class TaskbarControllerInputValidationTest {
|
||||
void updateLogicRunsWithoutCrash() {
|
||||
GameState state = new GameState();
|
||||
|
||||
state.players = List.of(
|
||||
new Player(PlayerId.of("me"), 20000),
|
||||
new Player(PlayerId.of("other"), 20000)
|
||||
);
|
||||
state.players =
|
||||
List.of(
|
||||
new Player(PlayerId.of("me"), 20000),
|
||||
new Player(PlayerId.of("other"), 20000));
|
||||
|
||||
state.phase = "PREFLOP";
|
||||
state.currentBet = 200;
|
||||
@@ -31,7 +31,6 @@ class TaskbarControllerInputValidationTest {
|
||||
}
|
||||
|
||||
static class DummyController {
|
||||
void update(GameState state, PlayerId id) {
|
||||
}
|
||||
void update(GameState state, PlayerId id) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ 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.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.RuleEngine;
|
||||
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 java.util.ArrayList;
|
||||
@@ -281,13 +282,9 @@ public class GameControllerTest {
|
||||
Set<String> seenCards = new HashSet<>();
|
||||
|
||||
for (List<Card> cards : playerCards.values()) {
|
||||
|
||||
for (Card c : cards) {
|
||||
|
||||
String key = c.getRank() + "-" + c.getSuit();
|
||||
|
||||
assertFalse(seenCards.contains(key), "Duplicate card found: " + key);
|
||||
|
||||
seenCards.add(key);
|
||||
}
|
||||
}
|
||||
@@ -617,4 +614,80 @@ public class GameControllerTest {
|
||||
|
||||
LOGGER.info("Test 5 was successfully completed");
|
||||
}
|
||||
|
||||
/**
|
||||
* This test simulates a scenario where two players have identical Two Pair hands at showdown.
|
||||
*/
|
||||
@Test
|
||||
public void testSplitPotWithTrueIdenticalTwoPair() {
|
||||
|
||||
GameState state = new GameState();
|
||||
|
||||
state.addPlayer(PlayerId.of("PlayerA"), 5000);
|
||||
state.addPlayer(PlayerId.of("PlayerB"), 5000);
|
||||
|
||||
state.getHoleCards(PlayerId.of("PlayerA")).add(new Card(Suit.CLUBS, Rank.TWO));
|
||||
state.getHoleCards(PlayerId.of("PlayerA")).add(new Card(Suit.DIAMONDS, Rank.THREE));
|
||||
state.getHoleCards(PlayerId.of("PlayerB")).add(new Card(Suit.HEARTS, Rank.FOUR));
|
||||
state.getHoleCards(PlayerId.of("PlayerB")).add(new Card(Suit.SPADES, Rank.FIVE));
|
||||
|
||||
state.addCommunityCard(new Card(Suit.CLUBS, Rank.QUEEN));
|
||||
state.addCommunityCard(new Card(Suit.DIAMONDS, Rank.QUEEN));
|
||||
state.addCommunityCard(new Card(Suit.SPADES, Rank.EIGHT));
|
||||
state.addCommunityCard(new Card(Suit.HEARTS, Rank.EIGHT));
|
||||
state.addCommunityCard(new Card(Suit.CLUBS, Rank.ACE));
|
||||
|
||||
List<Card> board = state.getCommunityCards();
|
||||
|
||||
List<Card> handA = new ArrayList<>(board);
|
||||
handA.addAll(state.getHoleCards(PlayerId.of("PlayerA")));
|
||||
|
||||
List<Card> handB = new ArrayList<>(board);
|
||||
handB.addAll(state.getHoleCards(PlayerId.of("PlayerB")));
|
||||
|
||||
HandRank rankA = HandEvaluator.evaluate(handA);
|
||||
HandRank rankB = HandEvaluator.evaluate(handB);
|
||||
|
||||
assertEquals(0, rankA.compareTo(rankB), "Both hands must be exactly the same");
|
||||
|
||||
CardsSpeakRule showdown = new CardsSpeakRule();
|
||||
List<Player> winners = showdown.determineWinners(state);
|
||||
|
||||
assertEquals(2, winners.size(), "There must be two winners");
|
||||
assertEquals("PlayerA", winners.get(0).getName());
|
||||
assertEquals("PlayerB", winners.get(1).getName());
|
||||
|
||||
int chipsBeforeA =
|
||||
state.getPlayers().stream()
|
||||
.filter(p -> p.getId().equals(PlayerId.of("PlayerA")))
|
||||
.findFirst()
|
||||
.orElseThrow()
|
||||
.getChips();
|
||||
int chipsBeforeB =
|
||||
state.getPlayers().stream()
|
||||
.filter(p -> p.getId().equals(PlayerId.of("PlayerB")))
|
||||
.findFirst()
|
||||
.orElseThrow()
|
||||
.getChips();
|
||||
|
||||
state.getPot().add(1000);
|
||||
showdown.awardPot(state);
|
||||
|
||||
Player playerA =
|
||||
state.getPlayers().stream()
|
||||
.filter(p -> p.getId().equals(PlayerId.of("PlayerA")))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
Player playerB =
|
||||
state.getPlayers().stream()
|
||||
.filter(p -> p.getId().equals(PlayerId.of("PlayerB")))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
assertEquals(chipsBeforeA + 500, playerA.getChips(), "Player A must receive half the pot");
|
||||
assertEquals(chipsBeforeB + 500, playerB.getChips(), "Player B must receive half the pot");
|
||||
assertEquals(0, state.getPot().getAmount(), "The pot must be 0 after the payout");
|
||||
|
||||
LOGGER.info("Passed the Split-Pot-Test");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for LobbyManager leave/rejoin functionality. */
|
||||
public class LobbyManagerTest {
|
||||
private LobbyManager lobbyManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
lobbyManager = new LobbyManager(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLeavePlayerFromLobbyMarkingAbsent() {
|
||||
LobbyId lobbyId = lobbyManager.createNewLobby("Test Lobby");
|
||||
assertNotNull(lobbyId);
|
||||
|
||||
// Add a player
|
||||
boolean joined = lobbyManager.addPlayerToLobby("Player1", lobbyId);
|
||||
assertTrue(joined);
|
||||
|
||||
Lobby lobby = lobbyManager.getLobby(lobbyId);
|
||||
assertTrue(lobby.getPlayerNames().contains("Player1"));
|
||||
assertTrue(lobby.getAbsentPlayers().isEmpty());
|
||||
|
||||
// Mark player as absent
|
||||
boolean left = lobbyManager.leavePlayerFromLobby("Player1", lobbyId);
|
||||
assertTrue(left);
|
||||
|
||||
// Player should be in absent set
|
||||
assertFalse(lobby.getPlayerNames().contains("Player1"));
|
||||
assertTrue(lobby.getAbsentPlayers().contains("Player1"));
|
||||
|
||||
// Player should still be in the playerToLobby mapping
|
||||
Lobby retrievedLobby = lobbyManager.getLobbyByUsername("Player1");
|
||||
assertEquals(lobbyId, retrievedLobby.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAbsentPlayerCanRejoin() {
|
||||
LobbyId lobbyId = lobbyManager.createNewLobby("Test Lobby");
|
||||
|
||||
// Add players to fill the lobby almost
|
||||
lobbyManager.addPlayerToLobby("Player1", lobbyId);
|
||||
lobbyManager.addPlayerToLobby("Player2", lobbyId);
|
||||
lobbyManager.addPlayerToLobby("Player3", lobbyId);
|
||||
|
||||
Lobby lobby = lobbyManager.getLobby(lobbyId);
|
||||
assertEquals(3, lobby.getPlayerNames().size());
|
||||
|
||||
// Player1 leaves
|
||||
lobbyManager.leavePlayerFromLobby("Player1", lobbyId);
|
||||
assertEquals(2, lobby.getPlayerNames().size());
|
||||
assertEquals(1, lobby.getAbsentPlayers().size());
|
||||
|
||||
// Player1 tries to rejoin - should succeed without error
|
||||
boolean rejoined = lobbyManager.addPlayerToLobby("Player1", lobbyId);
|
||||
assertTrue(rejoined);
|
||||
|
||||
assertEquals(3, lobby.getPlayerNames().size());
|
||||
assertEquals(0, lobby.getAbsentPlayers().size());
|
||||
assertTrue(lobby.getPlayerNames().contains("Player1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAbsentPlayerDoesNotCountAsFullLobby() {
|
||||
LobbyId lobbyId = lobbyManager.createNewLobby("Test Lobby");
|
||||
|
||||
// Fill lobby completely
|
||||
lobbyManager.addPlayerToLobby("P1", lobbyId);
|
||||
lobbyManager.addPlayerToLobby("P2", lobbyId);
|
||||
lobbyManager.addPlayerToLobby("P3", lobbyId);
|
||||
lobbyManager.addPlayerToLobby("P4", lobbyId);
|
||||
|
||||
Lobby lobby = lobbyManager.getLobby(lobbyId);
|
||||
assertEquals(4, lobby.getPlayerNames().size());
|
||||
|
||||
// P1 leaves
|
||||
lobbyManager.leavePlayerFromLobby("P1", lobbyId);
|
||||
assertEquals(3, lobby.getPlayerNames().size());
|
||||
assertEquals(1, lobby.getAbsentPlayers().size());
|
||||
|
||||
// P1 can rejoin without "LOBBY_FULL" error
|
||||
boolean rejoined = lobbyManager.addPlayerToLobby("P1", lobbyId);
|
||||
assertTrue(rejoined);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPlayerToLobbyMappingPreservedAfterLeave() {
|
||||
LobbyId lobbyId = lobbyManager.createNewLobby("Test Lobby");
|
||||
lobbyManager.addPlayerToLobby("Player1", lobbyId);
|
||||
|
||||
// Verify mapping exists
|
||||
Lobby beforeLeave = lobbyManager.getLobbyByUsername("Player1");
|
||||
assertEquals(lobbyId, beforeLeave.getId());
|
||||
|
||||
// Player leaves
|
||||
lobbyManager.leavePlayerFromLobby("Player1", lobbyId);
|
||||
|
||||
// Mapping should still exist for rejoin
|
||||
Lobby afterLeave = lobbyManager.getLobbyByUsername("Player1");
|
||||
assertEquals(lobbyId, afterLeave.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveLobbyCleanupsBothActiveAndAbsentPlayers() {
|
||||
LobbyId lobbyId = lobbyManager.createNewLobby("Test Lobby");
|
||||
|
||||
lobbyManager.addPlayerToLobby("P1", lobbyId);
|
||||
lobbyManager.addPlayerToLobby("P2", lobbyId);
|
||||
lobbyManager.leavePlayerFromLobby("P1", lobbyId);
|
||||
|
||||
// Verify both are in lobby
|
||||
Lobby lobby = lobbyManager.getLobby(lobbyId);
|
||||
assertTrue(lobby.getPlayerNames().contains("P2"));
|
||||
assertTrue(lobby.getAbsentPlayers().contains("P1"));
|
||||
|
||||
// Remove lobby
|
||||
lobbyManager.removeLobby(lobbyId);
|
||||
|
||||
// Both players should be removed from playerToLobby
|
||||
assertNull(lobbyManager.getLobbyByUsername("P1"));
|
||||
assertNull(lobbyManager.getLobbyByUsername("P2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLeaveNonExistentPlayerFromLobby() {
|
||||
LobbyId lobbyId = lobbyManager.createNewLobby("Test Lobby");
|
||||
|
||||
// Try to remove a player that was never added
|
||||
boolean left = lobbyManager.leavePlayerFromLobby("NonExistent", lobbyId);
|
||||
assertFalse(left);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLeavePlayerFromNonExistentLobby() {
|
||||
LobbyId nonExistentId = LobbyId.of(999);
|
||||
|
||||
// Try to remove from non-existent lobby
|
||||
boolean left = lobbyManager.leavePlayerFromLobby("Player1", nonExistentId);
|
||||
assertFalse(left);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGameStartedWithAbsentPlayerTracking() {
|
||||
// This test verifies that when a game starts with 4 players,
|
||||
// the absent players tracking is initialized and works
|
||||
LobbyId lobbyId = lobbyManager.createNewLobby("Test Lobby");
|
||||
|
||||
lobbyManager.addPlayerToLobby("P1", lobbyId);
|
||||
lobbyManager.addPlayerToLobby("P2", lobbyId);
|
||||
lobbyManager.addPlayerToLobby("P3", lobbyId);
|
||||
// Game should start when 4th player joins
|
||||
lobbyManager.addPlayerToLobby("P4", lobbyId);
|
||||
|
||||
Lobby lobby = lobbyManager.getLobby(lobbyId);
|
||||
assertNotNull(lobby.getGameController(), "Game should be started");
|
||||
assertEquals(4, lobby.getPlayerNames().size());
|
||||
assertEquals(0, lobby.getAbsentPlayers().size());
|
||||
|
||||
// P1 leaves during game
|
||||
boolean left = lobbyManager.leavePlayerFromLobby("P1", lobbyId);
|
||||
assertTrue(left);
|
||||
|
||||
assertEquals(3, lobby.getPlayerNames().size());
|
||||
assertEquals(1, lobby.getAbsentPlayers().size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.GameEngine;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.RoundManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.TurnManager;
|
||||
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 org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for Lobby absent players tracking functionality. */
|
||||
public class LobbyTest {
|
||||
private Lobby lobby;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
lobby = new Lobby(LobbyId.of(1), "Test Lobby");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddPlayerSuccessfully() {
|
||||
boolean added = lobby.addPlayer("Player1", 4);
|
||||
assertTrue(added);
|
||||
assertEquals(1, lobby.getPlayerNames().size());
|
||||
assertTrue(lobby.getPlayerNames().contains("Player1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddPlayerToFullLobby() {
|
||||
lobby.addPlayer("Player1", 4);
|
||||
lobby.addPlayer("Player2", 4);
|
||||
lobby.addPlayer("Player3", 4);
|
||||
lobby.addPlayer("Player4", 4);
|
||||
|
||||
// Try to add 5th player to a 4-player lobby
|
||||
boolean added = lobby.addPlayer("Player5", 4);
|
||||
assertFalse(added);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLeavePlayerMovesToAbsent() {
|
||||
lobby.addPlayer("Player1", 4);
|
||||
assertTrue(lobby.getPlayerNames().contains("Player1"));
|
||||
assertTrue(lobby.getAbsentPlayers().isEmpty());
|
||||
|
||||
// Player leaves
|
||||
boolean left = lobby.leavePlayer("Player1");
|
||||
assertTrue(left);
|
||||
|
||||
// Player should be in absent set
|
||||
assertFalse(lobby.getPlayerNames().contains("Player1"));
|
||||
assertTrue(lobby.getAbsentPlayers().contains("Player1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRestoreAbsentPlayerSuccessfully() {
|
||||
lobby.addPlayer("Player1", 4);
|
||||
lobby.leavePlayer("Player1");
|
||||
|
||||
// Restore the absent player
|
||||
boolean restored = lobby.restoreAbsentPlayer("Player1");
|
||||
assertTrue(restored);
|
||||
|
||||
// Player should be back in active list
|
||||
assertTrue(lobby.getPlayerNames().contains("Player1"));
|
||||
assertFalse(lobby.getAbsentPlayers().contains("Player1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRestoreAbsentPlayerCannotRestoreTwice() {
|
||||
lobby.addPlayer("Player1", 4);
|
||||
lobby.leavePlayer("Player1");
|
||||
|
||||
boolean restored1 = lobby.restoreAbsentPlayer("Player1");
|
||||
assertTrue(restored1);
|
||||
|
||||
// Try to restore again - should fail
|
||||
boolean restored2 = lobby.restoreAbsentPlayer("Player1");
|
||||
assertFalse(restored2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHasAnyPlayersWithActiveOnly() {
|
||||
lobby.addPlayer("Player1", 4);
|
||||
assertTrue(lobby.hasAnyPlayers());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHasAnyPlayersWithAbsentOnly() {
|
||||
lobby.addPlayer("Player1", 4);
|
||||
lobby.leavePlayer("Player1");
|
||||
assertTrue(lobby.hasAnyPlayers());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHasAnyPlayersEmpty() {
|
||||
assertFalse(lobby.hasAnyPlayers());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsPlayerActive() {
|
||||
lobby.addPlayer("Player1", 4);
|
||||
assertTrue(lobby.isPlayerActive("Player1"));
|
||||
|
||||
lobby.leavePlayer("Player1");
|
||||
assertFalse(lobby.isPlayerActive("Player1"));
|
||||
|
||||
lobby.restoreAbsentPlayer("Player1");
|
||||
assertTrue(lobby.isPlayerActive("Player1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultiplePlayersLeaveAndRejoin() {
|
||||
// Add 4 players
|
||||
lobby.addPlayer("P1", 4);
|
||||
lobby.addPlayer("P2", 4);
|
||||
lobby.addPlayer("P3", 4);
|
||||
lobby.addPlayer("P4", 4);
|
||||
|
||||
// All active
|
||||
assertEquals(4, lobby.getPlayerNames().size());
|
||||
assertEquals(0, lobby.getAbsentPlayers().size());
|
||||
|
||||
// P1 and P2 leave
|
||||
lobby.leavePlayer("P1");
|
||||
lobby.leavePlayer("P2");
|
||||
|
||||
assertEquals(2, lobby.getPlayerNames().size());
|
||||
assertEquals(2, lobby.getAbsentPlayers().size());
|
||||
|
||||
// P1 comes back
|
||||
lobby.restoreAbsentPlayer("P1");
|
||||
|
||||
assertEquals(3, lobby.getPlayerNames().size());
|
||||
assertEquals(1, lobby.getAbsentPlayers().size());
|
||||
assertTrue(lobby.getPlayerNames().contains("P1"));
|
||||
assertTrue(lobby.getAbsentPlayers().contains("P2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGameEndedCallbackInvoked() {
|
||||
// Create a simple game setup
|
||||
GameState state = new GameState();
|
||||
GameEngine engine =
|
||||
new GameEngine(
|
||||
state,
|
||||
new RuleEngine(new ArrayList<>()),
|
||||
new RoundManager(),
|
||||
new TurnManager());
|
||||
GameController gameController = new GameController(engine);
|
||||
|
||||
// Track if callback was invoked
|
||||
boolean[] callbackInvoked = {false};
|
||||
|
||||
// Set callback
|
||||
lobby.setOnGameEndedCallback(() -> callbackInvoked[0] = true);
|
||||
|
||||
// Simulate game ending
|
||||
gameController.endGame();
|
||||
// Callback should NOT be invoked yet because it's not registered in the
|
||||
// controller
|
||||
|
||||
// Now register the callback in the controller
|
||||
gameController.setOnGameEndedCallback(() -> callbackInvoked[0] = true);
|
||||
gameController.endGame();
|
||||
|
||||
assertTrue(callbackInvoked[0], "Game ended callback should have been invoked");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRenamePlayerInActiveList() {
|
||||
lobby.addPlayer("OldName", 4);
|
||||
assertTrue(lobby.getPlayerNames().contains("OldName"));
|
||||
|
||||
boolean renamed = lobby.renamePlayer("OldName", "NewName");
|
||||
assertTrue(renamed);
|
||||
|
||||
assertFalse(lobby.getPlayerNames().contains("OldName"));
|
||||
assertTrue(lobby.getPlayerNames().contains("NewName"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRenamePlayerCannotRenameIfAbsent() {
|
||||
lobby.addPlayer("Player1", 4);
|
||||
lobby.leavePlayer("Player1");
|
||||
|
||||
// Should NOT be able to rename an absent player directly
|
||||
boolean renamed = lobby.renamePlayer("Player1", "Player2");
|
||||
assertFalse(renamed);
|
||||
}
|
||||
}
|
||||