From 2167698740086dd06ffb242bf203a6f14eeb09e8 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 2 Jul 2015 22:16:14 -0500 Subject: [PATCH 001/104] Follow-up to #99: replay seeking improvements. - Added on/off option for replay seeking in the "custom" menu. - Mute sounds while seeking. - Draw a bar on the left of the screen during replays for seeking (instead of just clicking near the top of the screen). Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/Options.java | 9 ++- .../opsu/audio/SoundController.java | 11 +++- src/itdelatrisu/opsu/states/Game.java | 55 +++++++++++++++++-- src/itdelatrisu/opsu/states/OptionsMenu.java | 3 +- 4 files changed, 70 insertions(+), 8 deletions(-) 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. */ From 47f682352dd65cb322fb02907af219ba5e882472 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sat, 4 Jul 2015 23:29:46 -0500 Subject: [PATCH 002/104] Updating to version 0.10.0. Signed-off-by: Jeffrey Han --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4eb303ab..2921441f 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 itdelatrisu opsu - 0.9.0 + 0.10.0 ${maven.build.timestamp} yyyy-MM-dd HH:mm From 8b9e230fa75c3178f59dfee28ffbf2f127b589c3 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Tue, 7 Jul 2015 18:37:03 -0500 Subject: [PATCH 003/104] Updated dependencies. - LWJGL is now at the final 2.x release version. - Slick2D is now at the final (?) release version. - Added dependency for org.tukaani.xz, since it becomes optional in commons-compress 1.9 and is needed for LZMA compression. Signed-off-by: Jeffrey Han --- pom.xml | 15 ++++++++++----- src/org/newdawn/slick/Image.java | 3 +++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 2921441f..c3d33d3e 100644 --- a/pom.xml +++ b/pom.xml @@ -142,12 +142,12 @@ org.lwjgl.lwjgl lwjgl - 2.9.1 + 2.9.3 org.slick2d slick2d-core - 1.0.0 + 1.0.1 org.jcraft @@ -197,17 +197,22 @@ org.apache.maven maven-artifact - 3.0.3 + 3.3.3 org.apache.commons commons-compress - 1.8 + 1.9 + + + org.tukaani + xz + 1.5 com.github.jponge lzma-java - 1.2 + 1.3 diff --git a/src/org/newdawn/slick/Image.java b/src/org/newdawn/slick/Image.java index 1737a808..38d9e8be 100644 --- a/src/org/newdawn/slick/Image.java +++ b/src/org/newdawn/slick/Image.java @@ -595,6 +595,7 @@ public class Image implements Renderable { * @param y The y location to draw the image at * @param filter The color to filter with when drawing */ + @Override public void draw(float x, float y, Color filter) { init(); draw(x,y,width,height, filter); @@ -719,6 +720,7 @@ public class Image implements Renderable { * @param height * The height to render the image at */ + @Override public void draw(float x,float y,float width,float height) { init(); draw(x,y,width,height,Color.white); @@ -797,6 +799,7 @@ public class Image implements Renderable { * @param height The height to render the image at * @param filter The color to filter with while drawing */ + @Override public void draw(float x,float y,float width,float height,Color filter) { if (alpha != 1) { if (filter == null) { From 26ab61910e051dfb1dac5b8258b70bcf7df7ac8e Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Tue, 7 Jul 2015 19:03:54 -0500 Subject: [PATCH 004/104] Added option to disable automatic checking for updates. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/Opsu.java | 20 +++++++++++--------- src/itdelatrisu/opsu/Options.java | 9 ++++++++- src/itdelatrisu/opsu/states/OptionsMenu.java | 3 ++- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index 1f89dd5b..09e7026f 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -142,16 +142,18 @@ public class Opsu extends StateBasedGame { Updater.get().setUpdateInfo(args[0], args[1]); // check for updates - new Thread() { - @Override - public void run() { - try { - Updater.get().checkForUpdates(); - } catch (IOException e) { - Log.warn("Check for updates failed.", e); + if (!Options.isUpdaterDisabled()) { + new Thread() { + @Override + public void run() { + try { + Updater.get().checkForUpdates(); + } catch (IOException e) { + Log.warn("Check for updates failed.", e); + } } - } - }.start(); + }.start(); + } // start the game try { diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index dc81f1b0..4f15a3a7 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -454,7 +454,8 @@ public class Options { } }, 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); + REPLAY_SEEKING ("Replay Seeking", "ReplaySeeking", "Enable a seeking bar on the left side of the screen during replays.", false), + DISABLE_UPDATER ("Disable Automatic Updates", "DisableUpdater", "Disable automatic checking for updates upon starting opsu!.", false); /** Option name. */ private String name; @@ -965,6 +966,12 @@ public class Options { */ public static boolean isReplaySeekingEnabled() { return GameOption.REPLAY_SEEKING.getBooleanValue(); } + /** + * Returns whether or not automatic checking for updates is disabled. + * @return true if disabled + */ + public static boolean isUpdaterDisabled() { return GameOption.DISABLE_UPDATER.getBooleanValue(); } + /** * Sets the track checkpoint time, if within bounds. * @param time the track position (in ms) diff --git a/src/itdelatrisu/opsu/states/OptionsMenu.java b/src/itdelatrisu/opsu/states/OptionsMenu.java index 0c250719..b921e2ee 100644 --- a/src/itdelatrisu/opsu/states/OptionsMenu.java +++ b/src/itdelatrisu/opsu/states/OptionsMenu.java @@ -94,7 +94,8 @@ public class OptionsMenu extends BasicGameState { GameOption.FIXED_AR, GameOption.FIXED_OD, GameOption.CHECKPOINT, - GameOption.REPLAY_SEEKING + GameOption.REPLAY_SEEKING, + GameOption.DISABLE_UPDATER }); /** Total number of tabs. */ From 420f1fb02cbdbad90c28ed5c484f118e499d3fcd Mon Sep 17 00:00:00 2001 From: Peter Tissen Date: Wed, 8 Jul 2015 16:18:41 +0200 Subject: [PATCH 005/104] Backup and restore the viewport size when rendering sliders. Needed because Slick tends to allocate offscreen buffers for itself only with power of two textures, so it will use another viewport when rendering to its own offscreen buffers. --- src/itdelatrisu/opsu/render/CurveRenderState.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/itdelatrisu/opsu/render/CurveRenderState.java b/src/itdelatrisu/opsu/render/CurveRenderState.java index bd7a0914..1bd26bd3 100644 --- a/src/itdelatrisu/opsu/render/CurveRenderState.java +++ b/src/itdelatrisu/opsu/render/CurveRenderState.java @@ -24,6 +24,7 @@ import itdelatrisu.opsu.objects.curves.Vec2f; import java.nio.ByteBuffer; import java.nio.FloatBuffer; +import java.nio.IntBuffer; import org.lwjgl.BufferUtils; import org.lwjgl.opengl.GL11; @@ -114,9 +115,13 @@ public class CurveRenderState { mapping = cache.insert(hitObject); fbo = mapping; - int old_fb = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING); - int old_tex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + int oldFb = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING); + int oldTex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + //glGetInteger requires a buffer of size 16, even though just 4 + //values are returned in this specific case + IntBuffer oldViewport = BufferUtils.createIntBuffer(16); + GL11.glGetInteger(GL11.GL_VIEWPORT, oldViewport); GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fbo.getID()); GL11.glViewport(0, 0, fbo.width, fbo.height); GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); @@ -125,8 +130,9 @@ public class CurveRenderState { this.draw_curve(color, borderColor, curve); color.a = 1f; - GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_tex); - GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_fb); + GL11.glBindTexture(GL11.GL_TEXTURE_2D, oldTex); + GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, oldFb); + GL11.glViewport(oldViewport.get(0), oldViewport.get(1), oldViewport.get(2), oldViewport.get(3)); Utils.COLOR_WHITE_FADE.a = alpha; } From 7941a70238ad2a54e5f69359e10e6ff0f7813eb6 Mon Sep 17 00:00:00 2001 From: Peter Tissen Date: Thu, 9 Jul 2015 18:37:39 +0200 Subject: [PATCH 006/104] use EXT version of FBOs instead of the ARB version This doesn't actually make a difference in functionality. The issue is that the flashlight mod uses Slicks FBO functions which use the EXT version and Intel drivers generate the same FBO IDs twice if the EXT and ARB versions are mixed. --- .../opsu/render/CurveRenderState.java | 7 ++-- src/itdelatrisu/opsu/render/Rendertarget.java | 32 +++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/itdelatrisu/opsu/render/CurveRenderState.java b/src/itdelatrisu/opsu/render/CurveRenderState.java index 1bd26bd3..17a62e56 100644 --- a/src/itdelatrisu/opsu/render/CurveRenderState.java +++ b/src/itdelatrisu/opsu/render/CurveRenderState.java @@ -27,6 +27,7 @@ import java.nio.FloatBuffer; import java.nio.IntBuffer; import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.EXTFramebufferObject; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL13; import org.lwjgl.opengl.GL14; @@ -115,14 +116,14 @@ public class CurveRenderState { mapping = cache.insert(hitObject); fbo = mapping; - int oldFb = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING); + int oldFb = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT); int oldTex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); //glGetInteger requires a buffer of size 16, even though just 4 //values are returned in this specific case IntBuffer oldViewport = BufferUtils.createIntBuffer(16); GL11.glGetInteger(GL11.GL_VIEWPORT, oldViewport); - GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fbo.getID()); + EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fbo.getID()); GL11.glViewport(0, 0, fbo.width, fbo.height); GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); @@ -131,7 +132,7 @@ public class CurveRenderState { color.a = 1f; GL11.glBindTexture(GL11.GL_TEXTURE_2D, oldTex); - GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, oldFb); + EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, oldFb); GL11.glViewport(oldViewport.get(0), oldViewport.get(1), oldViewport.get(2), oldViewport.get(3)); Utils.COLOR_WHITE_FADE.a = alpha; } diff --git a/src/itdelatrisu/opsu/render/Rendertarget.java b/src/itdelatrisu/opsu/render/Rendertarget.java index c6f19387..a783b3ab 100644 --- a/src/itdelatrisu/opsu/render/Rendertarget.java +++ b/src/itdelatrisu/opsu/render/Rendertarget.java @@ -19,10 +19,8 @@ package itdelatrisu.opsu.render; import java.nio.ByteBuffer; +import org.lwjgl.opengl.EXTFramebufferObject; import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL20; -import org.lwjgl.opengl.GL30; -import org.lwjgl.opengl.GL32; /** * Represents a rendertarget. For now this maps to an OpenGL FBO via LWJGL. @@ -50,16 +48,16 @@ public class Rendertarget { private Rendertarget(int width, int height) { this.width = width; this.height = height; - fboID = GL30.glGenFramebuffers(); + fboID = EXTFramebufferObject.glGenFramebuffersEXT(); textureID = GL11.glGenTextures(); - depthBufferID = GL30.glGenRenderbuffers(); + depthBufferID = EXTFramebufferObject.glGenRenderbuffersEXT(); } /** * Bind this rendertarget as the primary framebuffer. */ public void bind() { - GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fboID); + EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fboID); } /** @@ -83,7 +81,7 @@ public class Rendertarget { * Bind the default framebuffer. */ public static void unbind() { - GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, 0); + EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 0); } /** @@ -93,8 +91,9 @@ public class Rendertarget { * @param height the height */ public static Rendertarget createRTTFramebuffer(int width, int height) { - int old_framebuffer = GL11.glGetInteger(GL30.GL_READ_FRAMEBUFFER_BINDING); + int old_framebuffer = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT); int old_texture = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + int old_drawbuffer = GL11.glGetInteger(GL11.GL_DRAW_BUFFER); Rendertarget buffer = new Rendertarget(width,height); buffer.bind(); @@ -104,16 +103,15 @@ public class Rendertarget { GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); - GL30.glBindRenderbuffer(GL30.GL_RENDERBUFFER, buffer.depthBufferID); - GL30.glRenderbufferStorage(GL30.GL_RENDERBUFFER, GL11.GL_DEPTH_COMPONENT, width, height); - GL30.glFramebufferRenderbuffer(GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_RENDERBUFFER, buffer.depthBufferID); + EXTFramebufferObject.glBindRenderbufferEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, buffer.depthBufferID); + EXTFramebufferObject.glRenderbufferStorageEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, GL11.GL_DEPTH_COMPONENT, width, height); + EXTFramebufferObject.glFramebufferRenderbufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_DEPTH_ATTACHMENT_EXT, EXTFramebufferObject.GL_RENDERBUFFER_EXT, buffer.depthBufferID); - GL32.glFramebufferTexture(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, fboTexture, 0); - GL20.glDrawBuffers(GL30.GL_COLOR_ATTACHMENT0); + EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, GL11.GL_TEXTURE_2D, fboTexture, 0); GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_texture); - GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_framebuffer); - + EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, old_framebuffer); + return buffer; } @@ -122,8 +120,8 @@ public class Rendertarget { * to use this rendertarget with OpenGL after calling this method. */ public void destroyRTT() { - GL30.glDeleteFramebuffers(fboID); - GL30.glDeleteRenderbuffers(depthBufferID); + EXTFramebufferObject.glDeleteFramebuffersEXT(fboID); + EXTFramebufferObject.glDeleteRenderbuffersEXT(depthBufferID); GL11.glDeleteTextures(textureID); } } From 05c7ac0a02d1f85927a39a0b7d29fb6ed31e9595 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 9 Jul 2015 11:59:53 -0500 Subject: [PATCH 007/104] Minor follow-up to #108. Set minimum OpenGL version for mmsliders to 3.0 (from 3.2) and removed an unused variable. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/objects/curves/Curve.java | 2 +- src/itdelatrisu/opsu/render/Rendertarget.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java index 0d2f4609..74ad3864 100644 --- a/src/itdelatrisu/opsu/objects/curves/Curve.java +++ b/src/itdelatrisu/opsu/objects/curves/Curve.java @@ -87,7 +87,7 @@ public abstract class Curve { Curve.borderColor = borderColor; ContextCapabilities capabilities = GLContext.getCapabilities(); - mmsliderSupported = capabilities.GL_EXT_framebuffer_object && capabilities.OpenGL32; + mmsliderSupported = capabilities.GL_EXT_framebuffer_object && capabilities.OpenGL30; if (mmsliderSupported) CurveRenderState.init(width, height, circleSize); else { diff --git a/src/itdelatrisu/opsu/render/Rendertarget.java b/src/itdelatrisu/opsu/render/Rendertarget.java index a783b3ab..abd16b9d 100644 --- a/src/itdelatrisu/opsu/render/Rendertarget.java +++ b/src/itdelatrisu/opsu/render/Rendertarget.java @@ -93,7 +93,6 @@ public class Rendertarget { public static Rendertarget createRTTFramebuffer(int width, int height) { int old_framebuffer = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT); int old_texture = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); - int old_drawbuffer = GL11.glGetInteger(GL11.GL_DRAW_BUFFER); Rendertarget buffer = new Rendertarget(width,height); buffer.bind(); @@ -111,7 +110,7 @@ public class Rendertarget { GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_texture); EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, old_framebuffer); - + return buffer; } From 4e2074e41bc399c848850bfebc2af79b5579ed9a Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sat, 11 Jul 2015 10:51:52 -0500 Subject: [PATCH 008/104] Show errors if any directories could not be created. (fixes #97) Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/Options.java | 22 +++++++++++++++------- src/itdelatrisu/opsu/Utils.java | 8 +++----- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index 4f15a3a7..a4b84447 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -140,8 +140,8 @@ public class Options { rootPath = String.format("%s/%s", home, fallback); } File dir = new File(rootPath, "opsu"); - if (!dir.isDirectory()) - dir.mkdir(); + if (!dir.isDirectory() && !dir.mkdir()) + ErrorHandler.error(String.format("Failed to create configuration folder at '%s/opsu'.", rootPath), null, false); return dir; } else return new File("./"); @@ -357,7 +357,7 @@ public class Options { public String getValueString() { return String.format("%dms", val); } }, DISABLE_SOUNDS ("Disable All Sound Effects", "DisableSound", "May resolve Linux sound driver issues. Requires a restart.", - (System.getProperty("os.name").toLowerCase().indexOf("linux") > -1)), + (System.getProperty("os.name").toLowerCase().contains("linux"))), KEY_LEFT ("Left Game Key", "keyOsuLeft", "Select this option to input a key.") { @Override public String getValueString() { return Keyboard.getKeyName(getGameKeyLeft()); } @@ -1094,7 +1094,10 @@ public class Options { if (beatmapDir.isDirectory()) return beatmapDir; } - beatmapDir.mkdir(); // none found, create new directory + + // none found, create new directory + if (!beatmapDir.mkdir()) + ErrorHandler.error(String.format("Failed to create beatmap directory at '%s'.", beatmapDir.getAbsolutePath()), null, false); return beatmapDir; } @@ -1108,7 +1111,8 @@ public class Options { return oszDir; oszDir = new File(DATA_DIR, "SongPacks/"); - oszDir.mkdir(); + if (!oszDir.isDirectory() && !oszDir.mkdir()) + ErrorHandler.error(String.format("Failed to create song packs directory at '%s'.", oszDir.getAbsolutePath()), null, false); return oszDir; } @@ -1122,7 +1126,8 @@ public class Options { return replayImportDir; replayImportDir = new File(DATA_DIR, "ReplayImport/"); - replayImportDir.mkdir(); + if (!replayImportDir.isDirectory() && !replayImportDir.mkdir()) + ErrorHandler.error(String.format("Failed to create replay import directory at '%s'.", replayImportDir.getAbsolutePath()), null, false); return replayImportDir; } @@ -1167,7 +1172,10 @@ public class Options { if (skinRootDir.isDirectory()) return skinRootDir; } - skinRootDir.mkdir(); // none found, create new directory + + // none found, create new directory + if (!skinRootDir.mkdir()) + ErrorHandler.error(String.format("Failed to create skins directory at '%s'.", skinRootDir.getAbsolutePath()), null, false); return skinRootDir; } diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index feb24d44..9e24be47 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -289,11 +289,9 @@ public class Utils { public static void takeScreenShot() { // create the screenshot directory File dir = Options.getScreenshotDir(); - if (!dir.isDirectory()) { - if (!dir.mkdir()) { - ErrorHandler.error("Failed to create screenshot directory.", null, false); - return; - } + if (!dir.isDirectory() && !dir.mkdir()) { + ErrorHandler.error(String.format("Failed to create screenshot directory at '%s'.", dir.getAbsolutePath()), null, false); + return; } // create file name From 5dac21a5458bb2f3f963926f642a47d69628b72b Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 16 Jul 2015 18:14:46 -0500 Subject: [PATCH 009/104] Cursor trail now considers actual FPS (not target FPS). (fixes #109) Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/ui/Cursor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/itdelatrisu/opsu/ui/Cursor.java b/src/itdelatrisu/opsu/ui/Cursor.java index 252a9c2c..a35c405c 100644 --- a/src/itdelatrisu/opsu/ui/Cursor.java +++ b/src/itdelatrisu/opsu/ui/Cursor.java @@ -125,7 +125,7 @@ public class Cursor { cursorMiddle = GameImage.CURSOR_MIDDLE.getImage(); int removeCount = 0; - int FPSmod = (Options.getTargetFPS() / 60); + float FPSmod = Math.max(container.getFPS(), 1) / 60f; Skin skin = Options.getSkin(); // scale cursor @@ -151,13 +151,13 @@ public class Cursor { lastX = mouseX; lastY = mouseY; - removeCount = (cursorX.size() / (6 * FPSmod)) + 1; + removeCount = (int) (cursorX.size() / (6 * FPSmod)) + 1; } else { // old style: sample one point at a time cursorX.add(mouseX); cursorY.add(mouseY); - int max = 10 * FPSmod; + int max = (int) (10 * FPSmod); if (cursorX.size() > max) removeCount = cursorX.size() - max; } From 6a4c6a8d37844a8263bad8537c4227f991ff2f05 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sat, 18 Jul 2015 23:55:06 -0500 Subject: [PATCH 010/104] Updating to version 0.10.1. Signed-off-by: Jeffrey Han --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c3d33d3e..bfaaef28 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 itdelatrisu opsu - 0.10.0 + 0.10.1 ${maven.build.timestamp} yyyy-MM-dd HH:mm From c91146b024fb218405c460eca007563db9d38e65 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Wed, 5 Aug 2015 22:28:14 -0500 Subject: [PATCH 011/104] Added easing functions for all-around better animations. These are Robert Penner's easing functions (http://robertpenner.com/easing/), refactored by CharlotteGore to only take a t parameter (https://github.com/CharlotteGore/functional-easing). Licensed under BSD (the former) and MIT (the latter). Related changes: - Added "AnimatedValue" utility class for updating values used in animations. - MenuButton now uses AnimatedValue to handle its animations (still linear by default). - Added in-out-back easings on logo, mods, and various other elements; added out-bounce easings on button menu. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/GameMod.java | 3 + src/itdelatrisu/opsu/Utils.java | 16 + src/itdelatrisu/opsu/states/ButtonMenu.java | 24 +- src/itdelatrisu/opsu/states/Game.java | 3 + .../opsu/states/GamePauseMenu.java | 9 + src/itdelatrisu/opsu/states/MainMenu.java | 20 +- src/itdelatrisu/opsu/ui/MenuButton.java | 155 +++++---- src/itdelatrisu/opsu/ui/UI.java | 3 + .../opsu/ui/animations/AnimatedValue.java | 124 +++++++ .../opsu/ui/animations/AnimationEquation.java | 309 ++++++++++++++++++ 10 files changed, 588 insertions(+), 78 deletions(-) create mode 100644 src/itdelatrisu/opsu/ui/animations/AnimatedValue.java create mode 100644 src/itdelatrisu/opsu/ui/animations/AnimationEquation.java diff --git a/src/itdelatrisu/opsu/GameMod.java b/src/itdelatrisu/opsu/GameMod.java index ff7c86a0..e6adb54c 100644 --- a/src/itdelatrisu/opsu/GameMod.java +++ b/src/itdelatrisu/opsu/GameMod.java @@ -19,6 +19,7 @@ package itdelatrisu.opsu; import itdelatrisu.opsu.ui.MenuButton; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.util.Arrays; import java.util.Collections; @@ -199,6 +200,8 @@ public enum GameMod { mod.button = new MenuButton(img, baseX + (offsetX * mod.categoryIndex) + img.getWidth() / 2f, mod.category.getY()); + mod.button.setHoverAnimationDuration(300); + mod.button.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK); mod.button.setHoverExpand(1.2f); mod.button.setHoverRotate(10f); diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 9e24be47..1737a10e 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -239,6 +239,22 @@ public class Utils { return val; } + /** + * Clamps a value between a lower and upper bound. + * @param val the value to clamp + * @param low the lower bound + * @param high the upper bound + * @return the clamped value + * @author fluddokt + */ + public static int clamp(int val, int low, int high) { + if (val < low) + return low; + if (val > high) + return high; + return val; + } + /** * Clamps a value between a lower and upper bound. * @param val the value to clamp diff --git a/src/itdelatrisu/opsu/states/ButtonMenu.java b/src/itdelatrisu/opsu/states/ButtonMenu.java index c27f0b67..de363c57 100644 --- a/src/itdelatrisu/opsu/states/ButtonMenu.java +++ b/src/itdelatrisu/opsu/states/ButtonMenu.java @@ -31,6 +31,8 @@ import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.beatmap.BeatmapSetNode; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimatedValue; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.util.ArrayList; import java.util.List; @@ -261,8 +263,11 @@ public class ButtonMenu extends BasicGameState { /** The actual title string list, generated upon entering the state. */ private List actualTitle; + /** The horizontal center offset, used for the initial button animation. */ + private AnimatedValue centerOffset; + /** Initial x coordinate offsets left/right of center (for shifting animation), times width. (TODO) */ - private static final float OFFSET_WIDTH_RATIO = 1 / 18f; + private static final float OFFSET_WIDTH_RATIO = 1 / 25f; /** * Constructor. @@ -336,18 +341,14 @@ public class ButtonMenu extends BasicGameState { */ public void update(GameContainer container, int delta, int mouseX, int mouseY) { float center = container.getWidth() / 2f; + boolean centerOffsetUpdated = centerOffset.update(delta); + float centerOffsetX = centerOffset.getValue(); for (int i = 0; i < buttons.length; i++) { menuButtons[i].hoverUpdate(delta, mouseX, mouseY); // move button to center - float x = menuButtons[i].getX(); - if (i % 2 == 0) { - if (x < center) - menuButtons[i].setX(Math.min(x + (delta / 5f), center)); - } else { - if (x > center) - menuButtons[i].setX(Math.max(x - (delta / 5f), center)); - } + if (centerOffsetUpdated) + menuButtons[i].setX((i % 2 == 0) ? center + centerOffsetX : center - centerOffsetX); } } @@ -404,9 +405,10 @@ public class ButtonMenu extends BasicGameState { */ public void enter(GameContainer container, StateBasedGame game) { float center = container.getWidth() / 2f; - float centerOffset = container.getWidth() * OFFSET_WIDTH_RATIO; + float centerOffsetX = container.getWidth() * OFFSET_WIDTH_RATIO; + centerOffset = new AnimatedValue(700, centerOffsetX, 0, AnimationEquation.OUT_BOUNCE); for (int i = 0; i < buttons.length; i++) { - menuButtons[i].setX(center + ((i % 2 == 0) ? centerOffset * -1 : centerOffset)); + menuButtons[i].setX(center + ((i % 2 == 0) ? centerOffsetX : centerOffsetX * -1)); menuButtons[i].resetHover(); } diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 6d2f0cdd..bfc65705 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -48,6 +48,7 @@ import itdelatrisu.opsu.replay.Replay; import itdelatrisu.opsu.replay.ReplayFrame; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.io.File; import java.util.LinkedList; @@ -1425,6 +1426,8 @@ public class Game extends BasicGameState { Image skip = GameImage.SKIP.getImage(); skipButton = new MenuButton(skip, width - skip.getWidth() / 2f, height - (skip.getHeight() / 2f)); } + skipButton.setHoverAnimationDuration(350); + skipButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK); skipButton.setHoverExpand(1.1f, MenuButton.Expand.UP_LEFT); // load other images... diff --git a/src/itdelatrisu/opsu/states/GamePauseMenu.java b/src/itdelatrisu/opsu/states/GamePauseMenu.java index e73e5a2a..7a01c492 100644 --- a/src/itdelatrisu/opsu/states/GamePauseMenu.java +++ b/src/itdelatrisu/opsu/states/GamePauseMenu.java @@ -27,6 +27,7 @@ import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import org.lwjgl.input.Keyboard; import org.newdawn.slick.Color; @@ -227,6 +228,14 @@ public class GamePauseMenu extends BasicGameState { continueButton = new MenuButton(GameImage.PAUSE_CONTINUE.getImage(), width / 2f, height * 0.25f); retryButton = new MenuButton(GameImage.PAUSE_RETRY.getImage(), width / 2f, height * 0.5f); backButton = new MenuButton(GameImage.PAUSE_BACK.getImage(), width / 2f, height * 0.75f); + final int buttonAnimationDuration = 300; + continueButton.setHoverAnimationDuration(buttonAnimationDuration); + retryButton.setHoverAnimationDuration(buttonAnimationDuration); + backButton.setHoverAnimationDuration(buttonAnimationDuration); + final AnimationEquation buttonAnimationEquation = AnimationEquation.IN_OUT_BACK; + continueButton.setHoverAnimationEquation(buttonAnimationEquation); + retryButton.setHoverAnimationEquation(buttonAnimationEquation); + backButton.setHoverAnimationEquation(buttonAnimationEquation); continueButton.setHoverExpand(); retryButton.setHoverExpand(); backButton.setHoverExpand(); diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index be484887..dd022368 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -33,6 +33,7 @@ import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.states.ButtonMenu.MenuState; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.MenuButton.Expand; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import itdelatrisu.opsu.ui.UI; import java.awt.Desktop; @@ -145,9 +146,18 @@ public class MainMenu extends BasicGameState { exitButton = new MenuButton(exitImg, width * 0.75f - exitOffset, (height / 2) + (exitImg.getHeight() / 2f) ); - logo.setHoverExpand(1.05f); - playButton.setHoverExpand(1.05f); - exitButton.setHoverExpand(1.05f); + final int logoAnimationDuration = 350; + logo.setHoverAnimationDuration(logoAnimationDuration); + playButton.setHoverAnimationDuration(logoAnimationDuration); + exitButton.setHoverAnimationDuration(logoAnimationDuration); + final AnimationEquation logoAnimationEquation = AnimationEquation.IN_OUT_BACK; + logo.setHoverAnimationEquation(logoAnimationEquation); + playButton.setHoverAnimationEquation(logoAnimationEquation); + exitButton.setHoverAnimationEquation(logoAnimationEquation); + final float logoHoverScale = 1.1f; + logo.setHoverExpand(logoHoverScale); + playButton.setHoverExpand(logoHoverScale); + exitButton.setHoverExpand(logoHoverScale); // initialize music buttons int musicWidth = GameImage.MUSIC_PLAY.getImage().getWidth(); @@ -170,6 +180,8 @@ public class MainMenu extends BasicGameState { // initialize downloads button Image dlImg = GameImage.DOWNLOADS.getImage(); downloadsButton = new MenuButton(dlImg, width - dlImg.getWidth() / 2f, height / 2f); + downloadsButton.setHoverAnimationDuration(350); + downloadsButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK); downloadsButton.setHoverExpand(1.03f, Expand.LEFT); // initialize repository button @@ -179,6 +191,8 @@ public class MainMenu extends BasicGameState { repoButton = new MenuButton(repoImg, startX - repoImg.getWidth(), startY - repoImg.getHeight() ); + repoButton.setHoverAnimationDuration(350); + repoButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK); repoButton.setHoverExpand(); startX -= repoImg.getWidth() * 1.75f; } else diff --git a/src/itdelatrisu/opsu/ui/MenuButton.java b/src/itdelatrisu/opsu/ui/MenuButton.java index 7370d0bc..3491cdf3 100644 --- a/src/itdelatrisu/opsu/ui/MenuButton.java +++ b/src/itdelatrisu/opsu/ui/MenuButton.java @@ -18,7 +18,8 @@ package itdelatrisu.opsu.ui; -import itdelatrisu.opsu.Utils; +import itdelatrisu.opsu.ui.animations.AnimatedValue; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import org.newdawn.slick.Animation; import org.newdawn.slick.Color; @@ -63,11 +64,23 @@ public class MenuButton { /** The hover actions for this button. */ private int hoverEffect = 0; - /** The current and max scale of the button. */ - private float scale = 1f, hoverScale = 1.25f; + /** The hover animation duration, in milliseconds. */ + private int animationDuration = 100; - /** The current and base alpha level of the button. */ - private float alpha = 1f, baseAlpha = 0.75f; + /** The hover animation equation. */ + private AnimationEquation animationEqn = AnimationEquation.LINEAR; + + /** The scale of the button. */ + private AnimatedValue scale; + + /** The default max scale of the button. */ + private static final float DEFAULT_SCALE_MAX = 1.25f; + + /** The alpha level of the button. */ + private AnimatedValue alpha; + + /** The default base alpha level of the button. */ + private static final float DEFAULT_ALPHA_BASE = 0.75f; /** The scaled expansion direction for the button. */ private Expand dir = Expand.CENTER; @@ -75,8 +88,11 @@ public class MenuButton { /** Scaled expansion directions. */ public enum Expand { CENTER, UP, RIGHT, LEFT, DOWN, UP_RIGHT, UP_LEFT, DOWN_RIGHT, DOWN_LEFT; } - /** The current and max rotation angles of the button. */ - private float angle = 0f, maxAngle = 30f; + /** The rotation angle of the button. */ + private AnimatedValue angle; + + /** The default max rotation angle of the button. */ + private static final float DEFAULT_ANGLE_MAX = 30f; /** * Creates a new button from an Image. @@ -192,15 +208,15 @@ public class MenuButton { float oldAlpha = image.getAlpha(); float oldAngle = image.getRotation(); if ((hoverEffect & EFFECT_EXPAND) > 0) { - if (scale != 1f) { - image = image.getScaledCopy(scale); + if (scale.getValue() != 1f) { + image = image.getScaledCopy(scale.getValue()); image.setAlpha(oldAlpha); } } if ((hoverEffect & EFFECT_FADE) > 0) - image.setAlpha(alpha); + image.setAlpha(alpha.getValue()); if ((hoverEffect & EFFECT_ROTATE) > 0) - image.setRotation(angle); + image.setRotation(angle.getValue()); image.draw(x - xRadius, y - yRadius, filter); if (image == this.img) { image.setAlpha(oldAlpha); @@ -217,9 +233,10 @@ public class MenuButton { imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter); } else if ((hoverEffect & EFFECT_FADE) > 0) { float a = image.getAlpha(), aL = imgL.getAlpha(), aR = imgR.getAlpha(); - image.setAlpha(alpha); - imgL.setAlpha(alpha); - imgR.setAlpha(alpha); + float currentAlpha = alpha.getValue(); + image.setAlpha(currentAlpha); + imgL.setAlpha(currentAlpha); + imgR.setAlpha(currentAlpha); image.draw(x - xRadius + imgL.getWidth(), y - yRadius, filter); imgL.draw(x - xRadius, y - yRadius, filter); imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter); @@ -267,28 +284,61 @@ public class MenuButton { */ public void resetHover() { if ((hoverEffect & EFFECT_EXPAND) > 0) { - this.scale = 1f; + scale.setTime(0); setHoverRadius(); } if ((hoverEffect & EFFECT_FADE) > 0) - this.alpha = baseAlpha; + alpha.setTime(0); if ((hoverEffect & EFFECT_ROTATE) > 0) - this.angle = 0f; + angle.setTime(0); } /** * Removes all hover effects that have been set for the button. */ - public void removeHoverEffects() { hoverEffect = 0; } + public void removeHoverEffects() { + this.hoverEffect = 0; + this.scale = null; + this.alpha = null; + this.angle = null; + } + + /** + * Sets the hover animation duration. + * @param duration the duration, in milliseconds + */ + public void setHoverAnimationDuration(int duration) { + this.animationDuration = duration; + if (scale != null) + scale.setDuration(duration); + if (alpha != null) + alpha.setDuration(duration); + if (angle != null) + angle.setDuration(duration); + } + + /** + * Sets the hover animation equation. + * @param eqn the equation to use + */ + public void setHoverAnimationEquation(AnimationEquation eqn) { + this.animationEqn = eqn; + if (scale != null) + scale.setEquation(eqn); + if (alpha != null) + alpha.setEquation(eqn); + if (angle != null) + angle.setEquation(eqn); + } /** * Sets the "expand" hover effect. */ - public void setHoverExpand() { hoverEffect |= EFFECT_EXPAND; } + public void setHoverExpand() { setHoverExpand(DEFAULT_SCALE_MAX, this.dir); } /** * Sets the "expand" hover effect. - * @param scale the maximum scale factor (default 1.25f) + * @param scale the maximum scale factor */ public void setHoverExpand(float scale) { setHoverExpand(scale, this.dir); } @@ -296,45 +346,45 @@ public class MenuButton { * Sets the "expand" hover effect. * @param dir the expansion direction */ - public void setHoverExpand(Expand dir) { setHoverExpand(this.hoverScale, dir); } + public void setHoverExpand(Expand dir) { setHoverExpand(DEFAULT_SCALE_MAX, dir); } /** * Sets the "expand" hover effect. - * @param scale the maximum scale factor (default 1.25f) + * @param scale the maximum scale factor * @param dir the expansion direction */ public void setHoverExpand(float scale, Expand dir) { hoverEffect |= EFFECT_EXPAND; - this.hoverScale = scale; + this.scale = new AnimatedValue(animationDuration, 1f, scale, animationEqn); this.dir = dir; } /** * Sets the "fade" hover effect. */ - public void setHoverFade() { hoverEffect |= EFFECT_FADE; } + public void setHoverFade() { setHoverFade(DEFAULT_ALPHA_BASE); } /** * Sets the "fade" hover effect. - * @param baseAlpha the base alpha level to fade in from (default 0.7f) + * @param baseAlpha the base alpha level to fade in from */ public void setHoverFade(float baseAlpha) { hoverEffect |= EFFECT_FADE; - this.baseAlpha = baseAlpha; + this.alpha = new AnimatedValue(animationDuration, baseAlpha, 1f, animationEqn); } /** * Sets the "rotate" hover effect. */ - public void setHoverRotate() { hoverEffect |= EFFECT_ROTATE; } + public void setHoverRotate() { setHoverRotate(DEFAULT_ANGLE_MAX); } /** * Sets the "rotate" hover effect. - * @param maxAngle the maximum rotation angle, in degrees (default 30f) + * @param maxAngle the maximum rotation angle, in degrees */ public void setHoverRotate(float maxAngle) { hoverEffect |= EFFECT_ROTATE; - this.maxAngle = maxAngle; + this.angle = new AnimatedValue(animationDuration, 0f, maxAngle, animationEqn); } /** @@ -371,45 +421,21 @@ public class MenuButton { if (hoverEffect == 0) return; + int d = delta * (isHover ? 1 : -1); + // scale the button if ((hoverEffect & EFFECT_EXPAND) > 0) { - int sign = 0; - if (isHover && scale < hoverScale) - sign = 1; - else if (!isHover && scale > 1f) - sign = -1; - if (sign != 0) { - scale = Utils.getBoundedValue(scale, sign * (hoverScale - 1f) * delta / 100f, 1, hoverScale); + if (scale.update(d)) setHoverRadius(); - } } // fade the button - if ((hoverEffect & EFFECT_FADE) > 0) { - int sign = 0; - if (isHover && alpha < 1f) - sign = 1; - else if (!isHover && alpha > baseAlpha) - sign = -1; - if (sign != 0) - alpha = Utils.getBoundedValue(alpha, sign * (1f - baseAlpha) * delta / 200f, baseAlpha, 1f); - } + if ((hoverEffect & EFFECT_FADE) > 0) + alpha.update(d); // rotate the button - if ((hoverEffect & EFFECT_ROTATE) > 0) { - int sign = 0; - boolean right = (maxAngle > 0); - if (isHover && angle != maxAngle) - sign = (right) ? 1 : -1; - else if (!isHover && angle != 0) - sign = (right) ? -1 : 1; - if (sign != 0) { - float diff = sign * Math.abs(maxAngle) * delta / 125f; - angle = (right) ? - Utils.getBoundedValue(angle, diff, 0, maxAngle) : - Utils.getBoundedValue(angle, diff, maxAngle, 0); - } - } + if ((hoverEffect & EFFECT_ROTATE) > 0) + angle.update(d); } /** @@ -422,10 +448,11 @@ public class MenuButton { image = anim.getCurrentFrame(); int xOffset = 0, yOffset = 0; + float currentScale = scale.getValue(); if (dir != Expand.CENTER) { // offset by difference between normal/scaled image dimensions - xOffset = (int) ((scale - 1f) * image.getWidth()); - yOffset = (int) ((scale - 1f) * image.getHeight()); + xOffset = (int) ((currentScale - 1f) * image.getWidth()); + yOffset = (int) ((currentScale - 1f) * image.getHeight()); if (dir == Expand.UP || dir == Expand.DOWN) xOffset = 0; // no horizontal offset if (dir == Expand.RIGHT || dir == Expand.LEFT) @@ -435,7 +462,7 @@ public class MenuButton { if (dir == Expand.DOWN || dir == Expand.DOWN_LEFT || dir == Expand.DOWN_RIGHT) yOffset *= -1; // flip y for down } - this.xRadius = ((image.getWidth() * scale) + xOffset) / 2f; - this.yRadius = ((image.getHeight() * scale) + yOffset) / 2f; + this.xRadius = ((image.getWidth() * currentScale) + xOffset) / 2f; + this.yRadius = ((image.getHeight() * currentScale) + yOffset) / 2f; } } diff --git a/src/itdelatrisu/opsu/ui/UI.java b/src/itdelatrisu/opsu/ui/UI.java index 91ee4c9a..ac198af0 100644 --- a/src/itdelatrisu/opsu/ui/UI.java +++ b/src/itdelatrisu/opsu/ui/UI.java @@ -26,6 +26,7 @@ import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.beatmap.BeatmapParser; import itdelatrisu.opsu.replay.ReplayImporter; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import javax.swing.JOptionPane; import javax.swing.UIManager; @@ -106,6 +107,8 @@ public class UI { Image back = GameImage.MENU_BACK.getImage(); backButton = new MenuButton(back, back.getWidth() / 2f, container.getHeight() - (back.getHeight() / 2f)); } + backButton.setHoverAnimationDuration(350); + backButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK); backButton.setHoverExpand(MenuButton.Expand.UP_RIGHT); } diff --git a/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java b/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java new file mode 100644 index 00000000..5e1636f9 --- /dev/null +++ b/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java @@ -0,0 +1,124 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015 Jeffrey Han + * + * opsu! is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * opsu! is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with opsu!. If not, see . + */ + +package itdelatrisu.opsu.ui.animations; + +import itdelatrisu.opsu.Utils; + +/** + * Utility class for updating a value using an animation equation. + */ +public class AnimatedValue { + /** The animation duration, in milliseconds. */ + private int duration; + + /** The current time, in milliseconds. */ + private int time; + + /** The base value. */ + private float base; + + /** The maximum difference from the base value. */ + private float diff; + + /** The current value. */ + private float value; + + /** The animation equation to use. */ + private AnimationEquation eqn; + + /** + * Constructor. + * @param duration the total animation duration, in milliseconds + * @param min the minimum value + * @param max the maximum value + * @param eqn the animation equation to use + */ + public AnimatedValue(int duration, float min, float max, AnimationEquation eqn) { + this.time = 0; + this.duration = duration; + this.value = min; + this.base = min; + this.diff = max - min; + this.eqn = eqn; + } + + /** + * Returns the current value. + */ + public float getValue() { return value; } + + /** + * Returns the current animation time, in milliseconds. + */ + public int getTime() { return time; } + + /** + * Sets the animation time manually. + * @param time the new time, in milliseconds + */ + public void setTime(int time) { + this.time = Utils.clamp(time, 0, duration); + updateValue(); + } + + /** + * Sets the animation duration. + * @param duration the new duration, in milliseconds + */ + public void setDuration(int duration) { + this.duration = duration; + int newTime = Utils.clamp(time, 0, duration); + if (time != newTime) { + this.time = newTime; + updateValue(); + } + } + + /** + * Sets the animation equation to use. + * @param eqn the new equation + */ + public void setEquation(AnimationEquation eqn) { + this.eqn = eqn; + updateValue(); + } + + /** + * Updates the animation by a delta interval. + * @param delta the delta interval since the last call. + * @return true if an update was applied, false if the animation was not updated + */ + public boolean update(int delta) { + int newTime = Utils.getBoundedValue(time, delta, 0, duration); + if (time != newTime) { + this.time = newTime; + updateValue(); + return true; + } + return false; + } + + /** + * Recalculates the value by applying the animation equation with the current time. + */ + private void updateValue() { + float t = eqn.calc((float) time / duration); + this.value = base + (t * diff); + } +} diff --git a/src/itdelatrisu/opsu/ui/animations/AnimationEquation.java b/src/itdelatrisu/opsu/ui/animations/AnimationEquation.java new file mode 100644 index 00000000..a30c5b30 --- /dev/null +++ b/src/itdelatrisu/opsu/ui/animations/AnimationEquation.java @@ -0,0 +1,309 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015 Jeffrey Han + * + * opsu! is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * opsu! is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with opsu!. If not, see . + */ + +package itdelatrisu.opsu.ui.animations; + + +/* + * These equations are copyright (c) 2001 Robert Penner, all rights reserved, + * and are open source under the BSD License. + * http://www.opensource.org/licenses/bsd-license.php + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the author nor the names of contributors may be used + * to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Easing functions for animations. + * + * @author Robert Penner (http://robertpenner.com/easing/) + * @author CharlotteGore (https://github.com/CharlotteGore/functional-easing) + */ +public enum AnimationEquation { + /* Linear */ + LINEAR { + @Override + public float calc(float t) { return t; } + }, + + /* Quadratic */ + IN_QUAD { + @Override + public float calc(float t) { return t * t; } + }, + OUT_QUAD { + @Override + public float calc(float t) { return -1 * t * (t - 2); } + }, + IN_OUT_QUAD { + @Override + public float calc(float t) { + t = t * 2; + if (t < 1) + return 0.5f * t * t; + t = t - 1; + return -0.5f * (t * (t - 2) - 1); + } + }, + + /* Cubic */ + IN_CUBIC { + @Override + public float calc(float t) { return t * t * t; } + }, + OUT_CUBIC { + @Override + public float calc(float t) { + t = t - 1; + return t * t * t + 1; + } + }, + IN_OUT_CUBIC { + @Override + public float calc(float t) { + t = t * 2; + if (t < 1) + return 0.5f * t * t * t; + t = t - 2; + return 0.5f * (t * t * t + 2); + } + }, + + /* Quartic */ + IN_QUART { + @Override + public float calc(float t) { return t * t * t * t; } + }, + OUT_QUART { + @Override + public float calc(float t) { + t = t - 1; + return -1 * (t * t * t * t - 1); + } + }, + IN_OUT_QUART { + @Override + public float calc(float t) { + t = t * 2; + if (t < 1) + return 0.5f * t * t * t * t; + t = t - 2; + return -0.5f * (t * t * t * t - 2); + } + }, + + /* Quintic */ + IN_QUINT { + @Override + public float calc(float t) { return t * t * t * t * t; } + }, + OUT_QUINT { + @Override + public float calc(float t) { + t = t - 1; + return (t * t * t * t * t + 1); + } + }, + IN_OUT_QUINT { + @Override + public float calc(float t) { + t = t * 2; + if (t < 1) + return 0.5f * t * t * t * t * t; + t = t - 2; + return 0.5f * (t * t * t * t * t + 2); + } + }, + + /* Sine */ + IN_SINE { + @Override + public float calc(float t) { return -1 * (float) Math.cos(t * (Math.PI / 2)) + 1; } + }, + OUT_SINE { + @Override + public float calc(float t) { return (float) Math.sin(t * (Math.PI / 2)); } + }, + IN_OUT_SINE { + @Override + public float calc(float t) { return (float) (Math.cos(Math.PI * t) - 1) / -2; } + }, + + /* Exponential */ + IN_EXPO { + @Override + public float calc(float t) { return (t == 0) ? 0 : (float) Math.pow(2, 10 * (t - 1)); } + }, + OUT_EXPO { + @Override + public float calc(float t) { return (t == 1) ? 1 : (float) -Math.pow(2, -10 * t) + 1; } + }, + IN_OUT_EXPO { + @Override + public float calc(float t) { + if (t == 0 || t == 1) + return t; + t = t * 2; + if (t < 1) + return 0.5f * (float) Math.pow(2, 10 * (t - 1)); + t = t - 1; + return 0.5f * ((float) -Math.pow(2, -10 * t) + 2); + } + }, + + /* Circular */ + IN_CIRC { + @Override + public float calc(float t) { return -1 * ((float) Math.sqrt(1 - t * t) - 1); } + }, + OUT_CIRC { + @Override + public float calc(float t) { + t = t - 1; + return (float) Math.sqrt(1 - t * t); + } + }, + IN_OUT_CIRC { + @Override + public float calc(float t) { + t = t * 2; + if (t < 1) + return -0.5f * ((float) Math.sqrt(1 - t * t) - 1); + t = t - 2; + return 0.5f * ((float) Math.sqrt(1 - t * t) + 1); + } + }, + + /* Back */ + IN_BACK { + @Override + public float calc(float t) { return t * t * ((OVERSHOOT + 1) * t - OVERSHOOT); } + }, + OUT_BACK { + @Override + public float calc(float t) { + t = t - 1; + return t * t * ((OVERSHOOT + 1) * t + OVERSHOOT) + 1; + } + }, + IN_OUT_BACK { + @Override + public float calc(float t) { + float overshoot = OVERSHOOT * 1.525f; + t = t * 2; + if (t < 1) + return 0.5f * (t * t * ((overshoot + 1) * t - overshoot)); + t = t - 2; + return 0.5f * (t * t * ((overshoot + 1) * t + overshoot) + 2); + } + }, + + /* Bounce */ + IN_BOUNCE { + @Override + public float calc(float t) { return 1 - OUT_BOUNCE.calc(1 - t); } + }, + OUT_BOUNCE { + @Override + public float calc(float t) { + if (t < 0.36363636f) + return 7.5625f * t * t; + else if (t < 0.72727273f) { + t = t - 0.54545454f; + return 7.5625f * t * t + 0.75f; + } else if (t < 0.90909091f) { + t = t - 0.81818182f; + return 7.5625f * t * t + 0.9375f; + } else { + t = t - 0.95454546f; + return 7.5625f * t * t + 0.984375f; + } + } + }, + IN_OUT_BOUNCE { + @Override + public float calc(float t) { + if (t < 0.5f) + return IN_BOUNCE.calc(t * 2) * 0.5f; + return OUT_BOUNCE.calc(t * 2 - 1) * 0.5f + 0.5f; + } + }, + + /* Elastic */ + IN_ELASTIC { + @Override + public float calc(float t) { + if (t == 0 || t == 1) + return t; + float period = 0.3f; + t = t - 1; + return -((float) Math.pow(2, 10 * t) * (float) Math.sin(((t - period / 4) * (Math.PI * 2)) / period)); + } + }, + OUT_ELASTIC { + @Override + public float calc(float t) { + if (t == 0 || t == 1) + return t; + float period = 0.3f; + return (float) Math.pow(2, -10 * t) * (float) Math.sin((t - period / 4) * (Math.PI * 2) / period) + 1; + } + }, + IN_OUT_ELASTIC { + @Override + public float calc(float t) { + if (t == 0 || t == 1) + return t; + float period = 0.44999996f; + t = t * 2 - 1; + if (t < 0) + return -0.5f * ((float) Math.pow(2, 10 * t) * (float) Math.sin((t - period / 4) * (Math.PI * 2) / period)); + return (float) Math.pow(2, -10 * t) * (float) Math.sin((t - period / 4) * (Math.PI * 2) / period) * 0.5f + 1; + } + }; + + /** Overshoot constant for "back" easings. */ + private static final float OVERSHOOT = 1.70158f; + + /** + * Calculates a new {@code t} value using the animation equation. + * @param t the raw {@code t} value [0,1] + * @return the new {@code t} value [0,1] + */ + public abstract float calc(float t); +} From c0b3da37c2fd92ea97521e5ea0ab33f4b94b65ac Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 6 Aug 2015 00:53:30 -0500 Subject: [PATCH 012/104] Changed more messy animations to use AnimatedValue. Also finally refactored the main menu logo controller code... Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/objects/Spinner.java | 2 +- src/itdelatrisu/opsu/states/MainMenu.java | 125 +++++++++++++--------- src/itdelatrisu/opsu/states/SongMenu.java | 42 ++++---- src/itdelatrisu/opsu/states/Splash.java | 19 ++-- 4 files changed, 106 insertions(+), 82 deletions(-) diff --git a/src/itdelatrisu/opsu/objects/Spinner.java b/src/itdelatrisu/opsu/objects/Spinner.java index 3f9cdcab..dfd138ba 100644 --- a/src/itdelatrisu/opsu/objects/Spinner.java +++ b/src/itdelatrisu/opsu/objects/Spinner.java @@ -162,7 +162,7 @@ public class Spinner implements GameObject { final int maxVel = 48; final int minTime = 2000; final int maxTime = 5000; - maxStoredDeltaAngles = (int) Utils.clamp((hitObject.getEndTime() - hitObject.getTime() - minTime) + maxStoredDeltaAngles = Utils.clamp((hitObject.getEndTime() - hitObject.getTime() - minTime) * (maxVel - minVel) / (maxTime - minTime) + minVel, minVel, maxVel); storedDeltaAngle = new float[maxStoredDeltaAngles]; diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index dd022368..383557d3 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -33,8 +33,9 @@ import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.states.ButtonMenu.MenuState; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.MenuButton.Expand; -import itdelatrisu.opsu.ui.animations.AnimationEquation; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimatedValue; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.awt.Desktop; import java.io.IOException; @@ -62,7 +63,7 @@ import org.newdawn.slick.state.transition.FadeOutTransition; */ public class MainMenu extends BasicGameState { /** Idle time, in milliseconds, before returning the logo to its original position. */ - private static final short MOVE_DELAY = 5000; + private static final short LOGO_IDLE_DELAY = 10000; /** Max alpha level of the menu background. */ private static final float BG_MAX_ALPHA = 0.9f; @@ -70,12 +71,21 @@ public class MainMenu extends BasicGameState { /** Logo button that reveals other buttons on click. */ private MenuButton logo; - /** Whether or not the logo has been clicked. */ - private boolean logoClicked = false; + /** Logo states. */ + private enum LogoState { DEFAULT, OPENING, OPEN, CLOSING } + + /** Current logo state. */ + private LogoState logoState = LogoState.DEFAULT; /** Delay timer, in milliseconds, before starting to move the logo back to the center. */ private int logoTimer = 0; + /** Logo horizontal offset for opening and closing actions. */ + private AnimatedValue logoOpen, logoClose; + + /** Logo button alpha levels. */ + private AnimatedValue logoButtonAlpha; + /** Main "Play" and "Exit" buttons. */ private MenuButton playButton, exitButton; @@ -98,7 +108,7 @@ public class MainMenu extends BasicGameState { private Stack previous; /** Background alpha level (for fade-in effect). */ - private float bgAlpha = 0f; + private AnimatedValue bgAlpha = new AnimatedValue(1100, 0f, BG_MAX_ALPHA, AnimationEquation.LINEAR); /** Whether or not a notification was already sent upon entering. */ private boolean enterNotification = false; @@ -154,7 +164,7 @@ public class MainMenu extends BasicGameState { logo.setHoverAnimationEquation(logoAnimationEquation); playButton.setHoverAnimationEquation(logoAnimationEquation); exitButton.setHoverAnimationEquation(logoAnimationEquation); - final float logoHoverScale = 1.1f; + final float logoHoverScale = 1.08f; logo.setHoverExpand(logoHoverScale); playButton.setHoverExpand(logoHoverScale); exitButton.setHoverExpand(logoHoverScale); @@ -203,6 +213,12 @@ public class MainMenu extends BasicGameState { updateButton = new MenuButton(bangImg, startX - bangImg.getWidth(), startY - bangImg.getHeight()); updateButton.setHoverExpand(1.15f); + // logo animations + float centerOffsetX = container.getWidth() / 5f; + logoOpen = new AnimatedValue(400, 0, centerOffsetX, AnimationEquation.OUT_QUAD); + logoClose = new AnimatedValue(2200, centerOffsetX, 0, AnimationEquation.OUT_QUAD); + logoButtonAlpha = new AnimatedValue(300, 0f, 1f, AnimationEquation.LINEAR); + reset(); } @@ -215,11 +231,11 @@ public class MainMenu extends BasicGameState { // draw background Beatmap beatmap = MusicController.getBeatmap(); if (Options.isDynamicBackgroundEnabled() && - beatmap != null && beatmap.drawBG(width, height, bgAlpha, true)) + beatmap != null && beatmap.drawBG(width, height, bgAlpha.getValue(), true)) ; else { Image bg = GameImage.MENU_BG.getImage(); - bg.setAlpha(bgAlpha); + bg.setAlpha(bgAlpha.getValue()); bg.draw(); } @@ -235,7 +251,7 @@ public class MainMenu extends BasicGameState { downloadsButton.draw(); // draw buttons - if (logoTimer > 0) { + if (logoState == LogoState.OPEN || logoState == LogoState.CLOSING) { playButton.draw(); exitButton.draw(); } @@ -336,46 +352,44 @@ public class MainMenu extends BasicGameState { MusicController.toggleTrackDimmed(0.33f); // fade in background - if (bgAlpha < BG_MAX_ALPHA) { - bgAlpha += delta / 1000f; - if (bgAlpha > BG_MAX_ALPHA) - bgAlpha = BG_MAX_ALPHA; - } + bgAlpha.update(delta); // buttons - if (logoClicked) { - if (logoTimer == 0) { // shifting to left - if (logo.getX() > container.getWidth() / 3.3f) - logo.setX(logo.getX() - delta); - else - logoTimer = 1; - } else if (logoTimer >= MOVE_DELAY) // timer over: shift back to center - logoClicked = false; - else { // increment timer + int centerX = container.getWidth() / 2; + float currentLogoButtonAlpha; + switch (logoState) { + case DEFAULT: + break; + case OPENING: + if (logoOpen.update(delta)) // shifting to left + logo.setX(centerX - logoOpen.getValue()); + else { + logoState = LogoState.OPEN; + logoTimer = 0; + logoButtonAlpha.setTime(0); + } + break; + case OPEN: + if (logoButtonAlpha.update(delta)) { // fade in buttons + currentLogoButtonAlpha = logoButtonAlpha.getValue(); + playButton.getImage().setAlpha(currentLogoButtonAlpha); + exitButton.getImage().setAlpha(currentLogoButtonAlpha); + } else if (logoTimer >= LOGO_IDLE_DELAY) { // timer over: shift back to center + logoState = LogoState.CLOSING; + logoClose.setTime(0); + logoTimer = 0; + } else // increment timer logoTimer += delta; - if (logoTimer <= 500) { - // fade in buttons - playButton.getImage().setAlpha(logoTimer / 400f); - exitButton.getImage().setAlpha(logoTimer / 400f); - } - } - } else { - // fade out buttons - if (logoTimer > 0) { - float alpha = playButton.getImage().getAlpha(); - if (alpha > 0f) { - playButton.getImage().setAlpha(alpha - (delta / 200f)); - exitButton.getImage().setAlpha(alpha - (delta / 200f)); - } else - logoTimer = 0; - } - - // move back to original location - if (logo.getX() < container.getWidth() / 2) { - logo.setX(logo.getX() + (delta / 3f)); - if (logo.getX() > container.getWidth() / 2) - logo.setX(container.getWidth() / 2); + break; + case CLOSING: + if (logoButtonAlpha.update(-delta)) { // fade out buttons + currentLogoButtonAlpha = logoButtonAlpha.getValue(); + playButton.getImage().setAlpha(currentLogoButtonAlpha); + exitButton.getImage().setAlpha(currentLogoButtonAlpha); } + if (logoClose.update(delta)) // shifting to right + logo.setX(centerX - logoClose.getValue()); + break; } // tooltips @@ -471,7 +485,7 @@ public class MainMenu extends BasicGameState { SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU); menu.setFocus(BeatmapSetList.get().getBaseNode(previous.pop()), -1, true, false); if (Options.isDynamicBackgroundEnabled()) - bgAlpha = 0f; + bgAlpha.setTime(0); } else MusicController.setPosition(0); UI.sendBarNotification("<< Previous"); @@ -511,9 +525,10 @@ public class MainMenu extends BasicGameState { } // start moving logo (if clicked) - else if (!logoClicked) { + else if (logoState == LogoState.DEFAULT || logoState == LogoState.CLOSING) { if (logo.contains(x, y, 0.25f)) { - logoClicked = true; + logoState = LogoState.OPENING; + logoOpen.setTime(0); logoTimer = 0; playButton.getImage().setAlpha(0f); exitButton.getImage().setAlpha(0f); @@ -522,7 +537,7 @@ public class MainMenu extends BasicGameState { } // other button actions (if visible) - else if (logoClicked) { + else if (logoState == LogoState.OPEN || logoState == LogoState.OPENING) { if (logo.contains(x, y, 0.25f) || playButton.contains(x, y, 0.25f)) { SoundController.playSound(SoundEffect.MENUHIT); enterSongMenu(); @@ -546,8 +561,9 @@ public class MainMenu extends BasicGameState { break; case Input.KEY_P: SoundController.playSound(SoundEffect.MENUHIT); - if (!logoClicked) { - logoClicked = true; + if (logoState == LogoState.DEFAULT || logoState == LogoState.CLOSING) { + logoState = LogoState.OPENING; + logoOpen.setTime(0); logoTimer = 0; playButton.getImage().setAlpha(0f); exitButton.getImage().setAlpha(0f); @@ -595,8 +611,11 @@ public class MainMenu extends BasicGameState { public void reset() { // reset logo logo.setX(container.getWidth() / 2); - logoClicked = false; + logoOpen.setTime(0); + logoClose.setTime(0); + logoButtonAlpha.setTime(0); logoTimer = 0; + logoState = LogoState.DEFAULT; logo.resetHover(); playButton.resetHover(); @@ -625,7 +644,7 @@ public class MainMenu extends BasicGameState { previous.add(node.index); } if (Options.isDynamicBackgroundEnabled() && !sameAudio && !MusicController.isThemePlaying()) - bgAlpha = 0f; + bgAlpha.setTime(0); } /** diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index d191d125..681035d6 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -41,6 +41,8 @@ import itdelatrisu.opsu.db.ScoreDB; import itdelatrisu.opsu.states.ButtonMenu.MenuState; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimatedValue; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.io.File; import java.util.Map; @@ -138,8 +140,8 @@ public class SongMenu extends BasicGameState { /** Button coordinate values. */ private float buttonX, buttonY, buttonOffset, buttonWidth, buttonHeight; - /** Current x offset of song buttons for mouse hover, in pixels. */ - private float hoverOffset = 0f; + /** Horizontal offset of song buttons for mouse hover, in pixels. */ + private AnimatedValue hoverOffset = new AnimatedValue(250, 0, MAX_HOVER_OFFSET, AnimationEquation.OUT_QUART); /** Current index of hovered song button. */ private int hoverIndex = -1; @@ -307,7 +309,7 @@ public class SongMenu extends BasicGameState { g.setClip(0, (int) (headerY + DIVIDER_LINE_WIDTH / 2), width, (int) (footerY - headerY)); for (int i = songButtonIndex; i <= MAX_SONG_BUTTONS && node != null; i++, node = node.next) { // draw the node - float offset = (i == hoverIndex) ? hoverOffset : 0f; + float offset = (i == hoverIndex) ? hoverOffset.getValue() : 0f; ScoreData[] scores = getScoreDataForNode(node, false); node.draw(buttonX - offset, buttonY + (i*buttonOffset) + DIVIDER_LINE_WIDTH / 2, (scores == null) ? Grade.NULL : scores[0].getGrade(), (node == focusNode)); @@ -560,15 +562,11 @@ public class SongMenu extends BasicGameState { float cx = (node.index == BeatmapSetList.get().getExpandedIndex()) ? buttonX * 0.9f : buttonX; if ((mouseX > cx && mouseX < cx + buttonWidth) && (mouseY > buttonY + (i * buttonOffset) && mouseY < buttonY + (i * buttonOffset) + buttonHeight)) { - if (i == hoverIndex) { - if (hoverOffset < MAX_HOVER_OFFSET) { - hoverOffset += delta / 3f; - if (hoverOffset > MAX_HOVER_OFFSET) - hoverOffset = MAX_HOVER_OFFSET; - } - } else { + if (i == hoverIndex) + hoverOffset.update(delta); + else { hoverIndex = i; - hoverOffset = 0f; + hoverOffset.setTime(0); } isHover = true; break; @@ -576,7 +574,7 @@ public class SongMenu extends BasicGameState { } } if (!isHover) { - hoverOffset = 0f; + hoverOffset.setTime(0); hoverIndex = -1; } else return; @@ -660,7 +658,7 @@ public class SongMenu extends BasicGameState { float cx = (node.index == expandedIndex) ? buttonX * 0.9f : buttonX; if ((x > cx && x < cx + buttonWidth) && (y > buttonY + (i * buttonOffset) && y < buttonY + (i * buttonOffset) + buttonHeight)) { - float oldHoverOffset = hoverOffset; + int oldHoverOffsetTime = hoverOffset.getTime(); int oldHoverIndex = hoverIndex; // clicked node is already expanded @@ -685,7 +683,7 @@ public class SongMenu extends BasicGameState { } // restore hover data - hoverOffset = oldHoverOffset; + hoverOffset.setTime(oldHoverOffsetTime); hoverIndex = oldHoverIndex; // open beatmap menu @@ -820,11 +818,11 @@ public class SongMenu extends BasicGameState { if (next != null) { SoundController.playSound(SoundEffect.MENUCLICK); BeatmapSetNode oldStartNode = startNode; - float oldHoverOffset = hoverOffset; + int oldHoverOffsetTime = hoverOffset.getTime(); int oldHoverIndex = hoverIndex; setFocus(next, 0, false, true); if (startNode == oldStartNode) { - hoverOffset = oldHoverOffset; + hoverOffset.setTime(oldHoverOffsetTime); hoverIndex = oldHoverIndex; } } @@ -836,11 +834,11 @@ public class SongMenu extends BasicGameState { if (prev != null) { SoundController.playSound(SoundEffect.MENUCLICK); BeatmapSetNode oldStartNode = startNode; - float oldHoverOffset = hoverOffset; + int oldHoverOffsetTime = hoverOffset.getTime(); int oldHoverIndex = hoverIndex; setFocus(prev, (prev.index == focusNode.index) ? 0 : prev.getBeatmapSet().size() - 1, false, true); if (startNode == oldStartNode) { - hoverOffset = oldHoverOffset; + hoverOffset.setTime(oldHoverOffsetTime); hoverIndex = oldHoverIndex; } } @@ -938,7 +936,7 @@ public class SongMenu extends BasicGameState { selectRandomButton.resetHover(); selectMapOptionsButton.resetHover(); selectOptionsButton.resetHover(); - hoverOffset = 0f; + hoverOffset.setTime(0); hoverIndex = -1; startScore = 0; beatmapMenuTimer = -1; @@ -1076,7 +1074,7 @@ public class SongMenu extends BasicGameState { oldFocusNode = null; randomStack = new Stack(); songInfo = null; - hoverOffset = 0f; + hoverOffset.setTime(0); hoverIndex = -1; search.setText(""); searchTimer = SEARCH_DELAY; @@ -1157,7 +1155,7 @@ public class SongMenu extends BasicGameState { break; } if (shifted) { - hoverOffset = 0f; + hoverOffset.setTime(0); hoverIndex = -1; } return; @@ -1175,7 +1173,7 @@ public class SongMenu extends BasicGameState { if (node == null) return null; - hoverOffset = 0f; + hoverOffset.setTime(0); hoverIndex = -1; songInfo = null; BeatmapSetNode oldFocus = focusNode; diff --git a/src/itdelatrisu/opsu/states/Splash.java b/src/itdelatrisu/opsu/states/Splash.java index 14d8245a..c83db4a0 100644 --- a/src/itdelatrisu/opsu/states/Splash.java +++ b/src/itdelatrisu/opsu/states/Splash.java @@ -29,13 +29,14 @@ import itdelatrisu.opsu.beatmap.BeatmapParser; import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.replay.ReplayImporter; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimatedValue; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.io.File; import org.newdawn.slick.Color; import org.newdawn.slick.GameContainer; import org.newdawn.slick.Graphics; -import org.newdawn.slick.Image; import org.newdawn.slick.Input; import org.newdawn.slick.SlickException; import org.newdawn.slick.state.BasicGameState; @@ -47,6 +48,9 @@ import org.newdawn.slick.state.StateBasedGame; * Loads game resources and enters "Main Menu" state. */ public class Splash extends BasicGameState { + /** Minimum time, in milliseconds, to display the splash screen (and fade in the logo). */ + private static final int MIN_SPLASH_TIME = 300; + /** Whether or not loading has completed. */ private boolean finished = false; @@ -59,6 +63,9 @@ public class Splash extends BasicGameState { /** Whether the skin being loaded is a new skin (for program restarts). */ private boolean newSkin = false; + /** Logo alpha level. */ + private AnimatedValue logoAlpha; + // game-related variables private int state; private GameContainer container; @@ -80,6 +87,8 @@ public class Splash extends BasicGameState { // load Utils class first (needed in other 'init' methods) Utils.init(container, game); + // fade in logo + this.logoAlpha = new AnimatedValue(MIN_SPLASH_TIME, 0f, 1f, AnimationEquation.LINEAR); GameImage.MENU_LOGO.getImage().setAlpha(0f); } @@ -144,13 +153,11 @@ public class Splash extends BasicGameState { } // fade in logo - Image logo = GameImage.MENU_LOGO.getImage(); - float alpha = logo.getAlpha(); - if (alpha < 1f) - logo.setAlpha(alpha + (delta / 500f)); + if (logoAlpha.update(delta)) + GameImage.MENU_LOGO.getImage().setAlpha(logoAlpha.getValue()); // change states when loading complete - if (finished && alpha >= 1f) { + if (finished && logoAlpha.getValue() >= 1f) { // initialize song list if (BeatmapSetList.get().size() > 0) { BeatmapSetList.get().init(); From 940e9baa41674d5a272bfad2a55f53a86d518b51 Mon Sep 17 00:00:00 2001 From: MatteoS Date: Sat, 8 Aug 2015 14:43:56 +0200 Subject: [PATCH 013/104] Correct detection of GLSL version for curve rendering --- src/itdelatrisu/opsu/objects/curves/Curve.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java index 74ad3864..a22c4b29 100644 --- a/src/itdelatrisu/opsu/objects/curves/Curve.java +++ b/src/itdelatrisu/opsu/objects/curves/Curve.java @@ -87,7 +87,7 @@ public abstract class Curve { Curve.borderColor = borderColor; ContextCapabilities capabilities = GLContext.getCapabilities(); - mmsliderSupported = capabilities.GL_EXT_framebuffer_object && capabilities.OpenGL30; + mmsliderSupported = capabilities.GL_EXT_framebuffer_object && capabilities.OpenGL33; if (mmsliderSupported) CurveRenderState.init(width, height, circleSize); else { From 110e54e06349cc9502c32c344efcec333a97176e Mon Sep 17 00:00:00 2001 From: MatteoS Date: Sat, 8 Aug 2015 16:20:00 +0200 Subject: [PATCH 014/104] Fix crash on pressing extra mouse buttons --- src/org/newdawn/slick/Input.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/newdawn/slick/Input.java b/src/org/newdawn/slick/Input.java index 1066ca34..c74eff95 100644 --- a/src/org/newdawn/slick/Input.java +++ b/src/org/newdawn/slick/Input.java @@ -1233,7 +1233,7 @@ public class Input { } while (Mouse.next()) { - if (Mouse.getEventButton() >= 0) { + if (Mouse.getEventButton() >= 0 && Mouse.getEventButton() < mousePressed.length) { if (Mouse.getEventButtonState()) { consumed = false; mousePressed[Mouse.getEventButton()] = true; From b537b83736b6afef9a9b2c8ca5bae99031afb6bd Mon Sep 17 00:00:00 2001 From: MatteoS Date: Sat, 8 Aug 2015 16:29:08 +0200 Subject: [PATCH 015/104] Game doesn't crash when being unable to go to the github repo page --- src/itdelatrisu/opsu/states/MainMenu.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index 383557d3..50dfd3b4 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -501,7 +501,7 @@ public class MainMenu extends BasicGameState { else if (repoButton != null && repoButton.contains(x, y)) { try { Desktop.getDesktop().browse(Options.REPOSITORY_URI); - } catch (IOException e) { + } catch (IOException | UnsupportedOperationException e) { ErrorHandler.error("Could not browse to repository URI.", e, false); } } From 40ab94794f7eb71c131a6200a2c0a0d87be849ab Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sat, 8 Aug 2015 12:04:15 -0500 Subject: [PATCH 016/104] Bug fixes and tweaks. - Check if other Desktop actions are supported (follow-up to #114). - The cursor-middle image is no longer scaled (when clicking). - Changed the options menu background image (created with Trianglify at http://qrohlf.com/trianglify/), and made it fit the entire page. - Slightly increased minimum splash screen time. - Switched more animations to use AnimatedValue. Signed-off-by: Jeffrey Han --- res/options-background.jpg | Bin 116484 -> 0 bytes res/options-background.png | Bin 0 -> 348154 bytes src/itdelatrisu/opsu/ErrorHandler.java | 161 ++++++++++++------- src/itdelatrisu/opsu/states/MainMenu.java | 6 +- src/itdelatrisu/opsu/states/OptionsMenu.java | 10 +- src/itdelatrisu/opsu/states/Splash.java | 2 +- src/itdelatrisu/opsu/ui/Cursor.java | 2 - src/itdelatrisu/opsu/ui/UI.java | 24 +-- 8 files changed, 116 insertions(+), 89 deletions(-) delete mode 100644 res/options-background.jpg create mode 100644 res/options-background.png diff --git a/res/options-background.jpg b/res/options-background.jpg deleted file mode 100644 index d590ad6b847222ab1fe723f12d7249e67b6d66f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116484 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 * @param index the beatmap index - * @throws IndexOutOfBoundsException + * @throws IndexOutOfBoundsException if the index is out of range */ public String[] getInfo(int index) { Beatmap beatmap = beatmaps.get(index); diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java b/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java index 3960856e..7a0efdd9 100644 --- a/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java +++ b/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java @@ -330,6 +330,7 @@ public class BeatmapSetList { /** * Expands the node at an index by inserting a new node for each Beatmap * in that node and hiding the group node. + * @param index the node index * @return the first of the newly-inserted nodes */ public BeatmapSetNode expand(int index) { diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapWatchService.java b/src/itdelatrisu/opsu/beatmap/BeatmapWatchService.java index e63482cf..8399cc22 100644 --- a/src/itdelatrisu/opsu/beatmap/BeatmapWatchService.java +++ b/src/itdelatrisu/opsu/beatmap/BeatmapWatchService.java @@ -129,7 +129,11 @@ public class BeatmapWatchService { /** Watch service listener interface. */ public interface BeatmapWatchServiceListener { - /** Indication that an event was received. */ + /** + * Indication that an event was received. + * @param kind the event kind + * @param child the child directory + */ public void eventReceived(WatchEvent.Kind kind, Path child); } diff --git a/src/itdelatrisu/opsu/downloads/DownloadList.java b/src/itdelatrisu/opsu/downloads/DownloadList.java index 2c396c9b..dfeac328 100644 --- a/src/itdelatrisu/opsu/downloads/DownloadList.java +++ b/src/itdelatrisu/opsu/downloads/DownloadList.java @@ -75,7 +75,7 @@ public class DownloadList { } /** - * Returns the size of the doownloads list. + * Returns the size of the downloads list. */ public int size() { return nodes.size(); } diff --git a/src/itdelatrisu/opsu/downloads/DownloadNode.java b/src/itdelatrisu/opsu/downloads/DownloadNode.java index 4205f058..b530a528 100644 --- a/src/itdelatrisu/opsu/downloads/DownloadNode.java +++ b/src/itdelatrisu/opsu/downloads/DownloadNode.java @@ -218,6 +218,13 @@ public class DownloadNode { /** * Constructor. + * @param beatmapSetID the beatmap set ID + * @param date the last modified date string + * @param title the song title + * @param titleUnicode the Unicode song title (or {@code null} if none) + * @param artist the song artist + * @param artistUnicode the Unicode song artist (or {@code null} if none) + * @param creator the beatmap creator */ public DownloadNode(int beatmapSetID, String date, String title, String titleUnicode, String artist, String artistUnicode, String creator) { diff --git a/src/itdelatrisu/opsu/downloads/Updater.java b/src/itdelatrisu/opsu/downloads/Updater.java index 25adb7a3..165d810a 100644 --- a/src/itdelatrisu/opsu/downloads/Updater.java +++ b/src/itdelatrisu/opsu/downloads/Updater.java @@ -194,6 +194,7 @@ public class Updater { /** * Checks the program version against the version file on the update server. + * @throws IOException if an I/O exception occurs */ public void checkForUpdates() throws IOException { if (status != Status.INITIAL || Options.USE_XDG) diff --git a/src/itdelatrisu/opsu/io/OsuReader.java b/src/itdelatrisu/opsu/io/OsuReader.java index 0cf84dcd..8af11c72 100644 --- a/src/itdelatrisu/opsu/io/OsuReader.java +++ b/src/itdelatrisu/opsu/io/OsuReader.java @@ -61,6 +61,7 @@ public class OsuReader { /** * Closes the input stream. + * @throws IOException if an I/O error occurs */ public void close() throws IOException { reader.close(); } diff --git a/src/itdelatrisu/opsu/io/OsuWriter.java b/src/itdelatrisu/opsu/io/OsuWriter.java index 91b2efce..92d32d54 100644 --- a/src/itdelatrisu/opsu/io/OsuWriter.java +++ b/src/itdelatrisu/opsu/io/OsuWriter.java @@ -62,7 +62,7 @@ public class OsuWriter { /** * Closes the output stream. - * @throws IOException + * @throws IOException if an I/O error occurs */ public void close() throws IOException { writer.close(); } diff --git a/src/itdelatrisu/opsu/replay/ReplayFrame.java b/src/itdelatrisu/opsu/replay/ReplayFrame.java index 8f6e5095..28233a7f 100644 --- a/src/itdelatrisu/opsu/replay/ReplayFrame.java +++ b/src/itdelatrisu/opsu/replay/ReplayFrame.java @@ -81,7 +81,8 @@ public class ReplayFrame { public int getTimeDiff() { return timeDiff; } /** - * Sets the time since the previous action, in milliseconds. + * Sets the time since the previous action. + * @param diff the time difference, in milliseconds */ public void setTimeDiff(int diff) { this.timeDiff = diff; } diff --git a/src/itdelatrisu/opsu/ui/DropdownMenu.java b/src/itdelatrisu/opsu/ui/DropdownMenu.java index a75d0d9a..4cfb8854 100644 --- a/src/itdelatrisu/opsu/ui/DropdownMenu.java +++ b/src/itdelatrisu/opsu/ui/DropdownMenu.java @@ -43,6 +43,8 @@ import org.newdawn.slick.gui.GUIContext; *
  • Call {@link #activate()}/{@link #deactivate()} whenever the component is needed * (e.g. in a state's {@code enter} and {@code leave} events. * + * + * @param the type of the elements in the menu */ public class DropdownMenu extends AbstractComponent { /** Padding ratios for drawing. */ diff --git a/src/itdelatrisu/opsu/ui/MenuButton.java b/src/itdelatrisu/opsu/ui/MenuButton.java index 99c454f2..1aa524dc 100644 --- a/src/itdelatrisu/opsu/ui/MenuButton.java +++ b/src/itdelatrisu/opsu/ui/MenuButton.java @@ -146,11 +146,13 @@ public class MenuButton { /** * Sets a new center x coordinate. + * @param x the x coordinate */ public void setX(float x) { this.x = x; } /** * Sets a new center y coordinate. + * @param y the y coordinate */ public void setY(float y) { this.y = y; } diff --git a/src/itdelatrisu/opsu/ui/UI.java b/src/itdelatrisu/opsu/ui/UI.java index ec9195b3..dfe8116d 100644 --- a/src/itdelatrisu/opsu/ui/UI.java +++ b/src/itdelatrisu/opsu/ui/UI.java @@ -38,7 +38,6 @@ import org.newdawn.slick.GameContainer; import org.newdawn.slick.Graphics; import org.newdawn.slick.Image; import org.newdawn.slick.Input; -import org.newdawn.slick.SlickException; import org.newdawn.slick.state.StateBasedGame; /** @@ -86,10 +85,8 @@ public class UI { * Initializes UI data. * @param container the game container * @param game the game object - * @throws SlickException */ - public static void init(GameContainer container, StateBasedGame game) - throws SlickException { + public static void init(GameContainer container, StateBasedGame game) { UI.container = container; UI.input = container.getInput(); @@ -271,8 +268,9 @@ public class UI { } /** - * Draws loading progress (OSZ unpacking, beatmap parsing, sound loading) + * Draws loading progress (OSZ unpacking, beatmap parsing, replay importing, sound loading) * at the bottom of the screen. + * @param g the graphics context */ public static void drawLoadingProgress(Graphics g) { String text, file; From 1fe45f7bb473678d93a2a0714afd8f71992b65c6 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Fri, 11 Sep 2015 11:43:19 -0400 Subject: [PATCH 104/104] Merged DISABLE_CURSOR option from fluddokt/opsu. --- src/itdelatrisu/opsu/Options.java | 7 +++++++ src/itdelatrisu/opsu/states/OptionsMenu.java | 3 ++- src/itdelatrisu/opsu/ui/Cursor.java | 3 +++ src/itdelatrisu/opsu/ui/DropdownMenu.java | 2 +- src/itdelatrisu/opsu/ui/UI.java | 4 ++-- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index 434bc9a6..18d701bd 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -413,6 +413,7 @@ public class Options { }, DISABLE_MOUSE_WHEEL ("Disable mouse wheel in play mode", "MouseDisableWheel", "During play, you can use the mouse wheel to adjust the volume and pause the game.\nThis will disable that functionality.", false), DISABLE_MOUSE_BUTTONS ("Disable mouse buttons in play mode", "MouseDisableButtons", "This option will disable all mouse buttons.\nSpecifically for people who use their keyboard to click.", false), + DISABLE_CURSOR ("Disable Cursor", "DisableCursor", "Hide the cursor sprite.", false), BACKGROUND_DIM ("Background Dim", "DimLevel", "Percentage to dim the background image during gameplay.", 50, 0, 100), FORCE_DEFAULT_PLAYFIELD ("Force Default Playfield", "ForceDefaultPlayfield", "Override the song background with the default playfield background.", false), IGNORE_BEATMAP_SKINS ("Ignore All Beatmap Skins", "IgnoreBeatmapSkins", "Never use skin element overrides provided by beatmaps.", false), @@ -1059,6 +1060,12 @@ public class Options { "Mouse buttons are disabled." : "Mouse buttons are enabled."); } + /** + * Returns whether or not the cursor sprite should be hidden. + * @return true if disabled + */ + public static boolean isCursorDisabled() { return GameOption.DISABLE_CURSOR.getBooleanValue(); } + /** * Returns the left game key. * @return the left key code diff --git a/src/itdelatrisu/opsu/states/OptionsMenu.java b/src/itdelatrisu/opsu/states/OptionsMenu.java index 3792d8b4..6cdee288 100644 --- a/src/itdelatrisu/opsu/states/OptionsMenu.java +++ b/src/itdelatrisu/opsu/states/OptionsMenu.java @@ -88,7 +88,8 @@ public class OptionsMenu extends BasicGameState { GameOption.DISABLE_MOUSE_WHEEL, GameOption.DISABLE_MOUSE_BUTTONS, GameOption.CURSOR_SIZE, - GameOption.NEW_CURSOR + GameOption.NEW_CURSOR, + GameOption.DISABLE_CURSOR }), CUSTOM ("Custom", new GameOption[] { GameOption.FIXED_CS, diff --git a/src/itdelatrisu/opsu/ui/Cursor.java b/src/itdelatrisu/opsu/ui/Cursor.java index f5d47ca7..d0382aae 100644 --- a/src/itdelatrisu/opsu/ui/Cursor.java +++ b/src/itdelatrisu/opsu/ui/Cursor.java @@ -115,6 +115,9 @@ public class Cursor { * @param mousePressed whether or not the mouse button is pressed */ public void draw(int mouseX, int mouseY, boolean mousePressed) { + if (Options.isCursorDisabled()) + return; + // determine correct cursor image Image cursor = null, cursorMiddle = null, cursorTrail = null; boolean beatmapSkinned = GameImage.CURSOR.hasBeatmapSkinImage(); diff --git a/src/itdelatrisu/opsu/ui/DropdownMenu.java b/src/itdelatrisu/opsu/ui/DropdownMenu.java index 4cfb8854..059751a0 100644 --- a/src/itdelatrisu/opsu/ui/DropdownMenu.java +++ b/src/itdelatrisu/opsu/ui/DropdownMenu.java @@ -375,7 +375,7 @@ public class DropdownMenu extends AbstractComponent { /** * Selects the item at the given index. * @param index the list item index - * @throws IllegalArgumentException if index < -1 or index is greater than or equal to size + * @throws IllegalArgumentException if {@code index} is negative or greater than or equal to size */ public void setSelectedIndex(int index) { if (index < 0 || index >= items.length) diff --git a/src/itdelatrisu/opsu/ui/UI.java b/src/itdelatrisu/opsu/ui/UI.java index dfe8116d..f2a7f63c 100644 --- a/src/itdelatrisu/opsu/ui/UI.java +++ b/src/itdelatrisu/opsu/ui/UI.java @@ -119,7 +119,7 @@ public class UI { } /** - * Draws the global UI components: cursor, FPS, volume bar, bar notifications. + * Draws the global UI components: cursor, FPS, volume bar, tooltips, bar notifications. * @param g the graphics context */ public static void draw(Graphics g) { @@ -131,7 +131,7 @@ public class UI { } /** - * Draws the global UI components: cursor, FPS, volume bar, bar notifications. + * Draws the global UI components: cursor, FPS, volume bar, tooltips, bar notifications. * @param g the graphics context * @param mouseX the mouse x coordinate * @param mouseY the mouse y coordinate