From ef1f3c20b6d6e46f6c7f3a91aaad2d11e07e7be6 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Thu, 30 Apr 2026 11:31:51 +0200 Subject: [PATCH 1/7] Feat: Add button cklick and card draw sound --- .../resources/sounds/buttons/button-click.wav | Bin 0 -> 14022 bytes src/main/resources/sounds/cards/card-reveal.wav | Bin 0 -> 67106 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/main/resources/sounds/buttons/button-click.wav create mode 100644 src/main/resources/sounds/cards/card-reveal.wav diff --git a/src/main/resources/sounds/buttons/button-click.wav b/src/main/resources/sounds/buttons/button-click.wav new file mode 100644 index 0000000000000000000000000000000000000000..397cb37df8a2c5d1e8dc2e4c2f6d8d5e9d2a5ff0 GIT binary patch literal 14022 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 Date: Thu, 30 Apr 2026 11:33:39 +0200 Subject: [PATCH 2/7] Add: Add sound manager to play sounds --- .../cs108/casono/ui/sound/SoundManager.java | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/ui/sound/SoundManager.java diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/ui/sound/SoundManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/ui/sound/SoundManager.java new file mode 100644 index 0000000..d0100b4 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/ui/sound/SoundManager.java @@ -0,0 +1,176 @@ +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.scene.media.AudioClip; +import javafx.util.Duration; + +/** + * Manages sound effects for the Casono game. Provides methods to play sounds for card reveals and + * button clicks. + */ +public class SoundManager { + private static final SoundManager INSTANCE = new SoundManager(); + private final Map soundCache = new HashMap<>(); + private float volume = 0.5f; // Volume level 0.0 - 1.0 + 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) { + System.err.println("Error preloading sounds: " + e.getMessage()); + } + } + + /** 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( + event -> + Platform.runLater( + () -> { + try { + playCardReveal(); + } catch (Exception e) { + System.err.println( + "Error playing card reveal sound: " + + e.getMessage()); + } + })); + pause.play(); + } + } + + /** 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) { + System.err.println("Error playing sound: " + soundKey); + e.printStackTrace(); + } + } + + /** 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 { + System.err.println("Sound resource not found: " + resourcePath); + return null; + } + } catch (Exception e) { + System.err.println("Failed to load sound: " + resourcePath); + e.printStackTrace(); + 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(0.0f, Math.min(1.0f, 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(0.5f); + } +} -- 2.52.0 From b3974783371309c9a54cdaf78a9113b5170c8d9a Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Thu, 30 Apr 2026 11:34:49 +0200 Subject: [PATCH 3/7] Feat: Add button click sounds to Lobby UI --- .../dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java | 4 ++++ .../casono/client/ui/lobbyui/CasinomainuiController.java | 4 ++++ .../casono/client/ui/lobbyui/LobbyButtonGridManager.java | 2 ++ 3 files changed, 10 insertions(+) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java index 67415c5..742bf3b 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java @@ -1,6 +1,7 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui; /** Main UI application for Casono. Loads the main FXML layout and sets up the stage. */ +import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager; import java.io.IOException; import javafx.application.Application; import javafx.fxml.FXMLLoader; @@ -30,6 +31,9 @@ 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(); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java index 518b8cf..0201504 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java @@ -5,6 +5,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController; import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient; import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.HighscoreViewController; +import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager; import java.io.IOException; import java.net.URL; import javafx.application.Platform; @@ -169,6 +170,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 +260,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 +274,7 @@ public class CasinomainuiController { */ @FXML public void handleCreateLobbyButton() { + SoundManager.getInstance().playButtonClick(); if (translationManager.isFull()) { LOGGER.warn("Grid is full! No more lobbies available."); return; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java index c2c5163..81d1a84 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java @@ -6,6 +6,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient; import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; +import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -363,6 +364,7 @@ public class LobbyButtonGridManager { btn.setOnAction( e -> { + SoundManager.getInstance().playButtonClick(); Integer targetLobbyId = translationManager.getLobbyIdForButton(buttonId); if (targetLobbyId != null) { -- 2.52.0 From cf52d293a4fbc61353f298c2946c5da3d63d199b Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Thu, 30 Apr 2026 11:35:34 +0200 Subject: [PATCH 4/7] Feat: Add sounds to chat UI --- .../client/ui/chatui/ChatBoxController.java | 40 +++++++++++-------- .../client/ui/chatui/ChatViewController.java | 2 + 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java index 897bc04..ceae38c 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java @@ -5,6 +5,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController.ChatKey; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType; import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message; +import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager; import java.io.IOException; import java.net.URL; import java.util.ArrayList; @@ -70,7 +71,11 @@ public class ChatBoxController { ChatModel globalChatModel = new ChatModel(global, username, -1, null); chatController.getChatModelMap().put(new ChatController.ChatKey(global), globalChatModel); addChatTab("GLOBAL", globalChatModel, global); - addWhisperChatButton.setOnAction(_ -> addWhisperChatButton.show()); + addWhisperChatButton.setOnAction( + _ -> { + SoundManager.getInstance().playButtonClick(); + addWhisperChatButton.show(); + }); } /** @@ -90,14 +95,13 @@ public class ChatBoxController { MenuItem menuItem = new MenuItem(targetUserName); addWhisperChatButton.getItems().add(menuItem); menuItem.setOnAction( - _ -> - addWhisperChat( - targetUserName, - new ChatModel( - ChatType.WHISPER, - username, - -1, - targetUserName))); + _ -> { + SoundManager.getInstance().playButtonClick(); + addWhisperChat( + targetUserName, + new ChatModel( + ChatType.WHISPER, username, -1, targetUserName)); + }); }); } @@ -124,14 +128,16 @@ public class ChatBoxController { if (oldUsername.equals(item.getText())) { item.setText(newUsername); item.setOnAction( - _ -> - addWhisperChat( - newUsername, - new ChatModel( - ChatType.WHISPER, - username, - -1, - newUsername))); + _ -> { + SoundManager.getInstance().playButtonClick(); + addWhisperChat( + newUsername, + new ChatModel( + ChatType.WHISPER, + username, + -1, + newUsername)); + }); break; } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java index fc9bff8..7b0593f 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java @@ -3,6 +3,7 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel; import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message; +import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager; import java.net.URL; import java.util.ResourceBundle; import javafx.application.Platform; @@ -76,6 +77,7 @@ public class ChatViewController implements Initializable { * The input field is cleared after sending. */ public void sendMessage() { + SoundManager.getInstance().playButtonClick(); String message = inputField.getText().trim(); if (!message.isEmpty()) { inputField.clear(); -- 2.52.0 From ba1249bf933c096f33292d2ff721b81a849faee6 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Thu, 30 Apr 2026 11:37:22 +0200 Subject: [PATCH 5/7] Feat: Add button cklick and card draw sounds to gameUI --- .../ui/gameui/CasinoGameController.java | 48 +++++++++++++++++++ .../gameuicomponents/TaskbarController.java | 12 ++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java index 61d28bc..2b9610c 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java @@ -12,6 +12,7 @@ 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.ui.gameui.gameuicomponents.PlayerStatusController; 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; @@ -65,6 +66,7 @@ public class CasinoGameController { private Image dealerImage; private javafx.animation.Timeline timeline; private boolean gameStarted = false; + private boolean firstCardUpdate = true; private java.util.List lastCommunityKeys = java.util.List.of(); private java.util.List lastMyCardKeys = java.util.List.of(); private int lastPot = Integer.MIN_VALUE; @@ -178,6 +180,38 @@ public class CasinoGameController { return suit + ":" + value; } + /** + * Count how many new non-null card keys were added when transitioning from old keys to new + * keys. + * + * @param oldKeys The previous list of card keys + * @param newKeys The new list of card keys + * @return The count of new (non-null) cards that were revealed + */ + private int countNewCards(java.util.List oldKeys, java.util.List newKeys) { + int count = 0; + int minSize = Math.min(oldKeys.size(), newKeys.size()); + + // Count cards that changed from "null" to actual card + for (int i = 0; i < minSize; i++) { + String oldKey = oldKeys.get(i); + String newKey = newKeys.get(i); + if ("null".equals(oldKey) && !"null".equals(newKey)) { + count++; + } + } + + // Count any new cards beyond the previous size (if applicable) + for (int i = minSize; i < newKeys.size(); i++) { + String newKey = newKeys.get(i); + if (!"null".equals(newKey)) { + count++; + } + } + + return Math.max(1, count); // At least play one sound if cards changed + } + /** * Generate a list of unique keys for a list of cards. * @@ -577,14 +611,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; } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java index ded5048..53583f2 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java @@ -8,6 +8,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState; import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient; import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui; +import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; @@ -319,7 +320,7 @@ public class TaskbarController { // Integer targetBet = resolveTargetBet(ActionType.RAISE_BUTTON, lastState); // if (targetBet == null) { - // return ""; + // return ""; // } // int totalContribution = Math.max(0, targetBet); @@ -412,7 +413,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); @@ -1374,18 +1375,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 +1413,7 @@ public class TaskbarController { /** Called when the Raise button is clicked. */ @FXML private void onInputPlayerRaise() { + SoundManager.getInstance().playButtonClick(); submitPresetInputAndProcess("raise"); } @@ -1450,6 +1455,7 @@ public class TaskbarController { */ @FXML private void onExitButtonClick() { + SoundManager.getInstance().playButtonClick(); javafx.application.Platform.runLater( () -> { // Close game stage @@ -1534,6 +1540,7 @@ public class TaskbarController { */ @FXML private void onBrowserButtonClick() { + SoundManager.getInstance().playButtonClick(); try { Path path = Paths.get( @@ -1553,6 +1560,7 @@ public class TaskbarController { /** 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()) { -- 2.52.0 From be8e46d356c23a369843307b8af198933ac10bbb Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Thu, 30 Apr 2026 11:41:04 +0200 Subject: [PATCH 6/7] Style: Fix spotless checkStyle Co-authored-by: Copilot --- .../cs108/casono/ui/sound/SoundManager.java | 70 +++++++++++-------- .../TaskbarControllerInputValidationTest.java | 11 ++- 2 files changed, 46 insertions(+), 35 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/ui/sound/SoundManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/ui/sound/SoundManager.java index d0100b4..d5b2f44 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/ui/sound/SoundManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/ui/sound/SoundManager.java @@ -7,18 +7,26 @@ import javafx.animation.PauseTransition; import javafx.application.Platform; 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 + * Manages sound effects for the Casono game. Provides methods to play sounds + * for card reveals and * button clicks. */ public class SoundManager { + private static final Logger LOGGER = LogManager.getLogger(SoundManager.class); private static final SoundManager INSTANCE = new SoundManager(); private final Map soundCache = new HashMap<>(); - private float volume = 0.5f; // Volume level 0.0 - 1.0 + 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() {} + private SoundManager() { + } /** Returns the singleton instance of SoundManager. */ public static SoundManager getInstance() { @@ -26,7 +34,8 @@ public class SoundManager { } /** - * Pre-loads critical sounds into cache to avoid delays on first play. Should be called at + * Pre-loads critical sounds into cache to avoid delays on first play. Should be + * called at * application startup. */ public void preloadSounds() { @@ -34,7 +43,7 @@ public class SoundManager { getOrLoadSound("button-click"); getOrLoadSound("card-reveal"); } catch (Exception e) { - System.err.println("Error preloading sounds: " + e.getMessage()); + LOGGER.error("Error preloading sounds", e); } } @@ -49,11 +58,15 @@ public class SoundManager { } /** - * 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. + * 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. + * @param count The number of cards being revealed. Each card gets its own sound + * with stagger. */ public void playCardRevealMultiple(int count) { if (count <= 0) { @@ -70,17 +83,14 @@ public class SoundManager { PauseTransition pause = new PauseTransition(Duration.millis(delayMs)); pause.setOnFinished( - event -> - Platform.runLater( - () -> { - try { - playCardReveal(); - } catch (Exception e) { - System.err.println( - "Error playing card reveal sound: " - + e.getMessage()); - } - })); + event -> Platform.runLater( + () -> { + try { + playCardReveal(); + } catch (Exception e) { + LOGGER.error("Error playing card reveal sound", e); + } + })); pause.play(); } } @@ -96,7 +106,8 @@ public class SoundManager { } /** - * Plays a generic sound by key. The sound file should exist at: /sounds/{category}/{key}.mp3 or + * Plays a generic sound by key. The sound file should exist at: + * /sounds/{category}/{key}.mp3 or * .wav */ public void playSound(String soundKey) { @@ -107,12 +118,14 @@ public class SoundManager { audioClip.play(); } } catch (Exception e) { - System.err.println("Error playing sound: " + soundKey); - e.printStackTrace(); + LOGGER.error("Error playing sound: {}", soundKey, e); } } - /** Gets or loads a sound from cache. Categorizes sounds automatically based on key prefix. */ + /** + * 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); @@ -128,12 +141,11 @@ public class SoundManager { soundCache.put(soundKey, clip); return clip; } else { - System.err.println("Sound resource not found: " + resourcePath); + LOGGER.warn("Sound resource not found: {}", resourcePath); return null; } } catch (Exception e) { - System.err.println("Failed to load sound: " + resourcePath); - e.printStackTrace(); + LOGGER.error("Failed to load sound: {}", resourcePath, e); return null; } } @@ -151,7 +163,7 @@ public class SoundManager { /** Sets the volume for sound effects (0.0 - 1.0). */ public void setVolume(float volume) { - this.volume = Math.max(0.0f, Math.min(1.0f, volume)); + this.volume = Math.max(MIN_VOLUME, Math.min(MAX_VOLUME, volume)); } /** Gets the current volume level. */ @@ -171,6 +183,6 @@ public class SoundManager { /** Unmutes sounds by restoring to default volume. */ public void unmute() { - setVolume(0.5f); + setVolume(DEFAULT_VOLUME); } } diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarControllerInputValidationTest.java b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarControllerInputValidationTest.java index bc2cd31..4751205 100644 --- a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarControllerInputValidationTest.java +++ b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarControllerInputValidationTest.java @@ -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) {} } } -- 2.52.0 From 5a3c47660debbaf08863696c54e9979071667069 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Thu, 30 Apr 2026 11:44:57 +0200 Subject: [PATCH 7/7] Refactor: rewrite lamdba execution to more elegant version Co-authored-by: Copilot --- .../cs108/casono/ui/sound/SoundManager.java | 52 ++++++++----------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/ui/sound/SoundManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/ui/sound/SoundManager.java index d5b2f44..5e7f6e6 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/ui/sound/SoundManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/ui/sound/SoundManager.java @@ -5,14 +5,14 @@ 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 + * Manages sound effects for the Casono game. Provides methods to play sounds for card reveals and * button clicks. */ public class SoundManager { @@ -25,8 +25,7 @@ public class SoundManager { private float volume = DEFAULT_VOLUME; private static final long CARD_REVEAL_STAGGER_MS = 120; // Delay between card reveal sounds - private SoundManager() { - } + private SoundManager() {} /** Returns the singleton instance of SoundManager. */ public static SoundManager getInstance() { @@ -34,8 +33,7 @@ public class SoundManager { } /** - * Pre-loads critical sounds into cache to avoid delays on first play. Should be - * called at + * Pre-loads critical sounds into cache to avoid delays on first play. Should be called at * application startup. */ public void preloadSounds() { @@ -58,15 +56,11 @@ public class SoundManager { } /** - * 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. + * 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. + * @param count The number of cards being revealed. Each card gets its own sound with stagger. */ public void playCardRevealMultiple(int count) { if (count <= 0) { @@ -82,19 +76,23 @@ public class SoundManager { long delayMs = cardIndex * CARD_REVEAL_STAGGER_MS; PauseTransition pause = new PauseTransition(Duration.millis(delayMs)); - pause.setOnFinished( - event -> Platform.runLater( - () -> { - try { - playCardReveal(); - } catch (Exception e) { - LOGGER.error("Error playing card reveal sound", e); - } - })); + 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"); @@ -106,8 +104,7 @@ public class SoundManager { } /** - * Plays a generic sound by key. The sound file should exist at: - * /sounds/{category}/{key}.mp3 or + * Plays a generic sound by key. The sound file should exist at: /sounds/{category}/{key}.mp3 or * .wav */ public void playSound(String soundKey) { @@ -122,10 +119,7 @@ public class SoundManager { } } - /** - * Gets or loads a sound from cache. Categorizes sounds automatically based on - * key prefix. - */ + /** 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); -- 2.52.0