diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index 0f3f48d6..dc81f1b0 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -453,7 +453,8 @@ public class Options { val - TimeUnit.MINUTES.toSeconds(TimeUnit.SECONDS.toMinutes(val))); } }, - ENABLE_THEME_SONG ("Enable Theme Song", "MenuMusic", "Whether to play the theme song upon starting opsu!", true); + ENABLE_THEME_SONG ("Enable Theme Song", "MenuMusic", "Whether to play the theme song upon starting opsu!", true), + REPLAY_SEEKING ("Replay Seeking", "ReplaySeeking", "Enable a seeking bar on the left side of the screen during replays.", false); /** Option name. */ private String name; @@ -958,6 +959,12 @@ public class Options { */ public static boolean isThemeSongEnabled() { return GameOption.ENABLE_THEME_SONG.getBooleanValue(); } + /** + * Returns whether or not replay seeking is enabled. + * @return true if enabled + */ + public static boolean isReplaySeekingEnabled() { return GameOption.REPLAY_SEEKING.getBooleanValue(); } + /** * Sets the track checkpoint time, if within bounds. * @param time the track position (in ms) diff --git a/src/itdelatrisu/opsu/audio/SoundController.java b/src/itdelatrisu/opsu/audio/SoundController.java index b91db34e..7f42f144 100644 --- a/src/itdelatrisu/opsu/audio/SoundController.java +++ b/src/itdelatrisu/opsu/audio/SoundController.java @@ -59,6 +59,9 @@ public class SoundController { /** Sample volume multiplier, from timing points [0, 1]. */ private static float sampleVolumeMultiplier = 1f; + /** Whether all sounds are muted. */ + private static boolean isMuted; + /** The name of the current sound file being loaded. */ private static String currentFileName; @@ -261,7 +264,7 @@ public class SoundController { if (clip == null) // clip failed to load properly return; - if (volume > 0f) { + if (volume > 0f && !isMuted) { try { clip.start(volume, listener); } catch (LineUnavailableException e) { @@ -317,6 +320,12 @@ public class SoundController { playClip(s.getClip(), Options.getHitSoundVolume() * sampleVolumeMultiplier * Options.getMasterVolume(), null); } + /** + * Mutes or unmutes all sounds (hit sounds and sound effects). + * @param mute true to mute, false to unmute + */ + public static void mute(boolean mute) { isMuted = mute; } + /** * Returns the name of the current file being loaded, or null if none. */ diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 2c46797f..6d2f0cdd 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -220,6 +220,15 @@ public class Game extends BasicGameState { /** Whether the game is currently seeking to a replay position. */ private boolean isSeeking; + /** Music position bar coordinates and dimensions (for replay seeking). */ + private float musicBarX, musicBarY, musicBarWidth, musicBarHeight; + + /** Music position bar background colors. */ + private static final Color + MUSICBAR_NORMAL = new Color(12, 9, 10, 0.25f), + MUSICBAR_HOVER = new Color(12, 9, 10, 0.35f), + MUSICBAR_FILL = new Color(255, 255, 255, 0.75f); + // game-related variables private GameContainer container; private StateBasedGame game; @@ -245,6 +254,12 @@ public class Game extends BasicGameState { gOffscreen = offscreen.getGraphics(); gOffscreen.setBackground(Color.black); + // initialize music position bar location + musicBarX = width * 0.01f; + musicBarY = height * 0.05f; + musicBarWidth = Math.max(width * 0.005f, 7); + musicBarHeight = height * 0.9f; + // create the associated GameData object data = new GameData(width, height); } @@ -525,6 +540,18 @@ public class Game extends BasicGameState { if (isReplay || GameMod.AUTO.isActive()) playbackSpeed.getButton().draw(); + // draw music position bar (for replay seeking) + if (isReplay && Options.isReplaySeekingEnabled()) { + int mouseX = input.getMouseX(), mouseY = input.getMouseY(); + g.setColor((musicPositionBarContains(mouseX, mouseY)) ? MUSICBAR_HOVER : MUSICBAR_NORMAL); + g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight, 4); + if (!isLeadIn()) { + g.setColor(MUSICBAR_FILL); + float musicBarPosition = Math.min((float) trackPosition / beatmap.endTime, 1f); + g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight * musicBarPosition, 4); + } + } + // returning from pause screen if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) { // darken the screen @@ -613,7 +640,7 @@ public class Game extends BasicGameState { if (replayIndex >= replay.frames.length) updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed); - //TODO probably should to disable sounds then reseek to the new position + // seeking to a position earlier than original track position if (isSeeking && replayIndex - 1 >= 1 && replayIndex < replay.frames.length && trackPosition < replay.frames[replayIndex - 1].getTime()) { replayIndex = 0; @@ -633,7 +660,6 @@ public class Game extends BasicGameState { timingPointIndex++; } } - isSeeking = false; } // update and run replay frames @@ -648,6 +674,12 @@ public class Game extends BasicGameState { } mouseX = replayX; mouseY = replayY; + + // unmute sounds + if (isSeeking) { + isSeeking = false; + SoundController.mute(false); + } } data.updateDisplays(delta); @@ -923,9 +955,10 @@ public class Game extends BasicGameState { MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier()); } - // TODO - else if (!GameMod.AUTO.isActive() && y < 50) { - float pos = (float) x / container.getWidth() * beatmap.endTime; + // replay seeking + else if (Options.isReplaySeekingEnabled() && !GameMod.AUTO.isActive() && musicPositionBarContains(x, y)) { + SoundController.mute(true); // mute sounds while seeking + float pos = (y - musicBarY) / musicBarHeight * beatmap.endTime; MusicController.setPosition((int) pos); isSeeking = true; } @@ -1188,6 +1221,8 @@ public class Game extends BasicGameState { MusicController.setPosition(0); MusicController.setPitch(GameMod.getSpeedMultiplier()); MusicController.pause(); + + SoundController.mute(false); } skipButton.resetHover(); @@ -1753,4 +1788,14 @@ public class Game extends BasicGameState { gameObjects[i].updatePosition(); } } + + /** + * Returns true if the coordinates are within the music position bar bounds. + * @param cx the x coordinate + * @param cy the y coordinate + */ + private boolean musicPositionBarContains(float cx, float cy) { + return ((cx > musicBarX && cx < musicBarX + musicBarWidth) && + (cy > musicBarY && cy < musicBarY + musicBarHeight)); + } } diff --git a/src/itdelatrisu/opsu/states/OptionsMenu.java b/src/itdelatrisu/opsu/states/OptionsMenu.java index 6f303701..0c250719 100644 --- a/src/itdelatrisu/opsu/states/OptionsMenu.java +++ b/src/itdelatrisu/opsu/states/OptionsMenu.java @@ -93,7 +93,8 @@ public class OptionsMenu extends BasicGameState { GameOption.FIXED_HP, GameOption.FIXED_AR, GameOption.FIXED_OD, - GameOption.CHECKPOINT + GameOption.CHECKPOINT, + GameOption.REPLAY_SEEKING }); /** Total number of tabs. */