diff --git a/Projekte/mdga/client/src/main/java/pp/mdga/client/MdgaApp.java b/Projekte/mdga/client/src/main/java/pp/mdga/client/MdgaApp.java index 5aa92fc0..cb0371c0 100644 --- a/Projekte/mdga/client/src/main/java/pp/mdga/client/MdgaApp.java +++ b/Projekte/mdga/client/src/main/java/pp/mdga/client/MdgaApp.java @@ -106,7 +106,7 @@ public void simpleInitApp() { gameView = new GameView(this); ceremonyView = new CeremonyView(this); - enter(MdgaState.GAME); + enter(MdgaState.MAIN); } /** diff --git a/Projekte/mdga/client/src/main/java/pp/mdga/client/ModelSynchronizer.java b/Projekte/mdga/client/src/main/java/pp/mdga/client/ModelSynchronizer.java index e1c1b987..0b26a305 100644 --- a/Projekte/mdga/client/src/main/java/pp/mdga/client/ModelSynchronizer.java +++ b/Projekte/mdga/client/src/main/java/pp/mdga/client/ModelSynchronizer.java @@ -107,8 +107,8 @@ public void selectTsk(Color color) { app.getGameLogic().selectTsk(color); } - public void unselectTsk() { - app.getGameLogic().selectTsk(Color.NONE); + public void unselectTsk(Color color) { + app.getGameLogic().deselectTSK(color); } public void rolledDice() { diff --git a/Projekte/mdga/client/src/main/java/pp/mdga/client/NotificationSynchronizer.java b/Projekte/mdga/client/src/main/java/pp/mdga/client/NotificationSynchronizer.java index b943e955..6e080158 100644 --- a/Projekte/mdga/client/src/main/java/pp/mdga/client/NotificationSynchronizer.java +++ b/Projekte/mdga/client/src/main/java/pp/mdga/client/NotificationSynchronizer.java @@ -60,7 +60,6 @@ private void handleLobby(Notification notification) { if (notification instanceof TskSelectNotification n) { lobbyView.setTaken(n.getColor(), true, n.isSelf(), n.getName()); - lobbyView.setTaken(n.getColor(), true, false, n.getName()); } else if (notification instanceof StartDialogNotification) { app.enter(MdgaState.MAIN); } else if (notification instanceof TskUnselectNotification n) { diff --git a/Projekte/mdga/client/src/main/java/pp/mdga/client/acoustic/AcousticHandler.java b/Projekte/mdga/client/src/main/java/pp/mdga/client/acoustic/AcousticHandler.java index efab4407..264197d4 100644 --- a/Projekte/mdga/client/src/main/java/pp/mdga/client/acoustic/AcousticHandler.java +++ b/Projekte/mdga/client/src/main/java/pp/mdga/client/acoustic/AcousticHandler.java @@ -7,46 +7,41 @@ import java.util.*; import java.util.prefs.Preferences; -/** - * Handles the acoustic functionality for the game, including music playback, sound effects, - * volume management, and transitions between game states. - */ public class AcousticHandler { - private MdgaApp app; // Reference to the main application - private MdgaState state = MdgaState.NONE; // Current state of the game - private boolean playGame = false; // Whether the game tracks are currently playing - private ArrayList gameTracks = new ArrayList<>(); // List of available game music tracks - private NanoTimer trackTimer = new NanoTimer(); // Timer for managing track transitions + private MdgaApp app; - private boolean fading = false; // Whether a fade transition is in progress - private NanoTimer fadeTimer = new NanoTimer(); // Timer for fade transitions - private static final float FADE_DURATION = 3.0f; // Duration of fade-out - private static final float CROSSFADE_DURATION = 1.5f; // Duration of fade-in - private GameMusic playing = null; // Currently playing music track - private GameMusic scheduled = null; // Track scheduled to play next - private GameMusic old = null; // Track being faded out + private MdgaState state = MdgaState.NONE; - private float mainVolume = 0.0f; // Main volume level - private float musicVolume = 1.0f; // Music volume level - private float soundVolume = 1.0f; // Sound effects volume level + private boolean playGame = false; + private ArrayList gameTracks = new ArrayList<>(); + private NanoTimer trackTimer = new NanoTimer(); - private ArrayList sounds = new ArrayList<>(); // List of active sound effects - private Preferences prefs = Preferences.userNodeForPackage(AcousticHandler.class); // User preferences for volume settings + private boolean fading = false; // Indicates if a fade is in progress + private NanoTimer fadeTimer = new NanoTimer(); // Timer to track fade progress + private static final float FADE_DURATION = 3.0f; // Duration for outfade + private static final float CROSSFADE_DURATION = 1.5f; // Duration for infade + private GameMusic playing = null; // Currently playing track + private GameMusic scheduled = null; // Scheduled track to play next + private GameMusic old = null; // Old track being faded out + + private float mainVolume = 0.0f; + private float musicVolume = 1.0f; + private float soundVolume = 1.0f; + + private ArrayList sounds = new ArrayList<>(); + + private Preferences prefs = Preferences.userNodeForPackage(AcousticHandler.class); - /** - * Initializes the AcousticHandler with the main application and loads user volume settings. - * - * @param app The main application instance. - */ public AcousticHandler(MdgaApp app) { this.app = app; + mainVolume = prefs.getFloat("mainVolume", 1.0f); musicVolume = prefs.getFloat("musicVolume", 1.0f); soundVolume = prefs.getFloat("soundVolume", 1.0f); } /** - * Updates the state of the AcousticHandler. Should be called every frame. + * This method updates the acousticHandler and should be called every frame */ public void update() { updateVolumeAndTrack(); @@ -58,7 +53,9 @@ public void update() { Iterator iterator = sounds.iterator(); while (iterator.hasNext()) { GameSound s = iterator.next(); + s.update(getSoundVolumeTotal()); + if (!s.isPlaying()) { iterator.remove(); } @@ -66,76 +63,314 @@ public void update() { } /** - * Plays a sound effect immediately. + * This method instantly plays a sound * - * @param sound The sound effect to play. + * @param sound the sound to be played */ public void playSound(MdgaSound sound) { - // Implementation for playing predefined sound effects based on game events + ArrayList assets = new ArrayList(); + switch (sound) { + case LOST: + assets.add(new SoundAssetDelayVolume(SoundAsset.LOST, 1.0f, 0.0f)); + break; + case VICTORY: + assets.add(new SoundAssetDelayVolume(SoundAsset.VICTORY, 1.0f, 0.0f)); + break; + case BUTTON_PRESSED: + assets.add(new SoundAssetDelayVolume(SoundAsset.BUTTON_PRESS, 0.7f, 0.0f)); + break; + case WRONG_INPUT: + assets.add(new SoundAssetDelayVolume(SoundAsset.ERROR, 1.0f, 0.0f)); + break; + case UI_CLICK: + assets.add(new SoundAssetDelayVolume(SoundAsset.UI_CLICK, 0.8f, 0.0f)); + break; + case START: + assets.add(new SoundAssetDelayVolume(SoundAsset.START, 0.8f, 0.5f)); + break; + case THROW: + assets.add(new SoundAssetDelayVolume(SoundAsset.LAUGHT, 1.0f, 0.2f)); + break; + case POWERUP: + assets.add(new SoundAssetDelayVolume(SoundAsset.POWERUP, 1.0f, 0.2f)); + break; + case SELF_READY: + assets.add(new SoundAssetDelayVolume(SoundAsset.ROBOT_READY, 1.0f, 0.0f)); + break; + case OTHER_READY: + assets.add(new SoundAssetDelayVolume(SoundAsset.UNIT_READY, 1.0f, 0.0f)); + break; + case OTHER_CONNECTED: + assets.add(new SoundAssetDelayVolume(SoundAsset.CONNECTED, 1.0f, 0.0f)); + break; + case NOT_READY: + assets.add(new SoundAssetDelayVolume(SoundAsset.UI_SOUND, 1.0f, 0.0f)); + break; + case LEAVE: + assets.add(new SoundAssetDelayVolume(SoundAsset.UI_SOUND2, 0.6f, 0.0f)); + break; + default: + break; + } + + for (SoundAssetDelayVolume sawd : assets) { + GameSound gameSound = new GameSound(app, sawd.asset(), getSoundVolumeTotal(), sawd.subVolume(), sawd.delay()); + sounds.add(gameSound); + } } /** - * Transitions music playback to match the specified game state. + * This method fades the played music to fit the state. * - * @param state The new game state. + * @param state the state of which the corresponding music should be played to be played */ public void playState(MdgaState state) { - // Implementation for managing state-specific music playback + if (this.state == state) { + return; + } + MusicAsset asset = null; + + switch (state) { + case MAIN: + playGame = false; + asset = MusicAsset.MAIN_MENU; + break; + case LOBBY: + playGame = false; + asset = MusicAsset.LOBBY; + break; + case GAME: + addGameTracks(); + playGame = true; + assert (!gameTracks.isEmpty()) : "no more game music available"; + asset = gameTracks.remove(0); + break; + case CEREMONY: + playGame = false; + asset = MusicAsset.CEREMONY; + break; + case NONE: + throw new RuntimeException("no music for state NONE"); + } + + assert (null != asset) : "music sceduling went wrong"; + + scheduled = new GameMusic(app, asset, getMusicVolumeTotal(), asset.getSubVolume(), asset.getLoop(), 0.0f); } /** - * Performs linear interpolation between two values. + * Performs linear interpolation between two float values. * * @param start The starting value. * @param end The ending value. - * @param t The interpolation factor (0 to 1). - * @return The interpolated value. + * @param t The interpolation factor, typically between 0 and 1. + * @return The interpolated value between start and end. */ private float lerp(float start, float end, float t) { return start + t * (end - start); } /** - * Manages audio playback transitions and volume adjustments. + * Updates the state of audio playback, handling track transitions and volume adjustments. + * + * This method ensures smooth transitions between tracks using fade-in and fade-out effects. + * It also handles cases where no track is playing, starting a scheduled track immediately at full volume. + * The method prioritizes the latest scheduled track if multiple scheduling occurs quickly. + * + * Behavior: + * 1. If nothing is scheduled and no track is playing, it exits early. + * 2. If a scheduled track exists and no track is playing, the scheduled track starts immediately at full volume. + * 3. If a scheduled track exists while a track is playing, it initiates a fade-out for the currently playing track + * and prepares for the new track to fade in. + * 4. If a track transition is in progress (fading), it processes the fade-out and fade-in states. + * If a new track is scheduled during this process, it interrupts the current transition and prioritizes the new track. + * 5. If no fading is needed and a track is playing, it ensures the track's volume is updated. + * + * Special cases: + * - If no track is playing and a new track is scheduled, it starts the track immediately without fading. + * - If a new track is scheduled during fading, it resets the transition to prioritize the new track. */ private void updateVolumeAndTrack() { - // Implementation for handling fade-ins, fade-outs, and volume updates + if (scheduled == null && !fading && playing == null) { + // Nothing to do, early exit + return; + } + + if (scheduled != null && playing == null && !fading) { + // No current track, start scheduled track immediately at full volume + playing = scheduled; + scheduled = null; + playing.play(); + playing.update(getMusicVolumeTotal()); // Set volume to full + return; + } + + if (scheduled != null && !fading) { + // Initiate a fade process if a new track is scheduled + fading = true; + fadeTimer.reset(); + old = playing; // The currently playing track becomes the old track + playing = null; // Clear the playing track during the fade process + } + + if (fading) { + handleFadeProcess(); + + // Handle any interruptions due to newly scheduled tracks + if (scheduled != null && playing != null && playing != scheduled) { + // Interrupt the current infade and switch to the new scheduled track + old = playing; // Treat the currently infading track as the old track + playing = null; // Reset playing to allow switching + fadeTimer.reset(); // Restart fade timer for the new track + } + } else if (playing != null) { + // Update volume for the currently playing track + playing.update(getMusicVolumeTotal()); + } else if (scheduled != null) { + // If no track is playing and one is scheduled, start it immediately at full volume + playing = scheduled; + scheduled = null; + playing.play(); + playing.update(getMusicVolumeTotal()); // Set volume to full + } } /** - * Handles the fade-out and fade-in processes for audio transitions. + * Manages the fading process during audio track transitions. + * + * This method handles the fade-out of the currently playing (old) track, manages any pause between the fade-out + * and fade-in, and initiates the fade-in for the new track if applicable. It ensures smooth transitions between + * tracks while maintaining the correct volume adjustments. + * + * Behavior: + * 1. **Outfade:** Gradually decreases the volume of the `old` track over the duration of `FADE_DURATION`. + * Once the outfade completes, the `old` track is paused and cleared. + * 2. **Pause Handling:** Waits for a defined pause (if applicable) before initiating the infade for the next track. + * 3. **Infade:** If a `scheduled` track exists and the outfade and pause are complete, it begins playing + * the new track (`playing`) and initiates the infade process. + * + * Key Details: + * - The outfade volume adjustment is interpolated linearly from full volume to zero using the `lerp` function. + * - The pause duration is retrieved from the scheduled track if it is specified. + * - If a new track is scheduled during the fade process, it is handled by external logic to prioritize transitions. + * + * Preconditions: + * - `fading` is expected to be `true` when this method is called. + * - The method is invoked as part of the `updateVolumeAndTrack` process. */ private void handleFadeProcess() { - // Implementation for managing fade transitions + float time = fadeTimer.getTimeInSeconds(); + + // Handle outfade for the old track + if (old != null && time <= FADE_DURATION) { + float t = Math.min(time / FADE_DURATION, 1.0f); + float oldVolume = lerp(1.0f, 0.0f, t); + old.update(getMusicVolumeTotal() * oldVolume); + } + + if (old != null && time > FADE_DURATION) { + // Complete outfade + old.pause(); + old = null; + } + + // Handle pause duration before infade + float pause = (scheduled != null) ? scheduled.getPause() : 0.0f; + if (time > FADE_DURATION + pause) { + if (playing == null && scheduled != null) { + // Begin infade for the new track + playing = scheduled; + scheduled = null; + playing.play(); // Start playing the new track + } + handleInfade(time - FADE_DURATION - pause); + } } /** * Manages the fade-in process for the currently playing track. * - * @param infadeTime Time elapsed since the fade-in process started. + * This method gradually increases the volume of the `playing` track from zero to full volume + * over the duration of `CROSSFADE_DURATION`. It ensures a smooth transition into the new track. + * + * Behavior: + * 1. If no track is set as `playing`, the method exits early, as there is nothing to fade in. + * 2. Linearly interpolates the volume of the `playing` track from 0.0 to 1.0 based on the elapsed + * `infadeTime` and the specified `CROSSFADE_DURATION`. + * 3. Once the fade-in is complete (when `infadeTime` exceeds `CROSSFADE_DURATION`), the method: + * - Marks the fade process (`fading`) as complete. + * - Ensures the `playing` track is updated to its full volume. + * + * Key Details: + * - Uses the `lerp` function to calculate the volume level for the `playing` track during the fade-in. + * - Ensures the volume is always a value between 0.0 and 1.0. + * - The `infadeTime` parameter should be relative to the start of the fade-in process. + * + * Preconditions: + * - The `playing` track must be initialized and actively fading in for this method to have an effect. + * - The method is invoked as part of the `updateVolumeAndTrack` process. + * + * @param infadeTime The elapsed time (in seconds) since the fade-in process started. */ private void handleInfade(float infadeTime) { - // Implementation for handling the fade-in process + if (playing == null) { + // Nothing to infade + return; + } + + // Proceed with the infade for the current playing track + float t = Math.min(infadeTime / CROSSFADE_DURATION, 1.0f); + float newVolume = lerp(0.0f, 1.0f, t); + playing.update(getMusicVolumeTotal() * newVolume); + + if (infadeTime > CROSSFADE_DURATION) { + // Infade is complete, finalize state + fading = false; + playing.update(getMusicVolumeTotal()); // Ensure full volume + } } /** - * Adds and shuffles game music tracks. + * Adds a list of game tracks to the gameTracks collection and shuffles them. + * This method adds predefined game tracks to the track list and shuffles the order. */ private void addGameTracks() { - // Adds predefined game music tracks to the list and shuffles them + Random random = new Random(); + + for (int i = 1; i <= 6; i++) { + gameTracks.add(MusicAsset.valueOf("GAME_" + i)); + } + Collections.shuffle(gameTracks, random); } /** - * Updates the game music tracks, scheduling new ones as needed. + * Updates the current game tracks. If the currently playing track is nearing its end, + * a new track will be scheduled to play. If the list of game tracks is empty, it will be refreshed. */ private void updateGameTracks() { - // Handles scheduling and transitioning between game tracks + if(null == playing) { + return; + } + + if (playing.nearEnd(10)) { + if (gameTracks.isEmpty()) { + addGameTracks(); + } + } + + if (playing != null && playing.nearEnd(3) && trackTimer.getTimeInSeconds() > 20) { + trackTimer.reset(); + + MusicAsset nextTrack = gameTracks.remove(0); + + scheduled = new GameMusic(app, nextTrack, getMusicVolumeTotal(), nextTrack.getSubVolume(), nextTrack.getLoop(), 0.0f); + } } /** - * Gets the main volume level. + * Retrieves the main volume level. * - * @return The main volume level. + * @return The current main volume level. */ public float getMainVolume() { return mainVolume; @@ -160,7 +395,7 @@ public float getSoundVolume() { } /** - * Sets the main volume level and saves the setting. + * Sets the main volume level. * * @param mainVolume The desired main volume level. */ @@ -169,27 +404,6 @@ public void setMainVolume(float mainVolume) { prefs.putFloat("mainVolume", mainVolume); } - // Similar getters and setters for musicVolume and soundVolume... - - /** - * Calculates the total music volume (main volume × music volume). - * - * @return The total music volume. - */ - float getMusicVolumeTotal() { - return musicVolume * mainVolume; - } - - /** - * Calculates the total sound volume (main volume × sound volume). - * - * @return The total sound volume. - */ - float getSoundVolumeTotal() { - return soundVolume * mainVolume; - } - - /** * Sets the music volume level. * @@ -209,4 +423,23 @@ public void setSoundVolume(float soundVolume) { this.soundVolume = soundVolume; prefs.putFloat("soundVolume", soundVolume); } + + /** + * Calculates the total music volume by multiplying the music volume by the main volume. + * + * @return The total music volume. + */ + float getMusicVolumeTotal() { + + return getMusicVolume() * getMainVolume(); + } + + /** + * Calculates the total sound volume by multiplying the sound volume by the main volume. + * + * @return The total sound volume. + */ + float getSoundVolumeTotal() { + return getSoundVolume() * getMainVolume(); + } } diff --git a/Projekte/mdga/client/src/main/java/pp/mdga/client/view/LobbyView.java b/Projekte/mdga/client/src/main/java/pp/mdga/client/view/LobbyView.java index 0ee82c56..242cae89 100644 --- a/Projekte/mdga/client/src/main/java/pp/mdga/client/view/LobbyView.java +++ b/Projekte/mdga/client/src/main/java/pp/mdga/client/view/LobbyView.java @@ -152,6 +152,23 @@ public void setTaken(Color color, boolean isTaken, boolean isSelf, String name) if(isTaken) { if(isSelf) { + /*if(own != null) { + switch (color) { + case CYBER: + cyberButton.setTaken(LobbyButton.Taken.NOT, name); + break; + case AIRFORCE: + airforceButton.setTaken(LobbyButton.Taken.NOT, name); + break; + case ARMY: + armyButton.setTaken(LobbyButton.Taken.NOT, name); + break; + case NAVY: + navyButton.setTaken(LobbyButton.Taken.NOT, name); + break; + } + }*/ + own = color; taken = LobbyButton.Taken.SELF; } else { @@ -228,7 +245,7 @@ private void toggleTsk(Color color) { app.getModelSynchronize().selectTsk(color); break; case SELF: - app.getModelSynchronize().unselectTsk(); + app.getModelSynchronize().unselectTsk(color); break; case OTHER: //nothing diff --git a/Projekte/mdga/model/src/main/java/pp/mdga/client/ClientGameLogic.java b/Projekte/mdga/model/src/main/java/pp/mdga/client/ClientGameLogic.java index c2526b46..133d5689 100644 --- a/Projekte/mdga/model/src/main/java/pp/mdga/client/ClientGameLogic.java +++ b/Projekte/mdga/model/src/main/java/pp/mdga/client/ClientGameLogic.java @@ -239,6 +239,10 @@ public void selectTsk(Color color){ state.selectTSK(color); } + public void deselectTSK(Color color){ + state.deselectTSK(color); + } + public void selectDice(){ state.selectDice(); } diff --git a/Projekte/mdga/model/src/main/java/pp/mdga/client/dialogState/LobbyState.java b/Projekte/mdga/model/src/main/java/pp/mdga/client/dialogState/LobbyState.java index 7678624a..fdb8e5d2 100644 --- a/Projekte/mdga/model/src/main/java/pp/mdga/client/dialogState/LobbyState.java +++ b/Projekte/mdga/model/src/main/java/pp/mdga/client/dialogState/LobbyState.java @@ -101,9 +101,13 @@ public void received(LobbyPlayerJoinedMessage msg){ @Override public void received(UpdateTSKMessage msg){ - logic.addNotification(new TskUnselectNotification(logic.getGame().getPlayers().get(msg.getId()).getColor())); + if(msg.isTaken()) { + logic.addNotification(new TskSelectNotification(msg.getColor(), logic.getGame().getPlayers().get(msg.getId()).getName(), parent.getOwnPlayerId()== msg.getId())); + } else { + logic.addNotification(new TskUnselectNotification(logic.getGame().getPlayers().get(msg.getId()).getColor())); + } + logic.getGame().getPlayers().get(msg.getId()).setColor(msg.getColor()); - logic.addNotification(new TskSelectNotification(msg.getColor(), logic.getGame().getPlayers().get(msg.getId()).getName(), parent.getOwnPlayerId()== msg.getId())); } @Override diff --git a/Projekte/mdga/model/src/main/java/pp/mdga/message/server/UpdateTSKMessage.java b/Projekte/mdga/model/src/main/java/pp/mdga/message/server/UpdateTSKMessage.java index bbbdf14b..2da6f682 100644 --- a/Projekte/mdga/model/src/main/java/pp/mdga/message/server/UpdateTSKMessage.java +++ b/Projekte/mdga/model/src/main/java/pp/mdga/message/server/UpdateTSKMessage.java @@ -18,23 +18,26 @@ public class UpdateTSKMessage extends ServerMessage { */ private final Color color; + private final boolean isTaken; + /** * Constructs a new UpdateTSK instance with the specified id and color. * * @param id the name associated with the update * @param color the color associated with the update */ - public UpdateTSKMessage(int id, Color color) { + public UpdateTSKMessage(int id, Color color, boolean isTaken) { super(); this.id = id; this.color = color; + this.isTaken = isTaken; } /** * Default constructor for serialization purposes. */ private UpdateTSKMessage() { - this(0, null); + this(0, null, false); } /** @@ -84,4 +87,8 @@ public String toString() { public String getInfoTextKey() { return ""; } + + public boolean isTaken() { + return isTaken; + } } diff --git a/Projekte/mdga/model/src/main/java/pp/mdga/server/automaton/LobbyState.java b/Projekte/mdga/model/src/main/java/pp/mdga/server/automaton/LobbyState.java index 2a4eba34..708a26d5 100644 --- a/Projekte/mdga/model/src/main/java/pp/mdga/server/automaton/LobbyState.java +++ b/Projekte/mdga/model/src/main/java/pp/mdga/server/automaton/LobbyState.java @@ -7,6 +7,7 @@ import pp.mdga.message.server.*; import pp.mdga.server.ServerGameLogic; +import java.lang.foreign.StructLayout; import java.util.Map; /** @@ -84,8 +85,13 @@ public void received(SelectTSKMessage msg, int from) { return; } } + + if(this.logic.getGame().getPlayerById(from).getColor() != Color.NONE) { + this.logic.getServerSender().broadcast(new UpdateTSKMessage(from, this.logic.getGame().getPlayerById(from).getColor(), false)); + } + this.logic.getGame().getPlayerById(from).setColor(msg.getColor()); - this.logic.getServerSender().broadcast(new UpdateTSKMessage(from, msg.getColor())); + this.logic.getServerSender().broadcast(new UpdateTSKMessage(from, msg.getColor(), true)); } /** @@ -98,7 +104,7 @@ public void received(SelectTSKMessage msg, int from) { @Override public void received(DeselectTSKMessage msg, int from) { this.logic.getGame().getPlayerById(from).setColor(Color.NONE); - this.logic.getServerSender().broadcast(new UpdateTSKMessage(from, Color.NONE)); + this.logic.getServerSender().broadcast(new UpdateTSKMessage(from, Color.NONE, false)); } /**