diff --git a/res/playback-05x.png b/res/playback-05x.png new file mode 100644 index 00000000..358d7e0c Binary files /dev/null and b/res/playback-05x.png differ diff --git a/res/playback-1x.png b/res/playback-1x.png new file mode 100644 index 00000000..a5949368 Binary files /dev/null and b/res/playback-1x.png differ diff --git a/res/playback-2x.png b/res/playback-2x.png new file mode 100644 index 00000000..ba43cdfb Binary files /dev/null and b/res/playback-2x.png differ diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index 12f9a814..50b36914 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -104,6 +104,10 @@ public enum GameImage { } }, + REPLAY_05XPLAYBACK ("playback-05x", "png"), + REPLAY_1XPLAYBACK ("playback-1x", "png"), + REPLAY_2XPLAYBACK ("playback-2x", "png"), + // Circle HITCIRCLE ("hitcircle", "png"), HITCIRCLE_OVERLAY ("hitcircleoverlay", "png"), diff --git a/src/itdelatrisu/opsu/GameMod.java b/src/itdelatrisu/opsu/GameMod.java index a7bd3e97..d5e09a5f 100644 --- a/src/itdelatrisu/opsu/GameMod.java +++ b/src/itdelatrisu/opsu/GameMod.java @@ -33,7 +33,7 @@ public enum GameMod { "Easy", "Reduces overall difficulty - larger circles, more forgiving HP drain, less accuracy required."), NO_FAIL (Category.EASY, 1, GameImage.MOD_NO_FAIL, "NF", 1, Input.KEY_W, 0.5f, "NoFail", "You can't fail. No matter what."), - HALF_TIME (Category.EASY, 2, GameImage.MOD_HALF_TIME, "HT", 256, Input.KEY_E, 0.3f, false, + HALF_TIME (Category.EASY, 2, GameImage.MOD_HALF_TIME, "HT", 256, Input.KEY_E, 0.3f, "HalfTime", "Less zoom."), HARD_ROCK (Category.HARD, 0, GameImage.MOD_HARD_ROCK, "HR", 16, Input.KEY_A, 1.06f, "HardRock", "Everything just got a bit harder..."), @@ -41,7 +41,7 @@ public enum GameMod { "SuddenDeath", "Miss a note and fail."), // PERFECT (Category.HARD, 1, GameImage.MOD_PERFECT, "PF", 64, Input.KEY_S, 1f, // "Perfect", "SS or quit."), - DOUBLE_TIME (Category.HARD, 2, GameImage.MOD_DOUBLE_TIME, "DT", 64, Input.KEY_D, 1.12f, false, + DOUBLE_TIME (Category.HARD, 2, GameImage.MOD_DOUBLE_TIME, "DT", 64, Input.KEY_D, 1.12f, "DoubleTime", "Zoooooooooom."), // NIGHTCORE (Category.HARD, 2, GameImage.MOD_NIGHTCORE, "NT", 64, Input.KEY_D, 1.12f, // "Nightcore", "uguuuuuuuu"), @@ -173,6 +173,9 @@ public enum GameMod { /** The last calculated score multiplier, or -1f if it must be recalculated. */ private static float scoreMultiplier = -1f; + /** */ + private static float speedMultiplier = -1f; + /** * Initializes the game mods. * @param width the container width @@ -198,7 +201,7 @@ public enum GameMod { mod.active = false; } - scoreMultiplier = -1f; + scoreMultiplier = speedMultiplier = -1f; } /** @@ -216,6 +219,21 @@ public enum GameMod { return scoreMultiplier; } + /** + * + */ + public static float getSpeedMultiplier() { + if (speedMultiplier < 0f) { + float multiplier = 1f; + if (DOUBLE_TIME.isActive()) + multiplier = 1.5f; + else if (HALF_TIME.isActive()) + multiplier = 0.75f; + speedMultiplier = multiplier; + } + return speedMultiplier; + } + /** * Returns the current game mod state (bitwise OR of active mods). */ @@ -233,6 +251,7 @@ public enum GameMod { * @param state the state (bitwise OR of active mods) */ public static void loadModState(int state) { + scoreMultiplier = speedMultiplier = -1f; for (GameMod mod : GameMod.values()) mod.active = ((state & mod.getBit()) > 0); } @@ -352,7 +371,7 @@ public enum GameMod { return; active = !active; - scoreMultiplier = -1f; + scoreMultiplier = speedMultiplier = -1f; if (checkInverse) { if (AUTO.isActive()) { diff --git a/src/itdelatrisu/opsu/OsuGroupNode.java b/src/itdelatrisu/opsu/OsuGroupNode.java index 8c8e15d2..b51eab5b 100644 --- a/src/itdelatrisu/opsu/OsuGroupNode.java +++ b/src/itdelatrisu/opsu/OsuGroupNode.java @@ -118,16 +118,19 @@ public class OsuGroupNode { return null; OsuFile osu = osuFiles.get(osuFileIndex); + float speedModifier = GameMod.getSpeedMultiplier(); + long endTime = (long) (osu.endTime / speedModifier); + int bpmMin = (int) (osu.bpmMin * speedModifier); + int bpmMax = (int) (osu.bpmMax * speedModifier); String[] info = new String[5]; info[0] = osu.toString(); info[1] = String.format("Mapped by %s", osu.creator); info[2] = String.format("Length: %d:%02d BPM: %s Objects: %d", - TimeUnit.MILLISECONDS.toMinutes(osu.endTime), - TimeUnit.MILLISECONDS.toSeconds(osu.endTime) - - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(osu.endTime)), - (osu.bpmMax <= 0) ? "--" : - ((osu.bpmMin == osu.bpmMax) ? osu.bpmMin : String.format("%d-%d", osu.bpmMin, osu.bpmMax)), + TimeUnit.MILLISECONDS.toMinutes(endTime), + TimeUnit.MILLISECONDS.toSeconds(endTime) - + TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(endTime)), + (bpmMax <= 0) ? "--" : ((bpmMin == bpmMax) ? bpmMin : String.format("%d-%d", bpmMin, bpmMax)), (osu.hitObjectCircle + osu.hitObjectSlider + osu.hitObjectSpinner)); info[3] = String.format("Circles: %d Sliders: %d Spinners: %d", osu.hitObjectCircle, osu.hitObjectSlider, osu.hitObjectSpinner); diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 61baf19a..33f1f42a 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -49,6 +49,7 @@ import java.util.Scanner; import javax.imageio.ImageIO; +import itdelatrisu.opsu.replay.PlaybackSpeed; import org.lwjgl.BufferUtils; import org.lwjgl.opengl.Display; import org.lwjgl.opengl.GL11; @@ -180,6 +181,9 @@ public class Utils { // initialize game mods GameMod.init(width, height); + // initialize playback buttons + PlaybackSpeed.init(width, height); + // initialize hit objects OsuHitObject.init(width, height); diff --git a/src/itdelatrisu/opsu/audio/MusicController.java b/src/itdelatrisu/opsu/audio/MusicController.java index 7c93c15d..8fc58c99 100644 --- a/src/itdelatrisu/opsu/audio/MusicController.java +++ b/src/itdelatrisu/opsu/audio/MusicController.java @@ -297,6 +297,14 @@ public class MusicController { SoundStore.get().setMusicVolume((isTrackDimmed()) ? volume * dimLevel : volume); } + /** + * Sets the music pitch. + * @param pitch [0, ..] + */ + public static void setPitch(float pitch) { + SoundStore.get().setMusicPitch(pitch); + } + /** * Returns whether or not the current track has ended. */ diff --git a/src/itdelatrisu/opsu/replay/PlaybackSpeed.java b/src/itdelatrisu/opsu/replay/PlaybackSpeed.java new file mode 100644 index 00000000..e8248a9e --- /dev/null +++ b/src/itdelatrisu/opsu/replay/PlaybackSpeed.java @@ -0,0 +1,62 @@ +package itdelatrisu.opsu.replay; + +import itdelatrisu.opsu.GameImage; +import itdelatrisu.opsu.GameMod; +import itdelatrisu.opsu.MenuButton; +import org.newdawn.slick.Image; + +public enum PlaybackSpeed { + NORMAL(GameImage.REPLAY_1XPLAYBACK, 1f), + DOUBLE(GameImage.REPLAY_2XPLAYBACK, 2f), + HALF(GameImage.REPLAY_05XPLAYBACK, 0.5f); + + /** The file name of the button image. */ + private GameImage gameImage; + + /** The button of the playback. */ + private MenuButton button; + + /** The speed modifier of the playback. */ + private float modifier; + + PlaybackSpeed(GameImage gameImage, float modifier) { + this.gameImage = gameImage; + this.modifier = modifier; + } + + public static void init(int width, int height) { + // create buttons + for (PlaybackSpeed playback : PlaybackSpeed.values()) { + Image img = playback.gameImage.getImage(); + playback.button = new MenuButton(img, width * 0.98f - (img.getWidth() / 2f), height * 0.25f); + playback.button.setHoverFade(); + } + } + + private static int index = 1; + + public static PlaybackSpeed next() { + PlaybackSpeed next = values()[index++ % values().length]; + if((GameMod.DOUBLE_TIME.isActive() && next == PlaybackSpeed.DOUBLE)) + next = next(); + + return next; + } + + public static void reset() { + index = 1; + } + + /** + * Returns the button. + * @return the associated button + */ + public MenuButton getButton() { return button; } + + /** + * Returns the speed modifier. + * @return the speed + */ + public float getModifier() { return modifier; } +} + diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 094116f5..57156c58 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -50,6 +50,7 @@ import java.io.File; import java.util.LinkedList; import java.util.Stack; +import itdelatrisu.opsu.replay.PlaybackSpeed; import org.lwjgl.input.Keyboard; import org.lwjgl.opengl.Display; import org.newdawn.slick.Animation; @@ -131,6 +132,9 @@ public class Game extends BasicGameState { /** Skip button (displayed at song start, when necessary). */ private MenuButton skipButton; + /** Playback button (displayed in replays). */ + private MenuButton playbackButton; + /** Current timing point index in timingPoints ArrayList. */ private int timingPointIndex; @@ -527,6 +531,9 @@ public class Game extends BasicGameState { cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY); } + if (isReplay || GameMod.AUTO.isActive()) + playbackButton.draw(); + if (isReplay) UI.draw(g, replayX, replayY, replayKeyPressed); else if (GameMod.AUTO.isActive()) @@ -544,6 +551,8 @@ public class Game extends BasicGameState { UI.update(delta); int mouseX = input.getMouseX(), mouseY = input.getMouseY(); skipButton.hoverUpdate(delta, mouseX, mouseY); + if (isReplay || GameMod.AUTO.isActive()) + playbackButton.hoverUpdate(delta, mouseX, mouseY); int trackPosition = MusicController.getPosition(); // returning from pause screen: must click previous mouse position @@ -871,17 +880,25 @@ public class Game extends BasicGameState { @Override public void mousePressed(int button, int x, int y) { - if (Options.isMouseDisabled()) - return; - // watching replay - if (isReplay) { - // only allow skip button - if (button != Input.MOUSE_MIDDLE_BUTTON && skipButton.contains(x, y)) + if (isReplay || GameMod.AUTO.isActive()) { + // allow skip button + if (button != Input.MOUSE_MIDDLE_BUTTON && skipButton.contains(x, y)) { skipIntro(); + return; + } + if (button != Input.MOUSE_MIDDLE_BUTTON && playbackButton.contains(x, y)) { + PlaybackSpeed playbackSpeed = PlaybackSpeed.next(); + playbackButton = playbackSpeed.getButton(); + MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier()); + return; + } return; } + if (Options.isMouseDisabled()) + return; + // mouse wheel: pause the game if (button == Input.MOUSE_MIDDLE_BUTTON && !Options.isMouseWheelDisabled()) { int trackPosition = MusicController.getPosition(); @@ -1023,11 +1040,6 @@ public class Game extends BasicGameState { // reset game data resetGameData(); - // needs to play before setting position to resume without lag later - MusicController.play(false); - MusicController.setPosition(0); - MusicController.pause(); - // initialize object maps for (int i = 0; i < osu.objects.length; i++) { OsuHitObject hitObject = osu.objects[i]; @@ -1090,6 +1102,8 @@ public class Game extends BasicGameState { previousMods = GameMod.getModState(); GameMod.loadModState(replay.mods); + PlaybackSpeed.reset(); + // load initial data replayX = container.getWidth() / 2; replayY = container.getHeight() / 2; @@ -1119,9 +1133,17 @@ public class Game extends BasicGameState { leadInTime = osu.audioLeadIn + approachTime; restart = Restart.FALSE; + + // needs to play before setting position to resume without lag later + MusicController.play(false); + MusicController.setPosition(0); + MusicController.setPitch(GameMod.getSpeedMultiplier()); + MusicController.pause(); } skipButton.resetHover(); + if (isReplay || GameMod.AUTO.isActive()) + playbackButton.resetHover(); } @Override @@ -1136,6 +1158,9 @@ public class Game extends BasicGameState { // replays if (isReplay) GameMod.loadModState(previousMods); + + // reset playback speed + MusicController.setPitch(1f); } /** @@ -1280,6 +1305,7 @@ public class Game extends BasicGameState { MusicController.resume(); } MusicController.setPosition(firstObjectTime - SKIP_OFFSET); + MusicController.setPitch(GameMod.getSpeedMultiplier()); replaySkipTime = (isReplay) ? -1 : trackPosition; if (isReplay) { replayX = (int) skipButton.getX(); @@ -1317,6 +1343,9 @@ public class Game extends BasicGameState { } skipButton.setHoverExpand(1.1f, MenuButton.Expand.UP_LEFT); + if (isReplay || GameMod.AUTO.isActive()) + playbackButton = PlaybackSpeed.NORMAL.getButton(); + // load other images... ((GamePauseMenu) game.getState(Opsu.STATE_GAMEPAUSEMENU)).loadImages(); data.loadImages(); diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index 87704504..ed2cace2 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -337,11 +337,7 @@ public class SongMenu extends BasicGameState { // song info text if (songInfo == null) { - songInfo = focusNode.getInfo(); - if (Options.useUnicodeMetadata()) { // load glyphs - OsuFile osu = focusNode.osuFiles.get(0); - Utils.loadGlyphs(Utils.FONT_LARGE, osu.titleUnicode, osu.artistUnicode); - } + songInfo = getSongInfo(); } marginX += 5; float headerTextY = marginY; @@ -349,7 +345,8 @@ public class SongMenu extends BasicGameState { headerTextY += Utils.FONT_LARGE.getLineHeight() - 8; Utils.FONT_DEFAULT.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[1], Color.white); headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 2; - Utils.FONT_BOLD.drawString(marginX, headerTextY, songInfo[2], Color.white); + Utils.FONT_BOLD.drawString(marginX, headerTextY, songInfo[2], + (GameMod.DOUBLE_TIME.isActive()) ? Color.red : (GameMod.HALF_TIME.isActive()) ? Color.green : Color.white); headerTextY += Utils.FONT_BOLD.getLineHeight() - 4; Utils.FONT_DEFAULT.drawString(marginX, headerTextY, songInfo[3], Color.white); headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 4; @@ -982,6 +979,9 @@ public class SongMenu extends BasicGameState { resetGame = false; } + // load song info + songInfo = getSongInfo(); + // state-based action if (stateAction != null) { switch (stateAction) { @@ -1302,6 +1302,20 @@ public class SongMenu extends BasicGameState { return null; // incorrect map } + /** + * Returns an array of strings containing song information. + * @return the String array + */ + private String[] getSongInfo () { + songInfo = focusNode.getInfo(); + if (Options.useUnicodeMetadata()) { // load glyphs + OsuFile osu = focusNode.osuFiles.get(0); + Utils.loadGlyphs(Utils.FONT_LARGE, osu.titleUnicode, osu.artistUnicode); + } + return songInfo; + } + + /** * Starts the game. */