", UPDATED = "Updated: ";
+ int index = -1;
+ int nextIndex = html.indexOf(START_TAG, index + 1);
+ while ((index = nextIndex) != -1) {
+ nextIndex = html.indexOf(START_TAG, index + 1);
+ int n = (nextIndex == -1) ? html.length() : nextIndex;
+ int i, j;
+
+ // find beatmap
+ i = html.indexOf(HREF_TAG, index + START_TAG.length());
+ if (i == -1 || i > n) continue;
+ i = html.indexOf('>', i + HREF_TAG.length());
+ if (i == -1 || i >= n) continue;
+ j = html.indexOf(HREF_TAG_END, i + 1);
+ if (j == -1 || j > n) continue;
+ String beatmap = html.substring(i + 1, j).trim();
+
+ // find date
+ i = html.indexOf(UPDATED, j);
+ if (i == -1 || i >= n) continue;
+ j = html.indexOf('<', i + UPDATED.length());
+ if (j == -1 || j > n) continue;
+ String date = html.substring(i + UPDATED.length(), j).trim();
+
+ // parse id, title, and artist
+ Matcher m = BEATMAP_PATTERN.matcher(beatmap);
+ if (!m.matches())
+ continue;
+
+ nodeList.add(new DownloadNode(Integer.parseInt(m.group(1)), date, m.group(3), null, m.group(2), null, ""));
+ }
+
+ nodes = nodeList.toArray(new DownloadNode[nodeList.size()]);
+
+ // store total result count
+ this.totalResults = nodes.length;
+ } catch (MalformedURLException | UnsupportedEncodingException e) {
+ ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true);
+ }
+ return nodes;
+ }
+
+ @Override
+ public int minQueryLength() { return 0; }
+
+ @Override
+ public int totalResults() { return totalResults; }
+}
diff --git a/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java b/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java
index d7179664..834f2b9f 100644
--- a/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java
+++ b/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java
@@ -39,6 +39,8 @@ import org.json.JSONObject;
/**
* Download server: http://loli.al/
+ *
+ * This server went offline in August 2015.
*/
public class OsuMirrorServer extends DownloadServer {
/** Server name. */
diff --git a/src/itdelatrisu/opsu/downloads/servers/YaSOnlineServer.java b/src/itdelatrisu/opsu/downloads/servers/YaSOnlineServer.java
new file mode 100644
index 00000000..95846bfd
--- /dev/null
+++ b/src/itdelatrisu/opsu/downloads/servers/YaSOnlineServer.java
@@ -0,0 +1,204 @@
+/*
+ * 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.downloads.servers;
+
+import itdelatrisu.opsu.ErrorHandler;
+import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.downloads.DownloadNode;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+
+import org.json.JSONObject;
+
+/**
+ * Download server: http://osu.yas-online.net/
+ */
+public class YaSOnlineServer extends DownloadServer {
+ /** Server name. */
+ private static final String SERVER_NAME = "YaS Online";
+
+ /** Formatted download URL (returns JSON): {@code beatmapSetID} */
+ private static final String DOWNLOAD_URL = "https://osu.yas-online.net/json.mapdata.php?mapId=%d";
+
+ /**
+ * Formatted download fetch URL: {@code downloadLink}
+ * (e.g. {@code /fetch/49125122158ef360a66a07bce2d0483596913843-m-10418.osz})
+ */
+ private static final String DOWNLOAD_FETCH_URL = "https://osu.yas-online.net%s";
+
+ /** Maximum beatmaps displayed per page. */
+ private static final int PAGE_LIMIT = 25;
+
+ /** Formatted home URL: {@code page} */
+ private static final String HOME_URL = "https://osu.yas-online.net/json.maplist.php?o=%d";
+
+ /** Formatted search URL: {@code query} */
+ private static final String SEARCH_URL = "https://osu.yas-online.net/json.search.php?searchQuery=%s";
+
+ /** Total result count from the last query. */
+ private int totalResults = -1;
+
+ /** Max server download ID seen (for approximating total pages). */
+ private int maxServerID = 0;
+
+ /** Constructor. */
+ public YaSOnlineServer() {}
+
+ @Override
+ public String getName() { return SERVER_NAME; }
+
+ @Override
+ public String getDownloadURL(int beatmapSetID) {
+ try {
+ // TODO: do this asynchronously (will require lots of changes...)
+ return getDownloadURLFromMapData(beatmapSetID);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the beatmap download URL by downloading its map data.
+ *
+ * This is needed because there is no other way to find a beatmap's direct
+ * download URL.
+ * @param beatmapSetID the beatmap set ID
+ * @return the URL string, or null if the address could not be determined
+ * @throws IOException if any connection error occurred
+ */
+ private String getDownloadURLFromMapData(int beatmapSetID) throws IOException {
+ try {
+ // read JSON
+ String search = String.format(DOWNLOAD_URL, beatmapSetID);
+ JSONObject json = Utils.readJsonObjectFromUrl(new URL(search));
+ JSONObject results;
+ if (json == null ||
+ !json.getString("result").equals("success") ||
+ (results = json.getJSONObject("success")).length() == 0) {
+ return null;
+ }
+
+ // parse result
+ Iterator> keys = results.keys();
+ if (!keys.hasNext())
+ return null;
+ String key = (String) keys.next();
+ JSONObject item = results.getJSONObject(key);
+ String downloadLink = item.getString("downloadLink");
+ return String.format(DOWNLOAD_FETCH_URL, downloadLink);
+ } catch (MalformedURLException | UnsupportedEncodingException e) {
+ ErrorHandler.error(String.format("Problem retrieving download URL for beatmap '%d'.", beatmapSetID), e, true);
+ return null;
+ }
+ }
+
+ @Override
+ public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException {
+ DownloadNode[] nodes = null;
+ try {
+ // read JSON
+ String search;
+ boolean isSearch;
+ if (query.isEmpty()) {
+ isSearch = false;
+ search = String.format(HOME_URL, (page - 1) * PAGE_LIMIT);
+ } else {
+ isSearch = true;
+ search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8"));
+ }
+ JSONObject json = Utils.readJsonObjectFromUrl(new URL(search));
+ if (json == null) {
+ this.totalResults = -1;
+ return null;
+ }
+ JSONObject results;
+ if (!json.getString("result").equals("success") ||
+ (results = json.getJSONObject("success")).length() == 0) {
+ this.totalResults = 0;
+ return new DownloadNode[0];
+ }
+
+ // parse result list
+ List nodeList = new ArrayList();
+ for (Object obj : results.keySet()) {
+ String key = (String) obj;
+ JSONObject item = results.getJSONObject(key);
+
+ // parse title and artist
+ String title, artist;
+ String str = item.getString("map");
+ int index = str.indexOf(" - ");
+ if (index > -1) {
+ title = str.substring(0, index);
+ artist = str.substring(index + 3);
+ } else { // should never happen...
+ title = str;
+ artist = "?";
+ }
+
+ // only contains date added if part of a beatmap pack
+ int added = item.getInt("added");
+ String date = (added == 0) ? "?" : formatDate(added);
+
+ // approximate page count
+ int serverID = item.getInt("id");
+ if (serverID > maxServerID)
+ maxServerID = serverID;
+
+ nodeList.add(new DownloadNode(item.getInt("mapid"), date, title, null, artist, null, ""));
+ }
+ nodes = nodeList.toArray(new DownloadNode[nodeList.size()]);
+
+ // store total result count
+ if (isSearch)
+ this.totalResults = nodes.length;
+ else
+ this.totalResults = maxServerID;
+ } catch (MalformedURLException | UnsupportedEncodingException e) {
+ ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true);
+ }
+ return nodes;
+ }
+
+ @Override
+ public int minQueryLength() { return 3; }
+
+ @Override
+ public int totalResults() { return totalResults; }
+
+ /**
+ * Returns a formatted date string from a raw date.
+ * @param timestamp the UTC timestamp, in seconds
+ * @return the formatted date
+ */
+ private String formatDate(int timestamp) {
+ Date d = new Date(timestamp * 1000L);
+ DateFormat fmt = new SimpleDateFormat("d MMM yyyy HH:mm:ss");
+ return fmt.format(d);
+ }
+}
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/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java
index 5bc1681c..15e94524 100644
--- a/src/itdelatrisu/opsu/objects/Circle.java
+++ b/src/itdelatrisu/opsu/objects/Circle.java
@@ -25,7 +25,9 @@ import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.HitObject;
+import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.states.Game;
+import itdelatrisu.opsu.ui.Colors;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
@@ -35,9 +37,6 @@ import org.newdawn.slick.Graphics;
* Data type representing a circle object.
*/
public class Circle implements GameObject {
- /** The amount of time, in milliseconds, to fade in the circle. */
- private static final int FADE_IN_TIME = 375;
-
/** The diameter of hit circles. */
private static float diameter;
@@ -62,11 +61,10 @@ public class Circle implements GameObject {
/**
* Initializes the Circle data type with map modifiers, images, and dimensions.
* @param container the game container
- * @param circleSize the map's circleSize value
+ * @param circleDiameter the circle diameter
*/
- public static void init(GameContainer container, float circleSize) {
- diameter = (104 - (circleSize * 8));
- diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
+ public static void init(GameContainer container, float circleDiameter) {
+ diameter = circleDiameter * HitObject.getXMultiplier(); // convert from Osupixels (640x480)
int diameterInt = (int) diameter;
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt));
@@ -93,26 +91,37 @@ public class Circle implements GameObject {
@Override
public void draw(Graphics g, int trackPosition) {
int timeDiff = hitObject.getTime() - trackPosition;
- float scale = timeDiff / (float) game.getApproachTime();
- float fadeinScale = (timeDiff - game.getApproachTime() + FADE_IN_TIME) / (float) FADE_IN_TIME;
+ final int approachTime = game.getApproachTime();
+ final int fadeInTime = game.getFadeInTime();
+ float scale = timeDiff / (float) approachTime;
float approachScale = 1 + scale * 3;
+ float fadeinScale = (timeDiff - approachTime + fadeInTime) / (float) fadeInTime;
float alpha = Utils.clamp(1 - fadeinScale, 0, 1);
- float oldAlpha = Utils.COLOR_WHITE_FADE.a;
- Utils.COLOR_WHITE_FADE.a = color.a = alpha;
+ if (GameMod.HIDDEN.isActive()) {
+ final int hiddenDecayTime = game.getHiddenDecayTime();
+ final int hiddenTimeDiff = game.getHiddenTimeDiff();
+ if (fadeinScale <= 0f && timeDiff < hiddenTimeDiff + hiddenDecayTime) {
+ float hiddenAlpha = (timeDiff < hiddenTimeDiff) ? 0f : (timeDiff - hiddenTimeDiff) / (float) hiddenDecayTime;
+ alpha = Math.min(alpha, hiddenAlpha);
+ }
+ }
- if (timeDiff >= 0)
+ float oldAlpha = Colors.WHITE_FADE.a;
+ Colors.WHITE_FADE.a = color.a = alpha;
+
+ if (timeDiff >= 0 && !GameMod.HIDDEN.isActive())
GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
GameImage.HITCIRCLE.getImage().drawCentered(x, y, color);
boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
if (!overlayAboveNumber)
- GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Utils.COLOR_WHITE_FADE);
+ GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Colors.WHITE_FADE);
data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
GameImage.HITCIRCLE.getImage().getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
if (overlayAboveNumber)
- GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Utils.COLOR_WHITE_FADE);
+ GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Colors.WHITE_FADE);
- Utils.COLOR_WHITE_FADE.a = oldAlpha;
+ Colors.WHITE_FADE.a = oldAlpha;
}
/**
@@ -186,7 +195,7 @@ public class Circle implements GameObject {
}
@Override
- public float[] getPointAt(int trackPosition) { return new float[] { x, y }; }
+ public Vec2f getPointAt(int trackPosition) { return new Vec2f(x, y); }
@Override
public int getEndTime() { return hitObject.getTime(); }
diff --git a/src/itdelatrisu/opsu/objects/DummyObject.java b/src/itdelatrisu/opsu/objects/DummyObject.java
index 45c9c1ed..a5426ba6 100644
--- a/src/itdelatrisu/opsu/objects/DummyObject.java
+++ b/src/itdelatrisu/opsu/objects/DummyObject.java
@@ -19,6 +19,7 @@
package itdelatrisu.opsu.objects;
import itdelatrisu.opsu.beatmap.HitObject;
+import itdelatrisu.opsu.objects.curves.Vec2f;
import org.newdawn.slick.Graphics;
@@ -53,7 +54,7 @@ public class DummyObject implements GameObject {
public boolean mousePressed(int x, int y, int trackPosition) { return false; }
@Override
- public float[] getPointAt(int trackPosition) { return new float[] { x, y }; }
+ public Vec2f getPointAt(int trackPosition) { return new Vec2f(x, y); }
@Override
public int getEndTime() { return hitObject.getTime(); }
diff --git a/src/itdelatrisu/opsu/objects/GameObject.java b/src/itdelatrisu/opsu/objects/GameObject.java
index 2a40ca47..f1f788d2 100644
--- a/src/itdelatrisu/opsu/objects/GameObject.java
+++ b/src/itdelatrisu/opsu/objects/GameObject.java
@@ -18,6 +18,8 @@
package itdelatrisu.opsu.objects;
+import itdelatrisu.opsu.objects.curves.Vec2f;
+
import org.newdawn.slick.Graphics;
/**
@@ -55,9 +57,9 @@ public interface GameObject {
/**
* Returns the coordinates of the hit object at a given track position.
* @param trackPosition the track position
- * @return the [x,y] coordinates
+ * @return the position vector
*/
- public float[] getPointAt(int trackPosition);
+ public Vec2f getPointAt(int trackPosition);
/**
* Returns the end time of the hit object.
diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java
index 2ba83c71..2d2476b4 100644
--- a/src/itdelatrisu/opsu/objects/Slider.java
+++ b/src/itdelatrisu/opsu/objects/Slider.java
@@ -26,11 +26,10 @@ import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.HitObject;
-import itdelatrisu.opsu.objects.curves.CatmullCurve;
-import itdelatrisu.opsu.objects.curves.CircumscribedCircle;
import itdelatrisu.opsu.objects.curves.Curve;
-import itdelatrisu.opsu.objects.curves.LinearBezier;
+import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.states.Game;
+import itdelatrisu.opsu.ui.Colors;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
@@ -56,9 +55,6 @@ public class Slider implements GameObject {
/** The diameter of hit circles. */
private static float diameter;
- /** The amount of time, in milliseconds, to fade in the slider. */
- private static final int FADE_IN_TIME = 375;
-
/** The associated HitObject. */
private HitObject hitObject;
@@ -113,22 +109,21 @@ public class Slider implements GameObject {
/**
* Initializes the Slider data type with images and dimensions.
* @param container the game container
- * @param circleSize the map's circleSize value
+ * @param circleDiameter the circle diameter
* @param beatmap the associated beatmap
*/
- public static void init(GameContainer container, float circleSize, Beatmap beatmap) {
+ public static void init(GameContainer container, float circleDiameter, Beatmap beatmap) {
containerWidth = container.getWidth();
containerHeight = container.getHeight();
- diameter = (104 - (circleSize * 8));
- diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
+ diameter = circleDiameter * HitObject.getXMultiplier(); // convert from Osupixels (640x480)
int diameterInt = (int) diameter;
followRadius = diameter / 2 * 3f;
// slider ball
- if (GameImage.SLIDER_BALL.hasSkinImages() ||
- (!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
+ if (GameImage.SLIDER_BALL.hasBeatmapSkinImages() ||
+ (!GameImage.SLIDER_BALL.hasBeatmapSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
sliderBallImages = GameImage.SLIDER_BALL.getImages();
else
sliderBallImages = new Image[]{ GameImage.SLIDER_BALL.getImage() };
@@ -160,7 +155,7 @@ public class Slider implements GameObject {
updatePosition();
// slider time calculations
- this.sliderTime = game.getBeatLength() * (hitObject.getPixelLength() / sliderMultiplier) / 100f;
+ this.sliderTime = hitObject.getSliderTime(sliderMultiplier, game.getBeatLength());
this.sliderTimeTotal = sliderTime * hitObject.getRepeatCount();
// ticks
@@ -178,36 +173,46 @@ public class Slider implements GameObject {
@Override
public void draw(Graphics g, int trackPosition) {
int timeDiff = hitObject.getTime() - trackPosition;
- float scale = timeDiff / (float) game.getApproachTime();
- float fadeinScale = (timeDiff - game.getApproachTime() + FADE_IN_TIME) / (float) FADE_IN_TIME;
+ final int approachTime = game.getApproachTime();
+ final int fadeInTime = game.getFadeInTime();
+ float scale = timeDiff / (float) approachTime;
float approachScale = 1 + scale * 3;
+ float fadeinScale = (timeDiff - approachTime + fadeInTime) / (float) fadeInTime;
float alpha = Utils.clamp(1 - fadeinScale, 0, 1);
boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
- float oldAlpha = Utils.COLOR_WHITE_FADE.a;
- Utils.COLOR_WHITE_FADE.a = color.a = alpha;
+ float oldAlpha = Colors.WHITE_FADE.a;
+ Colors.WHITE_FADE.a = color.a = alpha;
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
Image hitCircle = GameImage.HITCIRCLE.getImage();
- float[] endPos = curve.pointAt(1);
+ Vec2f endPos = curve.pointAt(1);
curve.draw(color);
color.a = alpha;
// end circle
- hitCircle.drawCentered(endPos[0], endPos[1], color);
- hitCircleOverlay.drawCentered(endPos[0], endPos[1], Utils.COLOR_WHITE_FADE);
+ hitCircle.drawCentered(endPos.x, endPos.y, color);
+ hitCircleOverlay.drawCentered(endPos.x, endPos.y, Colors.WHITE_FADE);
// start circle
hitCircle.drawCentered(x, y, color);
if (!overlayAboveNumber)
- hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE);
+ hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE);
// ticks
if (ticksT != null) {
Image tick = GameImage.SLIDER_TICK.getImage();
for (int i = 0; i < ticksT.length; i++) {
- float[] c = curve.pointAt(ticksT[i]);
- tick.drawCentered(c[0], c[1], Utils.COLOR_WHITE_FADE);
+ Vec2f c = curve.pointAt(ticksT[i]);
+ tick.drawCentered(c.x, c.y, Colors.WHITE_FADE);
+ }
+ }
+ if (GameMod.HIDDEN.isActive()) {
+ final int hiddenDecayTime = game.getHiddenDecayTime();
+ final int hiddenTimeDiff = game.getHiddenTimeDiff();
+ if (fadeinScale <= 0f && timeDiff < hiddenTimeDiff + hiddenDecayTime) {
+ float hiddenAlpha = (timeDiff < hiddenTimeDiff) ? 0f : (timeDiff - hiddenTimeDiff) / (float) hiddenDecayTime;
+ alpha = Math.min(alpha, hiddenAlpha);
}
}
if (sliderClickedInitial)
@@ -216,7 +221,7 @@ public class Slider implements GameObject {
data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
hitCircle.getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
if (overlayAboveNumber)
- hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE);
+ hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE);
// repeats
for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) {
@@ -232,7 +237,7 @@ public class Slider implements GameObject {
if (tcurRepeat % 2 == 0) {
// last circle
arrow.setRotation(curve.getEndAngle());
- arrow.drawCentered(endPos[0], endPos[1]);
+ arrow.drawCentered(endPos.x, endPos.y);
} else {
// first circle
arrow.setRotation(curve.getStartAngle());
@@ -243,40 +248,41 @@ public class Slider implements GameObject {
if (timeDiff >= 0) {
// approach circle
- GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
+ if (!GameMod.HIDDEN.isActive())
+ GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
} else {
// Since update() might not have run before drawing during a replay, the
// slider time may not have been calculated, which causes NAN numbers and flicker.
if (sliderTime == 0)
return;
- float[] c = curve.pointAt(getT(trackPosition, false));
- float[] c2 = curve.pointAt(getT(trackPosition, false) + 0.01f);
+ Vec2f c = curve.pointAt(getT(trackPosition, false));
+ Vec2f c2 = curve.pointAt(getT(trackPosition, false) + 0.01f);
float t = getT(trackPosition, false);
// float dis = hitObject.getPixelLength() * HitObject.getXMultiplier() * (t - (int) t);
// Image sliderBallFrame = sliderBallImages[(int) (dis / (diameter * Math.PI) * 30) % sliderBallImages.length];
Image sliderBallFrame = sliderBallImages[(int) (t * sliderTime * 60 / 1000) % sliderBallImages.length];
- float angle = (float) (Math.atan2(c2[1] - c[1], c2[0] - c[0]) * 180 / Math.PI);
+ float angle = (float) (Math.atan2(c2.y - c.y, c2.x - c.x) * 180 / Math.PI);
sliderBallFrame.setRotation(angle);
- sliderBallFrame.drawCentered(c[0], c[1]);
+ sliderBallFrame.drawCentered(c.x, c.y);
// follow circle
if (followCircleActive) {
- GameImage.SLIDER_FOLLOWCIRCLE.getImage().drawCentered(c[0], c[1]);
+ GameImage.SLIDER_FOLLOWCIRCLE.getImage().drawCentered(c.x, c.y);
// "flashlight" mod: dim the screen
if (GameMod.FLASHLIGHT.isActive()) {
- float oldAlphaBlack = Utils.COLOR_BLACK_ALPHA.a;
- Utils.COLOR_BLACK_ALPHA.a = 0.75f;
- g.setColor(Utils.COLOR_BLACK_ALPHA);
+ float oldAlphaBlack = Colors.BLACK_ALPHA.a;
+ Colors.BLACK_ALPHA.a = 0.75f;
+ g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, containerWidth, containerHeight);
- Utils.COLOR_BLACK_ALPHA.a = oldAlphaBlack;
+ Colors.BLACK_ALPHA.a = oldAlphaBlack;
}
}
}
- Utils.COLOR_WHITE_FADE.a = oldAlpha;
+ Colors.WHITE_FADE.a = oldAlpha;
}
/**
@@ -346,9 +352,9 @@ public class Slider implements GameObject {
float cx, cy;
HitObjectType type;
if (currentRepeats % 2 == 0) { // last circle
- float[] lastPos = curve.pointAt(1);
- cx = lastPos[0];
- cy = lastPos[1];
+ Vec2f lastPos = curve.pointAt(1);
+ cx = lastPos.x;
+ cy = lastPos.y;
type = HitObjectType.SLIDER_LAST;
} else { // first circle
cx = x;
@@ -429,8 +435,8 @@ public class Slider implements GameObject {
// check if cursor pressed and within end circle
if (keyPressed || GameMod.RELAX.isActive()) {
- float[] c = curve.pointAt(getT(trackPosition, false));
- double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
+ Vec2f c = curve.pointAt(getT(trackPosition, false));
+ double distance = Math.hypot(c.x - mouseX, c.y - mouseY);
if (distance < followRadius)
sliderHeldToEnd = true;
}
@@ -473,12 +479,11 @@ public class Slider implements GameObject {
}
// holding slider...
- float[] c = curve.pointAt(getT(trackPosition, false));
- double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
+ Vec2f c = curve.pointAt(getT(trackPosition, false));
+ double distance = Math.hypot(c.x - mouseX, c.y - mouseY);
if (((keyPressed || GameMod.RELAX.isActive()) && distance < followRadius) || isAutoMod) {
// mouse pressed and within follow circle
followCircleActive = true;
- data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER);
// held during new repeat
if (isNewRepeat) {
@@ -489,14 +494,14 @@ public class Slider implements GameObject {
curve.getX(lastIndex), curve.getY(lastIndex), hitObject, currentRepeats);
} else // first circle
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30,
- c[0], c[1], hitObject, currentRepeats);
+ c.x, c.y, hitObject, currentRepeats);
}
// held during new tick
if (isNewTick) {
ticksHit++;
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER10,
- c[0], c[1], hitObject, currentRepeats);
+ c.x, c.y, hitObject, currentRepeats);
}
// held near end of slider
@@ -518,22 +523,16 @@ public class Slider implements GameObject {
public void updatePosition() {
this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY();
-
- if (hitObject.getSliderType() == HitObject.SLIDER_PASSTHROUGH && hitObject.getSliderX().length == 2)
- this.curve = new CircumscribedCircle(hitObject, color);
- else if (hitObject.getSliderType() == HitObject.SLIDER_CATMULL)
- this.curve = new CatmullCurve(hitObject, color);
- else
- this.curve = new LinearBezier(hitObject, color, hitObject.getSliderType() == HitObject.SLIDER_LINEAR);
+ this.curve = hitObject.getSliderCurve(true);
}
@Override
- public float[] getPointAt(int trackPosition) {
+ public Vec2f getPointAt(int trackPosition) {
if (trackPosition <= hitObject.getTime())
- return new float[] { x, y };
+ return new Vec2f(x, y);
else if (trackPosition >= hitObject.getTime() + sliderTimeTotal) {
if (hitObject.getRepeatCount() % 2 == 0)
- return new float[] { x, y };
+ return new Vec2f(x, y);
else
return curve.pointAt(1);
} else
diff --git a/src/itdelatrisu/opsu/objects/Spinner.java b/src/itdelatrisu/opsu/objects/Spinner.java
index 3f9cdcab..e82fa6a3 100644
--- a/src/itdelatrisu/opsu/objects/Spinner.java
+++ b/src/itdelatrisu/opsu/objects/Spinner.java
@@ -27,7 +27,9 @@ import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.HitObject;
+import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.states.Game;
+import itdelatrisu.opsu.ui.Colors;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
@@ -50,9 +52,6 @@ public class Spinner implements GameObject {
/** The amount of time, in milliseconds, before another velocity is stored. */
private static final float DELTA_UPDATE_TIME = 1000 / 60f;
- /** The amount of time, in milliseconds, to fade in the spinner. */
- private static final int FADE_IN_TIME = 500;
-
/** Angle mod multipliers: "auto" (477rpm), "spun out" (287rpm) */
private static final float
AUTO_MULTIPLIER = 1 / 20f, // angle = 477/60f * delta/1000f * TWO_PI;
@@ -69,6 +68,9 @@ public class Spinner implements GameObject {
/** The associated HitObject. */
private HitObject hitObject;
+ /** The associated Game object. */
+ private Game game;
+
/** The associated GameData object. */
private GameData data;
@@ -124,6 +126,7 @@ public class Spinner implements GameObject {
*/
public Spinner(HitObject hitObject, Game game, GameData data) {
this.hitObject = hitObject;
+ this.game = game;
this.data = data;
/*
@@ -162,7 +165,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];
@@ -175,20 +178,21 @@ public class Spinner implements GameObject {
public void draw(Graphics g, int trackPosition) {
// only draw spinners shortly before start time
int timeDiff = hitObject.getTime() - trackPosition;
- if (timeDiff - FADE_IN_TIME > 0)
+ final int fadeInTime = game.getFadeInTime();
+ if (timeDiff - fadeInTime > 0)
return;
boolean spinnerComplete = (rotations >= rotationsNeeded);
- float alpha = Utils.clamp(1 - (float) timeDiff / FADE_IN_TIME, 0f, 1f);
+ float alpha = Utils.clamp(1 - (float) timeDiff / fadeInTime, 0f, 1f);
// darken screen
if (Options.getSkin().isSpinnerFadePlayfield()) {
- float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
+ float oldAlpha = Colors.BLACK_ALPHA.a;
if (timeDiff > 0)
- Utils.COLOR_BLACK_ALPHA.a *= alpha;
- g.setColor(Utils.COLOR_BLACK_ALPHA);
+ Colors.BLACK_ALPHA.a *= alpha;
+ g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height);
- Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
+ Colors.BLACK_ALPHA.a = oldAlpha;
}
// rpm
@@ -210,13 +214,15 @@ public class Spinner implements GameObject {
spinnerMetreSub.draw(0, height - spinnerMetreSub.getHeight());
// main spinner elements
- float approachScale = 1 - Utils.clamp(((float) timeDiff / (hitObject.getTime() - hitObject.getEndTime())), 0f, 1f);
GameImage.SPINNER_CIRCLE.getImage().setAlpha(alpha);
GameImage.SPINNER_CIRCLE.getImage().setRotation(drawRotation * 360f);
GameImage.SPINNER_CIRCLE.getImage().drawCentered(width / 2, height / 2);
- Image approachCircleScaled = GameImage.SPINNER_APPROACHCIRCLE.getImage().getScaledCopy(approachScale);
- approachCircleScaled.setAlpha(alpha);
- approachCircleScaled.drawCentered(width / 2, height / 2);
+ if (!GameMod.HIDDEN.isActive()) {
+ float approachScale = 1 - Utils.clamp(((float) timeDiff / (hitObject.getTime() - hitObject.getEndTime())), 0f, 1f);
+ Image approachCircleScaled = GameImage.SPINNER_APPROACHCIRCLE.getImage().getScaledCopy(approachScale);
+ approachCircleScaled.setAlpha(alpha);
+ approachCircleScaled.drawCentered(width / 2, height / 2);
+ }
GameImage.SPINNER_SPIN.getImage().setAlpha(alpha);
GameImage.SPINNER_SPIN.getImage().drawCentered(width / 2, height * 3 / 4);
@@ -342,7 +348,7 @@ public class Spinner implements GameObject {
public void updatePosition() {}
@Override
- public float[] getPointAt(int trackPosition) {
+ public Vec2f getPointAt(int trackPosition) {
// get spinner time
int timeDiff;
float x = hitObject.getScaledX(), y = hitObject.getScaledY();
@@ -357,10 +363,7 @@ public class Spinner implements GameObject {
float multiplier = (GameMod.AUTO.isActive()) ? AUTO_MULTIPLIER : SPUN_OUT_MULTIPLIER;
float angle = (timeDiff * multiplier) - HALF_PI;
final float r = height / 10f;
- return new float[] {
- (float) (x + r * Math.cos(angle)),
- (float) (y + r * Math.sin(angle))
- };
+ return new Vec2f((float) (x + r * Math.cos(angle)), (float) (y + r * Math.sin(angle)));
}
@Override
diff --git a/src/itdelatrisu/opsu/objects/curves/CatmullCurve.java b/src/itdelatrisu/opsu/objects/curves/CatmullCurve.java
index 95842e77..0d8c61f5 100644
--- a/src/itdelatrisu/opsu/objects/curves/CatmullCurve.java
+++ b/src/itdelatrisu/opsu/objects/curves/CatmullCurve.java
@@ -18,14 +18,10 @@
package itdelatrisu.opsu.objects.curves;
-import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.beatmap.HitObject;
import java.util.LinkedList;
-import org.newdawn.slick.Color;
-import org.newdawn.slick.SlickException;
-
/**
* Representation of Catmull Curve with equidistant points.
*
@@ -35,10 +31,18 @@ public class CatmullCurve extends EqualDistanceMultiCurve {
/**
* Constructor.
* @param hitObject the associated HitObject
- * @param color the color of this curve
*/
- public CatmullCurve(HitObject hitObject, Color color) {
- super(hitObject, color);
+ public CatmullCurve(HitObject hitObject) {
+ this(hitObject, true);
+ }
+
+ /**
+ * Constructor.
+ * @param hitObject the associated HitObject
+ * @param scaled whether to use scaled coordinates
+ */
+ public CatmullCurve(HitObject hitObject, boolean scaled) {
+ super(hitObject, scaled);
LinkedList catmulls = new LinkedList();
int ncontrolPoints = hitObject.getSliderX().length + 1;
LinkedList points = new LinkedList(); // temporary list of points to separate different curves
@@ -53,24 +57,15 @@ public class CatmullCurve extends EqualDistanceMultiCurve {
for (int i = 0; i < ncontrolPoints; i++) {
points.addLast(new Vec2f(getX(i), getY(i)));
if (points.size() >= 4) {
- try {
- catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
- } catch (SlickException e) {
- ErrorHandler.error(null, e, true);
- }
+ catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
points.removeFirst();
}
}
- if (getX(ncontrolPoints - 1) != getX(ncontrolPoints - 2)
- ||getY(ncontrolPoints - 1) != getY(ncontrolPoints - 2))
- points.addLast(new Vec2f(getX(ncontrolPoints - 1), getY(ncontrolPoints - 1)));
- if (points.size() >= 4) {
- try {
- catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
- } catch (SlickException e) {
- ErrorHandler.error(null, e, true);
- }
- }
+ if (getX(ncontrolPoints - 1) != getX(ncontrolPoints - 2) ||
+ getY(ncontrolPoints - 1) != getY(ncontrolPoints - 2))
+ points.addLast(new Vec2f(getX(ncontrolPoints - 1), getY(ncontrolPoints - 1)));
+ if (points.size() >= 4)
+ catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
init(catmulls);
}
diff --git a/src/itdelatrisu/opsu/objects/curves/CentripetalCatmullRom.java b/src/itdelatrisu/opsu/objects/curves/CentripetalCatmullRom.java
index 5a13d7ce..c45726dc 100644
--- a/src/itdelatrisu/opsu/objects/curves/CentripetalCatmullRom.java
+++ b/src/itdelatrisu/opsu/objects/curves/CentripetalCatmullRom.java
@@ -18,8 +18,6 @@
package itdelatrisu.opsu.objects.curves;
-import org.newdawn.slick.SlickException;
-
/**
* Representation of a Centripetal Catmull–Rom spline.
* (Currently not technically Centripetal Catmull–Rom.)
@@ -37,11 +35,10 @@ public class CentripetalCatmullRom extends CurveType {
/**
* Constructor.
* @param points the control points of the curve
- * @throws SlickException
*/
- protected CentripetalCatmullRom(Vec2f[] points) throws SlickException {
+ protected CentripetalCatmullRom(Vec2f[] points) {
if (points.length != 4)
- throw new SlickException(String.format("Need exactly 4 points to initialize CentripetalCatmullRom, %d provided.", points.length));
+ throw new RuntimeException(String.format("Need exactly 4 points to initialize CentripetalCatmullRom, %d provided.", points.length));
this.points = points;
time = new float[4];
diff --git a/src/itdelatrisu/opsu/objects/curves/CircumscribedCircle.java b/src/itdelatrisu/opsu/objects/curves/CircumscribedCircle.java
index b62c17af..866ade2e 100644
--- a/src/itdelatrisu/opsu/objects/curves/CircumscribedCircle.java
+++ b/src/itdelatrisu/opsu/objects/curves/CircumscribedCircle.java
@@ -18,11 +18,9 @@
package itdelatrisu.opsu.objects.curves;
-import itdelatrisu.opsu.ErrorHandler;
+import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.HitObject;
-import org.newdawn.slick.Color;
-
/**
* Representation of a curve along a Circumscribed Circle of three points.
* http://en.wikipedia.org/wiki/Circumscribed_circle
@@ -53,10 +51,18 @@ public class CircumscribedCircle extends Curve {
/**
* Constructor.
* @param hitObject the associated HitObject
- * @param color the color of this curve
*/
- public CircumscribedCircle(HitObject hitObject, Color color) {
- super(hitObject, color);
+ public CircumscribedCircle(HitObject hitObject) {
+ this(hitObject, true);
+ }
+
+ /**
+ * Constructor.
+ * @param hitObject the associated HitObject
+ * @param scaled whether to use scaled coordinates
+ */
+ public CircumscribedCircle(HitObject hitObject, boolean scaled) {
+ super(hitObject, scaled);
// construct the three points
this.start = new Vec2f(getX(0), getY(0));
@@ -70,8 +76,6 @@ public class CircumscribedCircle extends Curve {
Vec2f norb = mid.cpy().sub(end).nor();
this.circleCenter = intersect(mida, nora, midb, norb);
- if (circleCenter == null)
- return;
// find the angles relative to the circle center
Vec2f startAngPoint = start.cpy().sub(circleCenter);
@@ -92,13 +96,8 @@ public class CircumscribedCircle extends Curve {
startAng -= TWO_PI;
else if (Math.abs(startAng - (endAng - TWO_PI)) < TWO_PI && isIn(startAng, midAng, endAng - (TWO_PI)))
endAng -= TWO_PI;
- else {
- ErrorHandler.error(
- String.format("Cannot find angles between midAng (%.3f %.3f %.3f).",
- startAng, midAng, endAng), null, true
- );
- return;
- }
+ else
+ throw new RuntimeException(String.format("Cannot find angles between midAng (%.3f %.3f %.3f).", startAng, midAng, endAng));
}
// find an angle with an arc length of pixelLength along this circle
@@ -116,10 +115,8 @@ public class CircumscribedCircle extends Curve {
// calculate points
float step = hitObject.getPixelLength() / CURVE_POINTS_SEPERATION;
curve = new Vec2f[(int) step + 1];
- for (int i = 0; i < curve.length; i++) {
- float[] xy = pointAt(i / step);
- curve[i] = new Vec2f(xy[0], xy[1]);
- }
+ for (int i = 0; i < curve.length; i++)
+ curve[i] = pointAt(i / step);
}
/**
@@ -151,21 +148,19 @@ public class CircumscribedCircle extends Curve {
//u = ((b.y-a.y)ta.x +(a.x-b.x)ta.y) / (tb.x*ta.y - tb.y*ta.x);
float des = tb.x * ta.y - tb.y * ta.x;
- if (Math.abs(des) < 0.00001f) {
- ErrorHandler.error("Vectors are parallel.", null, true);
- return null;
- }
+ if (Math.abs(des) < 0.00001f)
+ throw new RuntimeException("Vectors are parallel.");
float u = ((b.y - a.y) * ta.x + (a.x - b.x) * ta.y) / des;
return b.cpy().add(tb.x * u, tb.y * u);
}
@Override
- public float[] pointAt(float t) {
- float ang = lerp(startAng, endAng, t);
- return new float[] {
+ public Vec2f pointAt(float t) {
+ float ang = Utils.lerp(startAng, endAng, t);
+ return new Vec2f(
(float) (Math.cos(ang) * radius + circleCenter.x),
(float) (Math.sin(ang) * radius + circleCenter.y)
- };
+ );
}
@Override
diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java
index 0d2f4609..313013db 100644
--- a/src/itdelatrisu/opsu/objects/curves/Curve.java
+++ b/src/itdelatrisu/opsu/objects/curves/Curve.java
@@ -20,10 +20,10 @@ package itdelatrisu.opsu.objects.curves;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.render.CurveRenderState;
import itdelatrisu.opsu.skins.Skin;
+import itdelatrisu.opsu.ui.Colors;
import org.lwjgl.opengl.ContextCapabilities;
import org.lwjgl.opengl.GLContext;
@@ -64,14 +64,21 @@ public abstract class Curve {
/**
* Constructor.
* @param hitObject the associated HitObject
- * @param color the color of this curve
+ * @param scaled whether to use scaled coordinates
*/
- protected Curve(HitObject hitObject, Color color) {
+ protected Curve(HitObject hitObject, boolean scaled) {
this.hitObject = hitObject;
- this.x = hitObject.getScaledX();
- this.y = hitObject.getScaledY();
- this.sliderX = hitObject.getScaledSliderX();
- this.sliderY = hitObject.getScaledSliderY();
+ if (scaled) {
+ this.x = hitObject.getScaledX();
+ this.y = hitObject.getScaledY();
+ this.sliderX = hitObject.getScaledSliderX();
+ this.sliderY = hitObject.getScaledSliderY();
+ } else {
+ this.x = hitObject.getX();
+ this.y = hitObject.getY();
+ this.sliderX = hitObject.getSliderX();
+ this.sliderY = hitObject.getSliderY();
+ }
this.renderState = null;
}
@@ -80,28 +87,28 @@ public abstract class Curve {
* Should be called before any curves are drawn.
* @param width the container width
* @param height the container height
- * @param circleSize the circle size
+ * @param circleDiameter the circle diameter
* @param borderColor the curve border color
*/
- public static void init(int width, int height, float circleSize, Color borderColor) {
+ public static void init(int width, int height, float circleDiameter, Color borderColor) {
Curve.borderColor = borderColor;
ContextCapabilities capabilities = GLContext.getCapabilities();
- mmsliderSupported = capabilities.GL_EXT_framebuffer_object && capabilities.OpenGL32;
+ mmsliderSupported = capabilities.GL_EXT_framebuffer_object;
if (mmsliderSupported)
- CurveRenderState.init(width, height, circleSize);
+ CurveRenderState.init(width, height, circleDiameter);
else {
if (Options.getSkin().getSliderStyle() != Skin.STYLE_PEPPYSLIDER)
- Log.warn("New slider style requires FBO support and OpenGL 3.2.");
+ Log.warn("New slider style requires FBO support.");
}
}
/**
* Returns the point on the curve at a value t.
* @param t the t value [0, 1]
- * @return the point [x, y]
+ * @return the position vector
*/
- public abstract float[] pointAt(float t);
+ public abstract Vec2f pointAt(float t);
/**
* Draws the full curve to the graphics context.
@@ -116,7 +123,7 @@ public abstract class Curve {
Image hitCircle = GameImage.HITCIRCLE.getImage();
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
for (int i = 0; i < curve.length; i++)
- hitCircleOverlay.drawCentered(curve[i].x, curve[i].y, Utils.COLOR_WHITE_FADE);
+ hitCircleOverlay.drawCentered(curve[i].x, curve[i].y, Colors.WHITE_FADE);
for (int i = 0; i < curve.length; i++)
hitCircle.drawCentered(curve[i].x, curve[i].y, color);
}
@@ -151,13 +158,6 @@ public abstract class Curve {
*/
public float getY(int i) { return (i == 0) ? y : sliderY[i - 1]; }
- /**
- * Linear interpolation of a and b at t.
- */
- protected float lerp(float a, float b, float t) {
- return a * (1 - t) + b * t;
- }
-
/**
* Discards the slider cache (only used for mmsliders).
*/
diff --git a/src/itdelatrisu/opsu/objects/curves/EqualDistanceMultiCurve.java b/src/itdelatrisu/opsu/objects/curves/EqualDistanceMultiCurve.java
index a15a1657..4393d1aa 100644
--- a/src/itdelatrisu/opsu/objects/curves/EqualDistanceMultiCurve.java
+++ b/src/itdelatrisu/opsu/objects/curves/EqualDistanceMultiCurve.java
@@ -18,13 +18,12 @@
package itdelatrisu.opsu.objects.curves;
+import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.HitObject;
import java.util.Iterator;
import java.util.LinkedList;
-import org.newdawn.slick.Color;
-
/**
* Representation of multiple curve with equidistant points.
* http://pomax.github.io/bezierinfo/#tracing
@@ -41,10 +40,18 @@ public abstract class EqualDistanceMultiCurve extends Curve {
/**
* Constructor.
* @param hitObject the associated HitObject
- * @param color the color of this curve
*/
- public EqualDistanceMultiCurve(HitObject hitObject, Color color) {
- super(hitObject, color);
+ public EqualDistanceMultiCurve(HitObject hitObject) {
+ this(hitObject, true);
+ }
+
+ /**
+ * Constructor.
+ * @param hitObject the associated HitObject
+ * @param scaled whether to use scaled coordinates
+ */
+ public EqualDistanceMultiCurve(HitObject hitObject, boolean scaled) {
+ super(hitObject, scaled);
}
/**
@@ -94,7 +101,7 @@ public abstract class EqualDistanceMultiCurve extends Curve {
// interpolate the point between the two closest distances
if (distanceAt - lastDistanceAt > 1) {
float t = (prefDistance - lastDistanceAt) / (distanceAt - lastDistanceAt);
- curve[i] = new Vec2f(lerp(lastCurve.x, thisCurve.x, t), lerp(lastCurve.y, thisCurve.y, t));
+ curve[i] = new Vec2f(Utils.lerp(lastCurve.x, thisCurve.x, t), Utils.lerp(lastCurve.y, thisCurve.y, t));
} else
curve[i] = thisCurve;
}
@@ -117,20 +124,19 @@ public abstract class EqualDistanceMultiCurve extends Curve {
}
@Override
- public float[] pointAt(float t) {
+ public Vec2f pointAt(float t) {
float indexF = t * ncurve;
int index = (int) indexF;
- if (index >= ncurve) {
- Vec2f poi = curve[ncurve];
- return new float[] { poi.x, poi.y };
- } else {
+ if (index >= ncurve)
+ return curve[ncurve].cpy();
+ else {
Vec2f poi = curve[index];
Vec2f poi2 = curve[index + 1];
float t2 = indexF - index;
- return new float[] {
- lerp(poi.x, poi2.x, t2),
- lerp(poi.y, poi2.y, t2)
- };
+ return new Vec2f(
+ Utils.lerp(poi.x, poi2.x, t2),
+ Utils.lerp(poi.y, poi2.y, t2)
+ );
}
}
diff --git a/src/itdelatrisu/opsu/objects/curves/LinearBezier.java b/src/itdelatrisu/opsu/objects/curves/LinearBezier.java
index 3ec6717a..4defb192 100644
--- a/src/itdelatrisu/opsu/objects/curves/LinearBezier.java
+++ b/src/itdelatrisu/opsu/objects/curves/LinearBezier.java
@@ -22,8 +22,6 @@ import itdelatrisu.opsu.beatmap.HitObject;
import java.util.LinkedList;
-import org.newdawn.slick.Color;
-
/**
* Representation of Bezier curve with equidistant points.
* http://pomax.github.io/bezierinfo/#tracing
@@ -34,11 +32,20 @@ public class LinearBezier extends EqualDistanceMultiCurve {
/**
* Constructor.
* @param hitObject the associated HitObject
- * @param color the color of this curve
* @param line whether a new curve should be generated for each sequential pair
*/
- public LinearBezier(HitObject hitObject, Color color, boolean line) {
- super(hitObject, color);
+ public LinearBezier(HitObject hitObject, boolean line) {
+ this(hitObject, line, true);
+ }
+
+ /**
+ * Constructor.
+ * @param hitObject the associated HitObject
+ * @param line whether a new curve should be generated for each sequential pair
+ * @param scaled whether to use scaled coordinates
+ */
+ public LinearBezier(HitObject hitObject, boolean line, boolean scaled) {
+ super(hitObject, scaled);
LinkedList beziers = new LinkedList();
diff --git a/src/itdelatrisu/opsu/objects/curves/Vec2f.java b/src/itdelatrisu/opsu/objects/curves/Vec2f.java
index 1005cd1d..3858823d 100644
--- a/src/itdelatrisu/opsu/objects/curves/Vec2f.java
+++ b/src/itdelatrisu/opsu/objects/curves/Vec2f.java
@@ -40,6 +40,16 @@ public class Vec2f {
*/
public Vec2f() {}
+ /**
+ * Sets the x and y components of this vector.
+ * @return itself (for chaining)
+ */
+ public Vec2f set(float nx, float ny) {
+ x = nx;
+ y = ny;
+ return this;
+ }
+
/**
* Finds the midpoint between this vector and another vector.
* @param o the other vector
@@ -93,6 +103,17 @@ public class Vec2f {
return this;
}
+ /**
+ * Turns this vector into a unit vector.
+ * @return itself (for chaining)
+ */
+ public Vec2f normalize() {
+ float len = len();
+ x /= len;
+ y /= len;
+ return this;
+ }
+
/**
* Returns a copy of this vector.
*/
diff --git a/src/itdelatrisu/opsu/render/CurveRenderState.java b/src/itdelatrisu/opsu/render/CurveRenderState.java
index bd7a0914..2d1ac1ad 100644
--- a/src/itdelatrisu/opsu/render/CurveRenderState.java
+++ b/src/itdelatrisu/opsu/render/CurveRenderState.java
@@ -18,20 +18,21 @@
package itdelatrisu.opsu.render;
import itdelatrisu.opsu.GameImage;
-import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.objects.curves.Vec2f;
+import itdelatrisu.opsu.ui.Colors;
import java.nio.ByteBuffer;
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;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
-import org.lwjgl.opengl.GL30;
import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
import org.newdawn.slick.util.Log;
@@ -63,15 +64,14 @@ public class CurveRenderState {
* Should be called before any curves are drawn.
* @param width the container width
* @param height the container height
- * @param circleSize the circle size
+ * @param circleDiameter the circle diameter
*/
- public static void init(int width, int height, float circleSize) {
+ public static void init(int width, int height, float circleDiameter) {
containerWidth = width;
containerHeight = height;
// equivalent to what happens in Slider.init()
- scale = (int) (104 - (circleSize * 8));
- scale = (int) (scale * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
+ scale = (int) (circleDiameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
//scale = scale * 118 / 128; //for curves exactly as big as the sliderball
FrameBufferCache.init(width, height);
}
@@ -114,20 +114,25 @@ 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(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT);
+ int oldTex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
- GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fbo.getID());
+ //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);
+ 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);
- Utils.COLOR_WHITE_FADE.a = 1.0f;
+ Colors.WHITE_FADE.a = 1.0f;
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);
- Utils.COLOR_WHITE_FADE.a = alpha;
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, oldTex);
+ EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, oldFb);
+ GL11.glViewport(oldViewport.get(0), oldViewport.get(1), oldViewport.get(2), oldViewport.get(3));
+ Colors.WHITE_FADE.a = alpha;
}
// draw a fullscreen quad with the texture that contains the curve
@@ -389,7 +394,7 @@ public class CurveRenderState {
buff.flip();
GL11.glBindTexture(GL11.GL_TEXTURE_1D, gradientTexture);
GL11.glTexImage1D(GL11.GL_TEXTURE_1D, 0, GL11.GL_RGBA, slider.getWidth(), 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buff);
- GL30.glGenerateMipmap(GL11.GL_TEXTURE_1D);
+ EXTFramebufferObject.glGenerateMipmapEXT(GL11.GL_TEXTURE_1D);
}
}
@@ -402,12 +407,12 @@ public class CurveRenderState {
program = GL20.glCreateProgram();
int vtxShdr = GL20.glCreateShader(GL20.GL_VERTEX_SHADER);
int frgShdr = GL20.glCreateShader(GL20.GL_FRAGMENT_SHADER);
- GL20.glShaderSource(vtxShdr, "#version 330\n"
+ GL20.glShaderSource(vtxShdr, "#version 110\n"
+ "\n"
- + "layout(location = 0) in vec4 in_position;\n"
- + "layout(location = 1) in vec2 in_tex_coord;\n"
+ + "attribute vec4 in_position;\n"
+ + "attribute vec2 in_tex_coord;\n"
+ "\n"
- + "out vec2 tex_coord;\n"
+ + "varying vec2 tex_coord;\n"
+ "void main()\n"
+ "{\n"
+ " gl_Position = in_position;\n"
@@ -419,22 +424,21 @@ public class CurveRenderState {
String error = GL20.glGetShaderInfoLog(vtxShdr, 1024);
Log.error("Vertex Shader compilation failed.", new Exception(error));
}
- GL20.glShaderSource(frgShdr, "#version 330\n"
+ GL20.glShaderSource(frgShdr, "#version 110\n"
+ "\n"
+ "uniform sampler1D tex;\n"
+ "uniform vec2 tex_size;\n"
+ "uniform vec3 col_tint;\n"
+ "uniform vec4 col_border;\n"
+ "\n"
- + "in vec2 tex_coord;\n"
- + "layout(location = 0) out vec4 out_colour;\n"
+ + "varying vec2 tex_coord;\n"
+ "\n"
+ "void main()\n"
+ "{\n"
- + " vec4 in_color = texture(tex, tex_coord.x);\n"
+ + " vec4 in_color = texture1D(tex, tex_coord.x);\n"
+ " float blend_factor = in_color.r-in_color.b;\n"
+ " vec4 new_color = vec4(mix(in_color.xyz*col_border.xyz,col_tint,blend_factor),in_color.w);\n"
- + " out_colour = new_color;\n"
+ + " gl_FragColor = new_color;\n"
+ "}");
GL20.glCompileShader(frgShdr);
res = GL20.glGetShaderi(frgShdr, GL20.GL_COMPILE_STATUS);
diff --git a/src/itdelatrisu/opsu/render/Rendertarget.java b/src/itdelatrisu/opsu/render/Rendertarget.java
index c6f19387..abd16b9d 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,7 +91,7 @@ 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);
Rendertarget buffer = new Rendertarget(width,height);
buffer.bind();
@@ -104,15 +102,14 @@ 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 +119,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);
}
}
diff --git a/src/itdelatrisu/opsu/replay/LifeFrame.java b/src/itdelatrisu/opsu/replay/LifeFrame.java
index 045075f9..de7daea4 100644
--- a/src/itdelatrisu/opsu/replay/LifeFrame.java
+++ b/src/itdelatrisu/opsu/replay/LifeFrame.java
@@ -25,10 +25,10 @@ package itdelatrisu.opsu.replay;
*/
public class LifeFrame {
/** Time. */
- private int time;
+ private final int time;
/** Percentage. */
- private float percentage;
+ private final float percentage;
/**
* Constructor.
diff --git a/src/itdelatrisu/opsu/replay/PlaybackSpeed.java b/src/itdelatrisu/opsu/replay/PlaybackSpeed.java
index ec2fad6d..4b17adac 100644
--- a/src/itdelatrisu/opsu/replay/PlaybackSpeed.java
+++ b/src/itdelatrisu/opsu/replay/PlaybackSpeed.java
@@ -35,14 +35,14 @@ public enum PlaybackSpeed {
HALF (GameImage.REPLAY_PLAYBACK_HALF, 0.5f);
/** The button image. */
- private GameImage gameImage;
+ private final GameImage gameImage;
+
+ /** The playback speed modifier. */
+ private final float modifier;
/** The button. */
private MenuButton button;
- /** The playback speed modifier. */
- private float modifier;
-
/** Enum values. */
private static PlaybackSpeed[] values = PlaybackSpeed.values();
diff --git a/src/itdelatrisu/opsu/replay/Replay.java b/src/itdelatrisu/opsu/replay/Replay.java
index 505ba128..6f2d9fb4 100644
--- a/src/itdelatrisu/opsu/replay/Replay.java
+++ b/src/itdelatrisu/opsu/replay/Replay.java
@@ -41,11 +41,11 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.List;
-import lzma.streams.LzmaOutputStream;
-
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
import org.newdawn.slick.util.Log;
+import lzma.streams.LzmaOutputStream;
+
/**
* Captures osu! replay data.
* https://osu.ppy.sh/wiki/Osr_%28file_format%29
diff --git a/src/itdelatrisu/opsu/replay/ReplayFrame.java b/src/itdelatrisu/opsu/replay/ReplayFrame.java
index 9f8e9f98..28233a7f 100644
--- a/src/itdelatrisu/opsu/replay/ReplayFrame.java
+++ b/src/itdelatrisu/opsu/replay/ReplayFrame.java
@@ -38,13 +38,13 @@ public class ReplayFrame {
private int timeDiff;
/** Time, in milliseconds. */
- private int time;
+ private final int time;
/** Cursor coordinates (in OsuPixels). */
- private float x, y;
+ private final float x, y;
/** Keys pressed (bitmask). */
- private int keys;
+ private final int keys;
/**
* Returns the start frame.
@@ -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/states/ButtonMenu.java b/src/itdelatrisu/opsu/states/ButtonMenu.java
index c27f0b67..7f31b3cb 100644
--- a/src/itdelatrisu/opsu/states/ButtonMenu.java
+++ b/src/itdelatrisu/opsu/states/ButtonMenu.java
@@ -29,8 +29,11 @@ import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
+import itdelatrisu.opsu.ui.Fonts;
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;
@@ -187,15 +190,15 @@ public class ButtonMenu extends BasicGameState {
float mult = GameMod.getScoreMultiplier();
String multString = String.format("Score Multiplier: %.2fx", mult);
Color multColor = (mult == 1f) ? Color.white : (mult > 1f) ? Color.green : Color.red;
- float multY = Utils.FONT_LARGE.getLineHeight() * 2 + height * 0.06f;
- Utils.FONT_LARGE.drawString(
- (width - Utils.FONT_LARGE.getWidth(multString)) / 2f,
+ float multY = Fonts.LARGE.getLineHeight() * 2 + height * 0.06f;
+ Fonts.LARGE.drawString(
+ (width - Fonts.LARGE.getWidth(multString)) / 2f,
multY, multString, multColor);
// category text
for (GameMod.Category category : GameMod.Category.values()) {
- Utils.FONT_LARGE.drawString(category.getX(),
- category.getY() - Utils.FONT_LARGE.getLineHeight() / 2f,
+ Fonts.LARGE.drawString(category.getX(),
+ category.getY() - Fonts.LARGE.getLineHeight() / 2f,
category.getName(), category.getColor());
}
@@ -217,7 +220,7 @@ public class ButtonMenu extends BasicGameState {
}
// tooltips
- if (hoverMod != null && hoverMod.isImplemented())
+ if (hoverMod != null)
UI.updateTooltip(delta, hoverMod.getDescription(), true);
}
@@ -253,7 +256,7 @@ public class ButtonMenu extends BasicGameState {
};
/** The buttons in the state. */
- private Button[] buttons;
+ private final Button[] buttons;
/** The associated MenuButton objects. */
private MenuButton[] menuButtons;
@@ -261,8 +264,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.
@@ -288,7 +294,7 @@ public class ButtonMenu extends BasicGameState {
menuButtons = new MenuButton[buttons.length];
for (int i = 0; i < buttons.length; i++) {
MenuButton b = new MenuButton(button, buttonL, buttonR, center, baseY + (i * offsetY));
- b.setText(String.format("%d. %s", i + 1, buttons[i].getText()), Utils.FONT_XLARGE, Color.white);
+ b.setText(String.format("%d. %s", i + 1, buttons[i].getText()), Fonts.XLARGE, Color.white);
b.setHoverFade();
menuButtons[i] = b;
}
@@ -301,7 +307,7 @@ public class ButtonMenu extends BasicGameState {
*/
protected float getBaseY(GameContainer container, StateBasedGame game) {
float baseY = container.getHeight() * 0.2f;
- baseY += ((getTitle(container, game).length - 1) * Utils.FONT_LARGE.getLineHeight());
+ baseY += ((getTitle(container, game).length - 1) * Fonts.LARGE.getLineHeight());
return baseY;
}
@@ -315,9 +321,9 @@ public class ButtonMenu extends BasicGameState {
// draw title
if (actualTitle != null) {
float marginX = container.getWidth() * 0.015f, marginY = container.getHeight() * 0.01f;
- int lineHeight = Utils.FONT_LARGE.getLineHeight();
+ int lineHeight = Fonts.LARGE.getLineHeight();
for (int i = 0, size = actualTitle.size(); i < size; i++)
- Utils.FONT_LARGE.drawString(marginX, marginY + (i * lineHeight), actualTitle.get(i), Color.white);
+ Fonts.LARGE.drawString(marginX, marginY + (i * lineHeight), actualTitle.get(i), Color.white);
}
// draw buttons
@@ -336,18 +342,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 +406,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();
}
@@ -416,8 +419,8 @@ public class ButtonMenu extends BasicGameState {
int maxLineWidth = (int) (container.getWidth() * 0.96f);
for (int i = 0; i < title.length; i++) {
// wrap text if too long
- if (Utils.FONT_LARGE.getWidth(title[i]) > maxLineWidth) {
- List list = Utils.wrap(title[i], Utils.FONT_LARGE, maxLineWidth);
+ if (Fonts.LARGE.getWidth(title[i]) > maxLineWidth) {
+ List list = Fonts.wrap(Fonts.LARGE, title[i], maxLineWidth);
actualTitle.addAll(list);
} else
actualTitle.add(title[i]);
@@ -545,10 +548,10 @@ public class ButtonMenu extends BasicGameState {
};
/** The text to show on the button. */
- private String text;
+ private final String text;
/** The button color. */
- private Color color;
+ private final Color color;
/**
* Constructor.
@@ -591,7 +594,7 @@ public class ButtonMenu extends BasicGameState {
private GameContainer container;
private StateBasedGame game;
private Input input;
- private int state;
+ private final int state;
public ButtonMenu(int state) {
this.state = state;
diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java
index 7b80bf4e..b6d43c65 100644
--- a/src/itdelatrisu/opsu/states/DownloadsMenu.java
+++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java
@@ -21,7 +21,6 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
@@ -29,13 +28,19 @@ import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
+import itdelatrisu.opsu.beatmap.OszUnpacker;
import itdelatrisu.opsu.downloads.Download;
import itdelatrisu.opsu.downloads.DownloadList;
import itdelatrisu.opsu.downloads.DownloadNode;
import itdelatrisu.opsu.downloads.servers.BloodcatServer;
import itdelatrisu.opsu.downloads.servers.DownloadServer;
import itdelatrisu.opsu.downloads.servers.HexideServer;
-import itdelatrisu.opsu.downloads.servers.OsuMirrorServer;
+import itdelatrisu.opsu.downloads.servers.MengSkyServer;
+import itdelatrisu.opsu.downloads.servers.MnetworkServer;
+import itdelatrisu.opsu.downloads.servers.YaSOnlineServer;
+import itdelatrisu.opsu.ui.Colors;
+import itdelatrisu.opsu.ui.DropdownMenu;
+import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.KinecticScrolling;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
@@ -75,10 +80,10 @@ public class DownloadsMenu extends BasicGameState {
private static final int MIN_REQUEST_INTERVAL = 300;
/** Available beatmap download servers. */
- private static final DownloadServer[] SERVERS = { new BloodcatServer(), new OsuMirrorServer(), new HexideServer() };
-
- /** The beatmap download server index. */
- private int serverIndex = 0;
+ private static final DownloadServer[] SERVERS = {
+ new BloodcatServer(), new HexideServer(), new YaSOnlineServer(),
+ new MnetworkServer(), new MengSkyServer()
+ };
/** The current list of search results. */
private DownloadNode[] resultList;
@@ -137,14 +142,14 @@ public class DownloadsMenu extends BasicGameState {
/** Page direction for last query. */
private Page lastQueryDir = Page.RESET;
- /** Number of active requests. */
- private int activeRequests = 0;
-
/** Previous and next page buttons. */
private MenuButton prevPage, nextPage;
/** Buttons. */
- private MenuButton clearButton, importButton, resetButton, rankedButton, serverButton;
+ private MenuButton clearButton, importButton, resetButton, rankedButton;
+
+ /** Dropdown menu. */
+ private DropdownMenu serverMenu;
/** Beatmap importing thread. */
private Thread importThread;
@@ -155,11 +160,97 @@ public class DownloadsMenu extends BasicGameState {
/** The bar notification to send upon entering the state. */
private String barNotificationOnLoad;
+ /** Search query, executed in {@code queryThread}. */
+ private SearchQuery searchQuery;
+
+ /** Search query helper class. */
+ private class SearchQuery implements Runnable {
+ /** The search query. */
+ private final String query;
+
+ /** The download server. */
+ private final DownloadServer server;
+
+ /** Whether the query was interrupted. */
+ private boolean interrupted = false;
+
+ /** Whether the query has completed execution. */
+ private boolean complete = false;
+
+ /**
+ * Constructor.
+ * @param query the search query
+ * @param server the download server
+ */
+ public SearchQuery(String query, DownloadServer server) {
+ this.query = query;
+ this.server = server;
+ }
+
+ /** Interrupt the query and prevent the results from being processed, if not already complete. */
+ public void interrupt() { interrupted = true; }
+
+ /** Returns whether the query has completed execution. */
+ public boolean isComplete() { return complete; }
+
+ @Override
+ public void run() {
+ // check page direction
+ Page lastPageDir = pageDir;
+ pageDir = Page.RESET;
+ int lastPageSize = (resultList != null) ? resultList.length : 0;
+ int newPage = page;
+ if (lastPageDir == Page.RESET)
+ newPage = 1;
+ else if (lastPageDir == Page.NEXT)
+ newPage++;
+ else if (lastPageDir == Page.PREVIOUS)
+ newPage--;
+ try {
+ DownloadNode[] nodes = server.resultList(query, newPage, rankedOnly);
+ if (!interrupted) {
+ // update page total
+ page = newPage;
+ if (nodes != null) {
+ if (lastPageDir == Page.NEXT)
+ pageResultTotal += nodes.length;
+ else if (lastPageDir == Page.PREVIOUS)
+ pageResultTotal -= lastPageSize;
+ else if (lastPageDir == Page.RESET)
+ pageResultTotal = nodes.length;
+ } else
+ pageResultTotal = 0;
+
+ resultList = nodes;
+ totalResults = server.totalResults();
+ focusResult = -1;
+ startResultPos.setPosition(0);
+ if (nodes == null)
+ searchResultString = "An error has occurred.";
+ else {
+ if (query.isEmpty())
+ searchResultString = "Type to search!";
+ else if (totalResults == 0 || resultList.length == 0)
+ searchResultString = "No results found.";
+ else
+ searchResultString = String.format("%d result%s found!",
+ totalResults, (totalResults == 1) ? "" : "s");
+ }
+ }
+ } catch (IOException e) {
+ if (!interrupted)
+ searchResultString = "Could not establish connection to server.";
+ } finally {
+ complete = true;
+ }
+ }
+ }
+
// game-related variables
private GameContainer container;
private StateBasedGame game;
private Input input;
- private int state;
+ private final int state;
public DownloadsMenu(int state) {
this.state = state;
@@ -175,17 +266,17 @@ public class DownloadsMenu extends BasicGameState {
int width = container.getWidth();
int height = container.getHeight();
float baseX = width * 0.024f;
- float searchY = (height * 0.04f) + Utils.FONT_LARGE.getLineHeight();
+ float searchY = (height * 0.04f) + Fonts.LARGE.getLineHeight();
float searchWidth = width * 0.3f;
// search
searchTimer = SEARCH_DELAY;
searchResultString = "Loading data from server...";
search = new TextField(
- container, Utils.FONT_DEFAULT, (int) baseX, (int) searchY,
- (int) searchWidth, Utils.FONT_MEDIUM.getLineHeight()
+ container, Fonts.DEFAULT, (int) baseX, (int) searchY,
+ (int) searchWidth, Fonts.MEDIUM.getLineHeight()
);
- search.setBackgroundColor(DownloadNode.BG_NORMAL);
+ search.setBackgroundColor(Colors.BLACK_BG_NORMAL);
search.setBorderColor(Color.white);
search.setTextColor(Color.white);
search.setConsumeEvents(false);
@@ -208,9 +299,8 @@ public class DownloadsMenu extends BasicGameState {
float buttonHeight = height * 0.038f;
float resetWidth = width * 0.085f;
float rankedWidth = width * 0.15f;
- float serverWidth = width * 0.12f;
float lowerWidth = width * 0.12f;
- float topButtonY = searchY + Utils.FONT_MEDIUM.getLineHeight() / 2f;
+ float topButtonY = searchY + Fonts.MEDIUM.getLineHeight() / 2f;
float lowerButtonY = height * 0.995f - searchY - buttonHeight / 2f;
Image button = GameImage.MENU_BUTTON_MID.getImage();
Image buttonL = GameImage.MENU_BUTTON_LEFT.getImage();
@@ -220,11 +310,9 @@ public class DownloadsMenu extends BasicGameState {
int lrButtonWidth = buttonL.getWidth() + buttonR.getWidth();
Image resetButtonImage = button.getScaledCopy((int) resetWidth - lrButtonWidth, (int) buttonHeight);
Image rankedButtonImage = button.getScaledCopy((int) rankedWidth - lrButtonWidth, (int) buttonHeight);
- Image serverButtonImage = button.getScaledCopy((int) serverWidth - lrButtonWidth, (int) buttonHeight);
Image lowerButtonImage = button.getScaledCopy((int) lowerWidth - lrButtonWidth, (int) buttonHeight);
float resetButtonWidth = resetButtonImage.getWidth() + lrButtonWidth;
float rankedButtonWidth = rankedButtonImage.getWidth() + lrButtonWidth;
- float serverButtonWidth = serverButtonImage.getWidth() + lrButtonWidth;
float lowerButtonWidth = lowerButtonImage.getWidth() + lrButtonWidth;
clearButton = new MenuButton(lowerButtonImage, buttonL, buttonR,
width * 0.75f + buttonMarginX + lowerButtonWidth / 2f, lowerButtonY);
@@ -234,16 +322,48 @@ public class DownloadsMenu extends BasicGameState {
baseX + searchWidth + buttonMarginX + resetButtonWidth / 2f, topButtonY);
rankedButton = new MenuButton(rankedButtonImage, buttonL, buttonR,
baseX + searchWidth + buttonMarginX * 2f + resetButtonWidth + rankedButtonWidth / 2f, topButtonY);
- serverButton = new MenuButton(serverButtonImage, buttonL, buttonR,
- baseX + searchWidth + buttonMarginX * 3f + resetButtonWidth + rankedButtonWidth + serverButtonWidth / 2f, topButtonY);
- clearButton.setText("Clear", Utils.FONT_MEDIUM, Color.white);
- importButton.setText("Import All", Utils.FONT_MEDIUM, Color.white);
- resetButton.setText("Reset", Utils.FONT_MEDIUM, Color.white);
+ clearButton.setText("Clear", Fonts.MEDIUM, Color.white);
+ importButton.setText("Import All", Fonts.MEDIUM, Color.white);
+ resetButton.setText("Reset", Fonts.MEDIUM, Color.white);
clearButton.setHoverFade();
importButton.setHoverFade();
resetButton.setHoverFade();
rankedButton.setHoverFade();
- serverButton.setHoverFade();
+
+ // dropdown menu
+ int serverWidth = (int) (width * 0.12f);
+ serverMenu = new DropdownMenu(container, SERVERS,
+ baseX + searchWidth + buttonMarginX * 3f + resetButtonWidth + rankedButtonWidth, searchY, serverWidth) {
+ @Override
+ public void itemSelected(int index, DownloadServer item) {
+ resultList = null;
+ startResultPos.setPosition(0);
+ focusResult = -1;
+ totalResults = 0;
+ page = 0;
+ pageResultTotal = 1;
+ pageDir = Page.RESET;
+ searchResultString = "Loading data from server...";
+ lastQuery = null;
+ pageDir = Page.RESET;
+ if (searchQuery != null)
+ searchQuery.interrupt();
+ resetSearchTimer();
+ }
+
+ @Override
+ public boolean menuClicked(int index) {
+ // block input during beatmap importing
+ if (importThread != null)
+ return false;
+
+ SoundController.playSound(SoundEffect.MENUCLICK);
+ return true;
+ }
+ };
+ serverMenu.setBackgroundColor(Colors.BLACK_BG_HOVER);
+ serverMenu.setBorderColor(Color.black);
+ serverMenu.setChevronRightColor(Color.white);
}
@Override
@@ -252,18 +372,19 @@ public class DownloadsMenu extends BasicGameState {
int width = container.getWidth();
int height = container.getHeight();
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
+ boolean inDropdownMenu = serverMenu.contains(mouseX, mouseY);
// background
GameImage.SEARCH_BG.getImage().draw();
// title
- Utils.FONT_LARGE.drawString(width * 0.024f, height * 0.03f, "Download Beatmaps!", Color.white);
+ Fonts.LARGE.drawString(width * 0.024f, height * 0.03f, "Download Beatmaps!", Color.white);
// search
g.setColor(Color.white);
g.setLineWidth(2f);
search.render(container, g);
- Utils.FONT_BOLD.drawString(
+ Fonts.BOLD.drawString(
search.getX() + search.getWidth() * 0.01f, search.getY() + search.getHeight() * 1.3f,
searchResultString, Color.white
);
@@ -283,7 +404,7 @@ public class DownloadsMenu extends BasicGameState {
if (index >= nodes.length)
break;
nodes[index].drawResult(g, offset + i * DownloadNode.getButtonOffset(),
- DownloadNode.resultContains(mouseX, mouseY - offset, i),
+ DownloadNode.resultContains(mouseX, mouseY - offset, i) && !inDropdownMenu,
(index == focusResult), (previewID == nodes[index].getID()));
}
g.clearClip();
@@ -297,9 +418,9 @@ public class DownloadsMenu extends BasicGameState {
float baseX = width * 0.024f;
float buttonY = height * 0.2f;
float buttonWidth = width * 0.7f;
- Utils.FONT_BOLD.drawString(
- baseX + (buttonWidth - Utils.FONT_BOLD.getWidth("Page 1")) / 2f,
- buttonY - Utils.FONT_BOLD.getLineHeight() * 1.3f,
+ Fonts.BOLD.drawString(
+ baseX + (buttonWidth - Fonts.BOLD.getWidth("Page 1")) / 2f,
+ buttonY - Fonts.BOLD.getLineHeight() * 1.3f,
String.format("Page %d", page), Color.white
);
if (page > 1)
@@ -311,10 +432,10 @@ public class DownloadsMenu extends BasicGameState {
// downloads
float downloadsX = width * 0.75f, downloadsY = search.getY();
- g.setColor(DownloadNode.BG_NORMAL);
+ g.setColor(Colors.BLACK_BG_NORMAL);
g.fillRect(downloadsX, downloadsY,
width * 0.25f, height - downloadsY * 2f);
- Utils.FONT_LARGE.drawString(downloadsX + width * 0.015f, downloadsY + height * 0.015f, "Downloads", Color.white);
+ Fonts.LARGE.drawString(downloadsX + width * 0.015f, downloadsY + height * 0.015f, "Downloads", Color.white);
int downloadsSize = DownloadList.get().size();
if (downloadsSize > 0) {
int maxDownloadsShown = DownloadNode.maxDownloadsShown();
@@ -344,15 +465,16 @@ public class DownloadsMenu extends BasicGameState {
clearButton.draw(Color.gray);
importButton.draw(Color.orange);
resetButton.draw(Color.red);
- rankedButton.setText((rankedOnly) ? "Show Unranked" : "Hide Unranked", Utils.FONT_MEDIUM, Color.white);
+ rankedButton.setText((rankedOnly) ? "Show Unranked" : "Hide Unranked", Fonts.MEDIUM, Color.white);
rankedButton.draw(Color.magenta);
- serverButton.setText(SERVERS[serverIndex].getName(), Utils.FONT_MEDIUM, Color.white);
- serverButton.draw(Color.blue);
+
+ // dropdown menu
+ serverMenu.render(container, g);
// importing beatmaps
if (importThread != null) {
// darken the screen
- g.setColor(Utils.COLOR_BLACK_ALPHA);
+ g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height);
UI.drawLoadingProgress(g);
@@ -379,7 +501,6 @@ public class DownloadsMenu extends BasicGameState {
importButton.hoverUpdate(delta, mouseX, mouseY);
resetButton.hoverUpdate(delta, mouseX, mouseY);
rankedButton.hoverUpdate(delta, mouseX, mouseY);
- serverButton.hoverUpdate(delta, mouseX, mouseY);
if (DownloadList.get() != null)
startDownloadIndexPos.setMinMax(0, DownloadNode.getInfoHeight() * (DownloadList.get().size() - DownloadNode.maxDownloadsShown()));
@@ -399,72 +520,22 @@ public class DownloadsMenu extends BasicGameState {
searchTimer = 0;
searchTimerReset = false;
- final String query = search.getText().trim().toLowerCase();
- final DownloadServer server = SERVERS[serverIndex];
+ String query = search.getText().trim().toLowerCase();
+ DownloadServer server = serverMenu.getSelectedItem();
if ((lastQuery == null || !query.equals(lastQuery)) &&
(query.length() == 0 || query.length() >= server.minQueryLength())) {
lastQuery = query;
lastQueryDir = pageDir;
- if (queryThread != null && queryThread.isAlive())
+ if (queryThread != null && queryThread.isAlive()) {
queryThread.interrupt();
+ if (searchQuery != null)
+ searchQuery.interrupt();
+ }
// execute query
- queryThread = new Thread() {
- @Override
- public void run() {
- activeRequests++;
-
- // check page direction
- Page lastPageDir = pageDir;
- pageDir = Page.RESET;
- int lastPageSize = (resultList != null) ? resultList.length : 0;
- int newPage = page;
- if (lastPageDir == Page.RESET)
- newPage = 1;
- else if (lastPageDir == Page.NEXT)
- newPage++;
- else if (lastPageDir == Page.PREVIOUS)
- newPage--;
- try {
- DownloadNode[] nodes = server.resultList(query, newPage, rankedOnly);
- if (activeRequests - 1 == 0) {
- // update page total
- page = newPage;
- if (nodes != null) {
- if (lastPageDir == Page.NEXT)
- pageResultTotal += nodes.length;
- else if (lastPageDir == Page.PREVIOUS)
- pageResultTotal -= lastPageSize;
- else if (lastPageDir == Page.RESET)
- pageResultTotal = nodes.length;
- } else
- pageResultTotal = 0;
-
- resultList = nodes;
- totalResults = server.totalResults();
- focusResult = -1;
- startResultPos.setPosition(0);
- if (nodes == null)
- searchResultString = "An error has occurred.";
- else {
- if (query.isEmpty())
- searchResultString = "Type to search!";
- else if (totalResults == 0 || resultList.length == 0)
- searchResultString = "No results found.";
- else
- searchResultString = String.format("%d result%s found!",
- totalResults, (totalResults == 1) ? "" : "s");
- }
- }
- } catch (IOException e) {
- searchResultString = "Could not establish connection to server.";
- } finally {
- activeRequests--;
- queryThread = null;
- }
- }
- };
+ searchQuery = new SearchQuery(query, server);
+ queryThread = new Thread(searchQuery);
queryThread.start();
}
}
@@ -474,7 +545,7 @@ public class DownloadsMenu extends BasicGameState {
UI.updateTooltip(delta, "Reset the current search.", false);
else if (rankedButton.contains(mouseX, mouseY))
UI.updateTooltip(delta, "Toggle the display of unranked maps.\nSome download servers may not support this option.", true);
- else if (serverButton.contains(mouseX, mouseY))
+ else if (serverMenu.baseContains(mouseX, mouseY))
UI.updateTooltip(delta, "Select a download server.", false);
}
@@ -534,7 +605,7 @@ public class DownloadsMenu extends BasicGameState {
} else {
// play preview
try {
- final URL url = new URL(SERVERS[serverIndex].getPreviewURL(node.getID()));
+ final URL url = new URL(serverMenu.getSelectedItem().getPreviewURL(node.getID()));
MusicController.pause();
new Thread() {
@Override
@@ -578,7 +649,7 @@ public class DownloadsMenu extends BasicGameState {
} else {
// start download
if (!DownloadList.get().contains(node.getID())) {
- node.createDownload(SERVERS[serverIndex]);
+ node.createDownload(serverMenu.getSelectedItem());
if (node.getDownload() == null)
UI.sendBarNotification("The download could not be started.");
else {
@@ -601,23 +672,27 @@ public class DownloadsMenu extends BasicGameState {
// pages
if (nodes.length > 0) {
if (page > 1 && prevPage.contains(x, y)) {
- if (lastQueryDir == Page.PREVIOUS && queryThread != null && queryThread.isAlive())
+ if (lastQueryDir == Page.PREVIOUS && searchQuery != null && !searchQuery.isComplete())
; // don't send consecutive requests
else {
SoundController.playSound(SoundEffect.MENUCLICK);
pageDir = Page.PREVIOUS;
lastQuery = null;
+ if (searchQuery != null)
+ searchQuery.interrupt();
resetSearchTimer();
}
return;
}
if (pageResultTotal < totalResults && nextPage.contains(x, y)) {
- if (lastQueryDir == Page.NEXT && queryThread != null && queryThread.isAlive())
+ if (lastQueryDir == Page.NEXT && searchQuery != null && !searchQuery.isComplete())
; // don't send consecutive requests
else {
SoundController.playSound(SoundEffect.MENUCLICK);
pageDir = Page.NEXT;
lastQuery = null;
+ if (searchQuery != null)
+ searchQuery.interrupt();
resetSearchTimer();
return;
}
@@ -670,6 +745,8 @@ public class DownloadsMenu extends BasicGameState {
search.setText("");
lastQuery = null;
pageDir = Page.RESET;
+ if (searchQuery != null)
+ searchQuery.interrupt();
resetSearchTimer();
return;
}
@@ -678,22 +755,8 @@ public class DownloadsMenu extends BasicGameState {
rankedOnly = !rankedOnly;
lastQuery = null;
pageDir = Page.RESET;
- resetSearchTimer();
- return;
- }
- if (serverButton.contains(x, y)) {
- SoundController.playSound(SoundEffect.MENUCLICK);
- resultList = null;
- startResultPos.setPosition(0);
- focusResult = -1;
- totalResults = 0;
- page = 0;
- pageResultTotal = 1;
- pageDir = Page.RESET;
- searchResultString = "Loading data from server...";
- serverIndex = (serverIndex + 1) % SERVERS.length;
- lastQuery = null;
- pageDir = Page.RESET;
+ if (searchQuery != null)
+ searchQuery.interrupt();
resetSearchTimer();
return;
}
@@ -806,6 +869,8 @@ public class DownloadsMenu extends BasicGameState {
SoundController.playSound(SoundEffect.MENUCLICK);
lastQuery = null;
pageDir = Page.CURRENT;
+ if (searchQuery != null)
+ searchQuery.interrupt();
resetSearchTimer();
break;
case Input.KEY_F7:
@@ -837,7 +902,8 @@ public class DownloadsMenu extends BasicGameState {
importButton.resetHover();
resetButton.resetHover();
rankedButton.resetHover();
- serverButton.resetHover();
+ serverMenu.activate();
+ serverMenu.reset();
focusResult = -1;
startResultPos.setPosition(0);
startDownloadIndexPos.setPosition(0);
@@ -853,6 +919,7 @@ public class DownloadsMenu extends BasicGameState {
public void leave(GameContainer container, StateBasedGame game)
throws SlickException {
search.setFocus(false);
+ serverMenu.deactivate();
SoundController.stopTrack();
MusicController.resume();
}
diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java
index 2c46797f..0bfa1d54 100644
--- a/src/itdelatrisu/opsu/states/Game.java
+++ b/src/itdelatrisu/opsu/states/Game.java
@@ -42,12 +42,16 @@ import itdelatrisu.opsu.objects.GameObject;
import itdelatrisu.opsu.objects.Slider;
import itdelatrisu.opsu.objects.Spinner;
import itdelatrisu.opsu.objects.curves.Curve;
+import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.render.FrameBufferCache;
import itdelatrisu.opsu.replay.PlaybackSpeed;
import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.replay.ReplayFrame;
+import itdelatrisu.opsu.ui.Colors;
+import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
+import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.io.File;
import java.util.LinkedList;
@@ -116,6 +120,15 @@ public class Game extends BasicGameState {
/** Hit object approach time, in milliseconds. */
private int approachTime;
+ /** The amount of time for hit objects to fade in, in milliseconds. */
+ private int fadeInTime;
+
+ /** Decay time for hit objects in the "Hidden" mod, in milliseconds. */
+ private int hiddenDecayTime;
+
+ /** Time before the hit object time by which the objects have completely faded in the "Hidden" mod, in milliseconds. */
+ private int hiddenTimeDiff;
+
/** Time offsets for obtaining each hit result (indexed by HIT_* constants). */
private int[] hitResultOffset;
@@ -146,7 +159,7 @@ public class Game extends BasicGameState {
countdown2Sound, countdownGoSound;
/** Mouse coordinates before game paused. */
- private int pausedMouseX = -1, pausedMouseY = -1;
+ private Vec2f pausedMousePosition;
/** Track position when game paused. */
private int pauseTime = -1;
@@ -209,7 +222,7 @@ public class Game extends BasicGameState {
private int flashlightRadius;
/** The cursor coordinates using the "auto" or "relax" mods. */
- private int autoMouseX = 0, autoMouseY = 0;
+ private Vec2f autoMousePosition;
/** Whether or not the cursor should be pressed using the "auto" mod. */
private boolean autoMousePressed;
@@ -220,11 +233,20 @@ 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;
private Input input;
- private int state;
+ private final int state;
public Game(int state) {
this.state = state;
@@ -245,6 +267,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);
}
@@ -278,7 +306,7 @@ public class Game extends BasicGameState {
else
dimLevel = 1f;
}
- if (Options.isDefaultPlayfieldForced() || !beatmap.drawBG(width, height, dimLevel, false)) {
+ if (Options.isDefaultPlayfieldForced() || !beatmap.drawBackground(width, height, dimLevel, false)) {
Image playfield = GameImage.PLAYFIELD.getImage();
playfield.setAlpha(dimLevel);
playfield.draw();
@@ -290,32 +318,31 @@ public class Game extends BasicGameState {
// "auto" and "autopilot" mods: move cursor automatically
// TODO: this should really be in update(), not render()
- autoMouseX = width / 2;
- autoMouseY = height / 2;
+ autoMousePosition.set(width / 2, height / 2);
autoMousePressed = false;
if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
- float[] autoXY = null;
+ Vec2f autoPoint = null;
if (isLeadIn()) {
// lead-in
float progress = Math.max((float) (leadInTime - beatmap.audioLeadIn) / approachTime, 0f);
- autoMouseY = (int) (height / (2f - progress));
+ autoMousePosition.y = height / (2f - progress);
} else if (objectIndex == 0 && trackPosition < firstObjectTime) {
// before first object
timeDiff = firstObjectTime - trackPosition;
if (timeDiff < approachTime) {
- float[] xy = gameObjects[0].getPointAt(trackPosition);
- autoXY = getPointAt(autoMouseX, autoMouseY, xy[0], xy[1], 1f - ((float) timeDiff / approachTime));
+ Vec2f point = gameObjects[0].getPointAt(trackPosition);
+ autoPoint = getPointAt(autoMousePosition.x, autoMousePosition.y, point.x, point.y, 1f - ((float) timeDiff / approachTime));
}
} else if (objectIndex < beatmap.objects.length) {
// normal object
int objectTime = beatmap.objects[objectIndex].getTime();
if (trackPosition < objectTime) {
- float[] xyStart = gameObjects[objectIndex - 1].getPointAt(trackPosition);
+ Vec2f startPoint = gameObjects[objectIndex - 1].getPointAt(trackPosition);
int startTime = gameObjects[objectIndex - 1].getEndTime();
if (beatmap.breaks != null && breakIndex < beatmap.breaks.size()) {
// starting a break: keep cursor at previous hit object position
if (breakTime > 0 || objectTime > beatmap.breaks.get(breakIndex))
- autoXY = xyStart;
+ autoPoint = startPoint;
// after a break ends: move startTime to break end time
else if (breakIndex > 1) {
@@ -324,10 +351,10 @@ public class Game extends BasicGameState {
startTime = lastBreakEndTime;
}
}
- if (autoXY == null) {
- float[] xyEnd = gameObjects[objectIndex].getPointAt(trackPosition);
+ if (autoPoint == null) {
+ Vec2f endPoint = gameObjects[objectIndex].getPointAt(trackPosition);
int totalTime = objectTime - startTime;
- autoXY = getPointAt(xyStart[0], xyStart[1], xyEnd[0], xyEnd[1], (float) (trackPosition - startTime) / totalTime);
+ autoPoint = getPointAt(startPoint.x, startPoint.y, endPoint.x, endPoint.y, (float) (trackPosition - startTime) / totalTime);
// hit circles: show a mouse press
int offset300 = hitResultOffset[GameData.HIT_300];
@@ -336,19 +363,17 @@ public class Game extends BasicGameState {
autoMousePressed = true;
}
} else {
- autoXY = gameObjects[objectIndex].getPointAt(trackPosition);
+ autoPoint = gameObjects[objectIndex].getPointAt(trackPosition);
autoMousePressed = true;
}
} else {
// last object
- autoXY = gameObjects[objectIndex - 1].getPointAt(trackPosition);
+ autoPoint = gameObjects[objectIndex - 1].getPointAt(trackPosition);
}
// set mouse coordinates
- if (autoXY != null) {
- autoMouseX = (int) autoXY[0];
- autoMouseY = (int) autoXY[1];
- }
+ if (autoPoint != null)
+ autoMousePosition.set(autoPoint.x, autoPoint.y);
}
// "flashlight" mod: restricted view of hit objects around cursor
@@ -366,12 +391,12 @@ public class Game extends BasicGameState {
g.setDrawMode(Graphics.MODE_ALPHA_MAP);
g.clearAlphaMap();
int mouseX, mouseY;
- if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) {
- mouseX = pausedMouseX;
- mouseY = pausedMouseY;
+ if (pauseTime > -1 && pausedMousePosition != null) {
+ mouseX = (int) pausedMousePosition.x;
+ mouseY = (int) pausedMousePosition.y;
} else if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
- mouseX = autoMouseX;
- mouseY = autoMouseY;
+ mouseX = (int) autoMousePosition.x;
+ mouseY = (int) autoMousePosition.y;
} else if (isReplay) {
mouseX = replayX;
mouseY = replayY;
@@ -455,15 +480,15 @@ public class Game extends BasicGameState {
GameImage.SCOREBAR_BG.getImage().getHeight(),
GameImage.SCOREBAR_KI.getImage().getHeight()
);
- float oldAlpha = Utils.COLOR_WHITE_FADE.a;
+ float oldAlpha = Colors.WHITE_FADE.a;
if (timeDiff < -500)
- Utils.COLOR_WHITE_FADE.a = (1000 + timeDiff) / 500f;
- Utils.FONT_MEDIUM.drawString(
+ Colors.WHITE_FADE.a = (1000 + timeDiff) / 500f;
+ Fonts.MEDIUM.drawString(
2 + (width / 100), retryHeight,
String.format("%d retries and counting...", retries),
- Utils.COLOR_WHITE_FADE
+ Colors.WHITE_FADE
);
- Utils.COLOR_WHITE_FADE.a = oldAlpha;
+ Colors.WHITE_FADE.a = oldAlpha;
}
if (isLeadIn())
@@ -525,28 +550,40 @@ 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) {
+ if (pauseTime > -1 && pausedMousePosition != null) {
// darken the screen
- g.setColor(Utils.COLOR_BLACK_ALPHA);
+ g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height);
// draw glowing hit select circle and pulse effect
- int circleRadius = GameImage.HITCIRCLE.getImage().getWidth();
- Image cursorCircle = GameImage.HITCIRCLE_SELECT.getImage().getScaledCopy(circleRadius, circleRadius);
+ int circleDiameter = GameImage.HITCIRCLE.getImage().getWidth();
+ Image cursorCircle = GameImage.HITCIRCLE_SELECT.getImage().getScaledCopy(circleDiameter, circleDiameter);
cursorCircle.setAlpha(1.0f);
- cursorCircle.drawCentered(pausedMouseX, pausedMouseY);
+ cursorCircle.drawCentered(pausedMousePosition.x, pausedMousePosition.y);
Image cursorCirclePulse = cursorCircle.getScaledCopy(1f + pausePulse);
cursorCirclePulse.setAlpha(1f - pausePulse);
- cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY);
+ cursorCirclePulse.drawCentered(pausedMousePosition.x, pausedMousePosition.y);
}
if (isReplay)
UI.draw(g, replayX, replayY, replayKeyPressed);
else if (GameMod.AUTO.isActive())
- UI.draw(g, autoMouseX, autoMouseY, autoMousePressed);
+ UI.draw(g, (int) autoMousePosition.x, (int) autoMousePosition.y, autoMousePressed);
else if (GameMod.AUTOPILOT.isActive())
- UI.draw(g, autoMouseX, autoMouseY, Utils.isGameKeyPressed());
+ UI.draw(g, (int) autoMousePosition.x, (int) autoMousePosition.y, Utils.isGameKeyPressed());
else
UI.draw(g);
}
@@ -564,8 +601,7 @@ public class Game extends BasicGameState {
// returning from pause screen: must click previous mouse position
if (pauseTime > -1) {
// paused during lead-in or break, or "relax" or "autopilot": continue immediately
- if ((pausedMouseX < 0 && pausedMouseY < 0) ||
- (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive())) {
+ if (pausedMousePosition == null || (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive())) {
pauseTime = -1;
if (!isLeadIn())
MusicController.resume();
@@ -603,6 +639,17 @@ public class Game extends BasicGameState {
return;
}
+ // "Easy" mod: multiple "lives"
+ if (GameMod.EASY.isActive() && deathTime > -1) {
+ if (data.getHealth() < 99f) {
+ data.changeHealth(delta / 10f);
+ data.updateDisplays(delta);
+ return;
+ }
+ MusicController.resume();
+ deathTime = -1;
+ }
+
// normal game update
if (!isReplay)
addReplayFrameAndRun(mouseX, mouseY, lastKeysPressed, trackPosition);
@@ -613,7 +660,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 +680,6 @@ public class Game extends BasicGameState {
timingPointIndex++;
}
}
- isSeeking = false;
}
// update and run replay frames
@@ -648,6 +694,12 @@ public class Game extends BasicGameState {
}
mouseX = replayX;
mouseY = replayY;
+
+ // unmute sounds
+ if (isSeeking) {
+ isSeeking = false;
+ SoundController.mute(false);
+ }
}
data.updateDisplays(delta);
@@ -662,16 +714,6 @@ public class Game extends BasicGameState {
* @param keys the keys that are pressed
*/
private void updateGame(int mouseX, int mouseY, int delta, int trackPosition, int keys) {
- // "Easy" mod: multiple "lives"
- if (GameMod.EASY.isActive() && deathTime > -1) {
- if (data.getHealth() < 99f)
- data.changeHealth(delta / 10f);
- else {
- MusicController.resume();
- deathTime = -1;
- }
- }
-
// map complete!
if (objectIndex >= gameObjects.length || (MusicController.trackEnded() && objectIndex > 0)) {
// track ended before last object was processed: force a hit result
@@ -699,6 +741,7 @@ public class Game extends BasicGameState {
r.save();
}
ScoreData score = data.getScoreData(beatmap);
+ data.setGameplay(!isReplay);
// add score to database
if (!unranked && !isReplay)
@@ -745,8 +788,7 @@ public class Game extends BasicGameState {
// pause game if focus lost
if (!container.hasFocus() && !GameMod.AUTO.isActive() && !isReplay) {
if (pauseTime < 0) {
- pausedMouseX = mouseX;
- pausedMouseY = mouseY;
+ pausedMousePosition = new Vec2f(mouseX, mouseY);
pausePulse = 0f;
}
if (MusicController.isPlaying() || isLeadIn())
@@ -819,8 +861,7 @@ public class Game extends BasicGameState {
// pause game
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= beatmap.objects[0].getTime()) {
- pausedMouseX = mouseX;
- pausedMouseY = mouseY;
+ pausedMousePosition = new Vec2f(mouseX, mouseY);
pausePulse = 0f;
}
if (MusicController.isPlaying() || isLeadIn())
@@ -888,6 +929,13 @@ public class Game extends BasicGameState {
}
}
break;
+ case Input.KEY_F:
+ // change playback speed
+ if (isReplay || GameMod.AUTO.isActive()) {
+ playbackSpeed = playbackSpeed.next();
+ MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
+ }
+ break;
case Input.KEY_UP:
UI.changeVolume(1);
break;
@@ -923,9 +971,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;
}
@@ -939,8 +988,7 @@ public class Game extends BasicGameState {
if (button == Input.MOUSE_MIDDLE_BUTTON && !Options.isMouseWheelDisabled()) {
int trackPosition = MusicController.getPosition();
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= beatmap.objects[0].getTime()) {
- pausedMouseX = x;
- pausedMouseY = y;
+ pausedMousePosition = new Vec2f(x, y);
pausePulse = 0f;
}
if (MusicController.isPlaying() || isLeadIn())
@@ -969,13 +1017,12 @@ public class Game extends BasicGameState {
private void gameKeyPressed(int keys, int x, int y, int trackPosition) {
// returning from pause screen
if (pauseTime > -1) {
- double distance = Math.hypot(pausedMouseX - x, pausedMouseY - y);
+ double distance = Math.hypot(pausedMousePosition.x - x, pausedMousePosition.y - y);
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2;
if (distance < circleRadius) {
// unpause the game
pauseTime = -1;
- pausedMouseX = -1;
- pausedMouseY = -1;
+ pausedMousePosition = null;
if (!isLeadIn())
MusicController.resume();
}
@@ -1065,6 +1112,15 @@ public class Game extends BasicGameState {
// restart the game
if (restart != Restart.FALSE) {
+ // load mods
+ if (isReplay) {
+ previousMods = GameMod.getModState();
+ GameMod.loadModState(replay.mods);
+ }
+
+ data.setGameplay(true);
+
+ // check restart state
if (restart == Restart.NEW) {
// new game
loadImages();
@@ -1149,10 +1205,6 @@ public class Game extends BasicGameState {
// load replay frames
if (isReplay) {
- // load mods
- previousMods = GameMod.getModState();
- GameMod.loadModState(replay.mods);
-
// load initial data
replayX = container.getWidth() / 2;
replayY = container.getHeight() / 2;
@@ -1188,6 +1240,8 @@ public class Game extends BasicGameState {
MusicController.setPosition(0);
MusicController.setPitch(GameMod.getSpeedMultiplier());
MusicController.pause();
+
+ SoundController.mute(false);
}
skipButton.resetHover();
@@ -1242,10 +1296,10 @@ public class Game extends BasicGameState {
final int followPointInterval = container.getHeight() / 14;
int lastObjectEndTime = gameObjects[lastObjectIndex].getEndTime() + 1;
int objectStartTime = beatmap.objects[index].getTime();
- float[] startXY = gameObjects[lastObjectIndex].getPointAt(lastObjectEndTime);
- float[] endXY = gameObjects[index].getPointAt(objectStartTime);
- float xDiff = endXY[0] - startXY[0];
- float yDiff = endXY[1] - startXY[1];
+ Vec2f startPoint = gameObjects[lastObjectIndex].getPointAt(lastObjectEndTime);
+ Vec2f endPoint = gameObjects[index].getPointAt(objectStartTime);
+ float xDiff = endPoint.x - startPoint.x;
+ float yDiff = endPoint.y - startPoint.y;
float dist = (float) Math.hypot(xDiff, yDiff);
int numPoints = (int) ((dist - GameImage.HITCIRCLE.getImage().getWidth()) / followPointInterval);
if (numPoints > 0) {
@@ -1266,8 +1320,8 @@ public class Game extends BasicGameState {
float step = 1f / (numPoints + 1);
float t = step;
for (int i = 0; i < numPoints; i++) {
- float x = startXY[0] + xDiff * t;
- float y = startXY[1] + yDiff * t;
+ float x = startPoint.x + xDiff * t;
+ float y = startPoint.y + yDiff * t;
float nextT = t + step;
if (lastObjectIndex < objectIndex) { // fade the previous trail
if (progress < nextT) {
@@ -1321,8 +1375,7 @@ public class Game extends BasicGameState {
timingPointIndex = 0;
beatLengthBase = beatLength = 1;
pauseTime = -1;
- pausedMouseX = -1;
- pausedMouseY = -1;
+ pausedMousePosition = null;
countdownReadySound = false;
countdown3Sound = false;
countdown1Sound = false;
@@ -1333,8 +1386,7 @@ public class Game extends BasicGameState {
deathTime = -1;
replayFrames = null;
lastReplayTime = 0;
- autoMouseX = 0;
- autoMouseY = 0;
+ autoMousePosition = new Vec2f();
autoMousePressed = false;
flashlightRadius = container.getHeight() * 2 / 3;
@@ -1376,9 +1428,9 @@ public class Game extends BasicGameState {
// set images
File parent = beatmap.getFile().getParentFile();
for (GameImage img : GameImage.values()) {
- if (img.isSkinnable()) {
+ if (img.isBeatmapSkinnable()) {
img.setDefaultImage();
- img.setSkinImage(parent);
+ img.setBeatmapSkinImage(parent);
}
}
@@ -1390,6 +1442,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...
@@ -1420,14 +1474,15 @@ public class Game extends BasicGameState {
// Stack modifier scales with hit object size
// StackOffset = HitObjectRadius / 10
- int diameter = (int) (104 - (circleSize * 8));
+ //int diameter = (int) (104 - (circleSize * 8));
+ float diameter = 108.848f - (circleSize * 8.9646f);
HitObject.setStackOffset(diameter * STACK_OFFSET_MODIFIER);
// initialize objects
- Circle.init(container, circleSize);
- Slider.init(container, circleSize, beatmap);
+ Circle.init(container, diameter);
+ Slider.init(container, diameter, beatmap);
Spinner.init(container, overallDifficulty);
- Curve.init(container.getWidth(), container.getHeight(), circleSize, (Options.isBeatmapSkinIgnored()) ?
+ Curve.init(container.getWidth(), container.getHeight(), diameter, (Options.isBeatmapSkinIgnored()) ?
Options.getSkin().getSliderBorderColor() : beatmap.getSliderBorderColor());
// approachRate (hit object approach time)
@@ -1438,9 +1493,9 @@ public class Game extends BasicGameState {
// overallDifficulty (hit result time offsets)
hitResultOffset = new int[GameData.HIT_MAX];
- hitResultOffset[GameData.HIT_300] = (int) (78 - (overallDifficulty * 6));
- hitResultOffset[GameData.HIT_100] = (int) (138 - (overallDifficulty * 8));
- hitResultOffset[GameData.HIT_50] = (int) (198 - (overallDifficulty * 10));
+ hitResultOffset[GameData.HIT_300] = (int) (79.5f - (overallDifficulty * 6));
+ hitResultOffset[GameData.HIT_100] = (int) (139.5f - (overallDifficulty * 8));
+ hitResultOffset[GameData.HIT_50] = (int) (199.5f - (overallDifficulty * 10));
hitResultOffset[GameData.HIT_MISS] = (int) (500 - (overallDifficulty * 10));
//final float mult = 0.608f;
//hitResultOffset[GameData.HIT_300] = (int) ((128 - (overallDifficulty * 9.6)) * mult);
@@ -1454,6 +1509,14 @@ public class Game extends BasicGameState {
// difficulty multiplier (scoring)
data.calculateDifficultyMultiplier(beatmap.HPDrainRate, beatmap.circleSize, beatmap.overallDifficulty);
+
+ // hit object fade-in time (TODO: formula)
+ fadeInTime = Math.min(375, (int) (approachTime / 2.5f));
+
+ // fade times ("Hidden" mod)
+ // TODO: find the actual formulas for this
+ hiddenDecayTime = (int) (approachTime / 3.6f);
+ hiddenTimeDiff = (int) (approachTime / 3.3f);
}
/**
@@ -1477,6 +1540,22 @@ public class Game extends BasicGameState {
*/
public int getApproachTime() { return approachTime; }
+ /**
+ * Returns the amount of time for hit objects to fade in, in milliseconds.
+ */
+ public int getFadeInTime() { return fadeInTime; }
+
+ /**
+ * Returns the object decay time in the "Hidden" mod, in milliseconds.
+ */
+ public int getHiddenDecayTime() { return hiddenDecayTime; }
+
+ /**
+ * Returns the time before the hit object time by which the objects have
+ * completely faded in the "Hidden" mod, in milliseconds.
+ */
+ public int getHiddenTimeDiff() { return hiddenTimeDiff; }
+
/**
* Returns an array of hit result offset times, in milliseconds (indexed by GameData.HIT_* constants).
*/
@@ -1536,8 +1615,8 @@ public class Game extends BasicGameState {
public synchronized void addReplayFrameAndRun(int x, int y, int keys, int time){
// "auto" and "autopilot" mods: use automatic cursor coordinates
if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
- x = autoMouseX;
- y = autoMouseY;
+ x = (int) autoMousePosition.x;
+ y = (int) autoMousePosition.y;
}
ReplayFrame frame = addReplayFrame(x, y, keys, time);
@@ -1611,17 +1690,13 @@ public class Game extends BasicGameState {
* @param endX the ending x coordinate
* @param endY the ending y coordinate
* @param t the t value [0, 1]
- * @return the [x,y] coordinates
+ * @return the position vector
*/
- private float[] getPointAt(float startX, float startY, float endX, float endY, float t) {
+ private Vec2f getPointAt(float startX, float startY, float endX, float endY, float t) {
// "autopilot" mod: move quicker between objects
if (GameMod.AUTOPILOT.isActive())
t = Utils.clamp(t * 2f, 0f, 1f);
-
- float[] xy = new float[2];
- xy[0] = startX + (endX - startX) * t;
- xy[1] = startY + (endY - startY) * t;
- return xy;
+ return new Vec2f(startX + (endX - startX) * t, startY + (endY - startY) * t);
}
/**
@@ -1715,9 +1790,9 @@ public class Game extends BasicGameState {
// possible special case: if slider end in the stack,
// all next hit objects in stack move right down
if (hitObjectN.isSlider()) {
- float[] p1 = gameObjects[i].getPointAt(hitObjectI.getTime());
- float[] p2 = gameObjects[n].getPointAt(gameObjects[n].getEndTime());
- float distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]);
+ Vec2f p1 = gameObjects[i].getPointAt(hitObjectI.getTime());
+ Vec2f p2 = gameObjects[n].getPointAt(gameObjects[n].getEndTime());
+ float distance = Utils.distance(p1.x, p1.y, p2.x, p2.y);
// check if hit object part of this stack
if (distance < STACK_LENIENCE * HitObject.getXMultiplier()) {
@@ -1725,7 +1800,7 @@ public class Game extends BasicGameState {
for (int j = n + 1; j <= i; j++) {
HitObject hitObjectJ = beatmap.objects[j];
p1 = gameObjects[j].getPointAt(hitObjectJ.getTime());
- distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]);
+ distance = Utils.distance(p1.x, p1.y, p2.x, p2.y);
// hit object below slider end
if (distance < STACK_LENIENCE * HitObject.getXMultiplier())
@@ -1753,4 +1828,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/GamePauseMenu.java b/src/itdelatrisu/opsu/states/GamePauseMenu.java
index e73e5a2a..35f943f8 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;
@@ -61,7 +62,7 @@ public class GamePauseMenu extends BasicGameState {
private GameContainer container;
private StateBasedGame game;
private Input input;
- private int state;
+ private final int state;
private Game gameState;
public GamePauseMenu(int state) {
@@ -86,10 +87,10 @@ public class GamePauseMenu extends BasicGameState {
// don't draw default background if button skinned and background unskinned
boolean buttonsSkinned =
- GameImage.PAUSE_CONTINUE.hasSkinImage() ||
- GameImage.PAUSE_RETRY.hasSkinImage() ||
- GameImage.PAUSE_BACK.hasSkinImage();
- if (!buttonsSkinned || bg.hasSkinImage())
+ GameImage.PAUSE_CONTINUE.hasBeatmapSkinImage() ||
+ GameImage.PAUSE_RETRY.hasBeatmapSkinImage() ||
+ GameImage.PAUSE_BACK.hasBeatmapSkinImage();
+ if (!buttonsSkinned || bg.hasBeatmapSkinImage())
bg.getImage().draw();
else
g.setBackground(Color.black);
@@ -133,7 +134,7 @@ public class GamePauseMenu extends BasicGameState {
SoundController.playSound(SoundEffect.MENUBACK);
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).resetGameDataOnLoad();
MusicController.playAt(MusicController.getBeatmap().previewTime, true);
- if (UI.getCursor().isSkinned())
+ if (UI.getCursor().isBeatmapSkinned())
UI.getCursor().reset();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
} else {
@@ -187,7 +188,7 @@ public class GamePauseMenu extends BasicGameState {
MusicController.playAt(MusicController.getBeatmap().previewTime, true);
else
MusicController.resume();
- if (UI.getCursor().isSkinned())
+ if (UI.getCursor().isBeatmapSkinned())
UI.getCursor().reset();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
}
@@ -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/GameRanking.java b/src/itdelatrisu/opsu/states/GameRanking.java
index 4fcc18ec..925f7f48 100644
--- a/src/itdelatrisu/opsu/states/GameRanking.java
+++ b/src/itdelatrisu/opsu/states/GameRanking.java
@@ -20,6 +20,7 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameData;
import itdelatrisu.opsu.GameImage;
+import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
@@ -68,7 +69,7 @@ public class GameRanking extends BasicGameState {
// game-related variables
private GameContainer container;
private StateBasedGame game;
- private int state;
+ private final int state;
private Input input;
public GameRanking(int state) {
@@ -105,7 +106,7 @@ public class GameRanking extends BasicGameState {
Beatmap beatmap = MusicController.getBeatmap();
// background
- if (!beatmap.drawBG(width, height, 0.7f, true))
+ if (!beatmap.drawBackground(width, height, 0.7f, true))
GameImage.PLAYFIELD.getImage().draw(0,0);
// ranking screen elements
@@ -113,7 +114,7 @@ public class GameRanking extends BasicGameState {
// buttons
replayButton.draw();
- if (data.isGameplay())
+ if (data.isGameplay() && !GameMod.AUTO.isActive())
retryButton.draw();
UI.getBackButton().draw();
@@ -175,7 +176,8 @@ public class GameRanking extends BasicGameState {
// replay
Game gameState = (Game) game.getState(Opsu.STATE_GAME);
boolean returnToGame = false;
- if (replayButton.contains(x, y)) {
+ boolean replayButtonPressed = replayButton.contains(x, y);
+ if (replayButtonPressed && !(data.isGameplay() && GameMod.AUTO.isActive())) {
Replay r = data.getReplay(null, null);
if (r != null) {
try {
@@ -194,7 +196,9 @@ public class GameRanking extends BasicGameState {
}
// retry
- else if (data.isGameplay() && retryButton.contains(x, y)) {
+ else if (data.isGameplay() &&
+ (!GameMod.AUTO.isActive() && retryButton.contains(x, y)) ||
+ (GameMod.AUTO.isActive() && replayButtonPressed)) {
gameState.setReplay(null);
gameState.setRestart(Game.Restart.MANUAL);
returnToGame = true;
@@ -221,7 +225,7 @@ public class GameRanking extends BasicGameState {
} else {
SoundController.playSound(SoundEffect.APPLAUSE);
retryButton.resetHover();
- replayButton.setY(replayY);
+ replayButton.setY(!GameMod.AUTO.isActive() ? replayY : retryY);
}
replayButton.resetHover();
}
@@ -239,12 +243,11 @@ public class GameRanking extends BasicGameState {
*/
private void returnToSongMenu() {
SoundController.playSound(SoundEffect.MENUBACK);
- if (data.isGameplay()) {
- SongMenu songMenu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
- songMenu.resetGameDataOnLoad();
+ SongMenu songMenu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
+ if (data.isGameplay())
songMenu.resetTrackOnLoad();
- }
- if (UI.getCursor().isSkinned())
+ songMenu.resetGameDataOnLoad();
+ if (UI.getCursor().isBeatmapSkinned())
UI.getCursor().reset();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
}
diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java
index be484887..f0901df8 100644
--- a/src/itdelatrisu/opsu/states/MainMenu.java
+++ b/src/itdelatrisu/opsu/states/MainMenu.java
@@ -31,9 +31,13 @@ import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
+import itdelatrisu.opsu.ui.Colors;
+import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.MenuButton.Expand;
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;
@@ -61,7 +65,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;
@@ -69,12 +73,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;
@@ -87,8 +100,8 @@ public class MainMenu extends BasicGameState {
/** Button linking to repository. */
private MenuButton repoButton;
- /** Button for installing updates. */
- private MenuButton updateButton;
+ /** Buttons for installing updates. */
+ private MenuButton updateButton, restartButton;
/** Application start time, for drawing the total running time. */
private long programStartTime;
@@ -97,7 +110,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;
@@ -105,16 +118,11 @@ public class MainMenu extends BasicGameState {
/** Music position bar coordinates and dimensions. */
private float musicBarX, musicBarY, musicBarWidth, musicBarHeight;
- /** Music position bar background colors. */
- private static final Color
- BG_NORMAL = new Color(0, 0, 0, 0.25f),
- BG_HOVER = new Color(0, 0, 0, 0.5f);
-
// game-related variables
private GameContainer container;
private StateBasedGame game;
private Input input;
- private int state;
+ private final int state;
public MainMenu(int state) {
this.state = state;
@@ -145,9 +153,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.08f;
+ logo.setHoverExpand(logoHoverScale);
+ playButton.setHoverExpand(logoHoverScale);
+ exitButton.setHoverExpand(logoHoverScale);
// initialize music buttons
int musicWidth = GameImage.MUSIC_PLAY.getImage().getWidth();
@@ -170,24 +187,40 @@ 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
float startX = width * 0.997f, startY = height * 0.997f;
- if (Desktop.isDesktopSupported()) { // only if a webpage can be opened
+ if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { // only if a webpage can be opened
Image repoImg = GameImage.REPOSITORY.getImage();
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
- startX -= width * 0.005f;
+ }
- // initialize update button
- Image bangImg = GameImage.BANG.getImage();
- updateButton = new MenuButton(bangImg, startX - bangImg.getWidth(), startY - bangImg.getHeight());
- updateButton.setHoverExpand(1.15f);
+ // initialize update buttons
+ float updateX = width / 2f, updateY = height * 17 / 18f;
+ Image downloadImg = GameImage.DOWNLOAD.getImage();
+ updateButton = new MenuButton(downloadImg, updateX, updateY);
+ updateButton.setHoverAnimationDuration(400);
+ updateButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_QUAD);
+ updateButton.setHoverExpand(1.1f);
+ Image updateImg = GameImage.UPDATE.getImage();
+ restartButton = new MenuButton(updateImg, updateX, updateY);
+ restartButton.setHoverAnimationDuration(2000);
+ restartButton.setHoverAnimationEquation(AnimationEquation.LINEAR);
+ restartButton.setHoverRotate(360);
+
+ // logo animations
+ float centerOffsetX = width / 5f;
+ logoOpen = new AnimatedValue(400, 0, centerOffsetX, AnimationEquation.OUT_QUAD);
+ logoClose = new AnimatedValue(2200, centerOffsetX, 0, AnimationEquation.OUT_QUAD);
+ logoButtonAlpha = new AnimatedValue(200, 0f, 1f, AnimationEquation.LINEAR);
reset();
}
@@ -201,27 +234,27 @@ 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.drawBackground(width, height, bgAlpha.getValue(), true))
;
else {
Image bg = GameImage.MENU_BG.getImage();
- bg.setAlpha(bgAlpha);
+ bg.setAlpha(bgAlpha.getValue());
bg.draw();
}
// top/bottom horizontal bars
- float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
- Utils.COLOR_BLACK_ALPHA.a = 0.2f;
- g.setColor(Utils.COLOR_BLACK_ALPHA);
+ float oldAlpha = Colors.BLACK_ALPHA.a;
+ Colors.BLACK_ALPHA.a = 0.2f;
+ g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height / 9f);
g.fillRect(0, height * 8 / 9f, width, height / 9f);
- Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
+ Colors.BLACK_ALPHA.a = oldAlpha;
// draw downloads button
downloadsButton.draw();
// draw buttons
- if (logoTimer > 0) {
+ if (logoState == LogoState.OPEN || logoState == LogoState.CLOSING) {
playButton.draw();
exitButton.draw();
}
@@ -237,7 +270,7 @@ public class MainMenu extends BasicGameState {
// draw music position bar
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
- g.setColor((musicPositionBarContains(mouseX, mouseY)) ? BG_HOVER : BG_NORMAL);
+ g.setColor((musicPositionBarContains(mouseX, mouseY)) ? Colors.BLACK_BG_HOVER : Colors.BLACK_BG_NORMAL);
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight, 4);
g.setColor(Color.white);
if (!MusicController.isTrackLoading() && beatmap != null) {
@@ -251,35 +284,26 @@ public class MainMenu extends BasicGameState {
// draw update button
if (Updater.get().showButton()) {
- Color updateColor = null;
- switch (Updater.get().getStatus()) {
- case UPDATE_AVAILABLE:
- updateColor = Color.red;
- break;
- case UPDATE_DOWNLOADED:
- updateColor = Color.green;
- break;
- case UPDATE_DOWNLOADING:
- updateColor = Color.yellow;
- break;
- default:
- updateColor = Color.white;
- break;
- }
- updateButton.draw(updateColor);
+ Updater.Status status = Updater.get().getStatus();
+ if (status == Updater.Status.UPDATE_AVAILABLE || status == Updater.Status.UPDATE_DOWNLOADING)
+ updateButton.draw();
+ else if (status == Updater.Status.UPDATE_DOWNLOADED)
+ restartButton.draw();
}
// draw text
float marginX = width * 0.015f, topMarginY = height * 0.01f, bottomMarginY = height * 0.015f;
- g.setFont(Utils.FONT_MEDIUM);
- float lineHeight = Utils.FONT_MEDIUM.getLineHeight() * 0.925f;
+ g.setFont(Fonts.MEDIUM);
+ float lineHeight = Fonts.MEDIUM.getLineHeight() * 0.925f;
g.drawString(String.format("Loaded %d songs and %d beatmaps.",
BeatmapSetList.get().getMapSetCount(), BeatmapSetList.get().getMapCount()), marginX, topMarginY);
if (MusicController.isTrackLoading())
g.drawString("Track loading...", marginX, topMarginY + lineHeight);
else if (MusicController.trackExists()) {
- if (Options.useUnicodeMetadata()) // load glyphs
- Utils.loadGlyphs(Utils.FONT_MEDIUM, beatmap.titleUnicode, beatmap.artistUnicode);
+ if (Options.useUnicodeMetadata()) { // load glyphs
+ Fonts.loadGlyphs(Fonts.MEDIUM, beatmap.titleUnicode);
+ Fonts.loadGlyphs(Fonts.MEDIUM, beatmap.artistUnicode);
+ }
g.drawString((MusicController.isPlaying()) ? "Now Playing:" : "Paused:", marginX, topMarginY + lineHeight);
g.drawString(String.format("%s: %s", beatmap.getArtist(), beatmap.getTitle()), marginX + 25, topMarginY + (lineHeight * 2));
}
@@ -305,7 +329,10 @@ public class MainMenu extends BasicGameState {
exitButton.hoverUpdate(delta, mouseX, mouseY, 0.25f);
if (repoButton != null)
repoButton.hoverUpdate(delta, mouseX, mouseY);
- updateButton.hoverUpdate(delta, mouseX, mouseY);
+ if (Updater.get().showButton()) {
+ updateButton.autoHoverUpdate(delta, true);
+ restartButton.autoHoverUpdate(delta, false);
+ }
downloadsButton.hoverUpdate(delta, mouseX, mouseY);
// ensure only one button is in hover state at once
boolean noHoverUpdate = musicPositionBarContains(mouseX, mouseY);
@@ -322,46 +349,46 @@ 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;
- }
+ Beatmap beatmap = MusicController.getBeatmap();
+ if (!(Options.isDynamicBackgroundEnabled() && beatmap != null && beatmap.isBackgroundLoading()))
+ 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
@@ -373,8 +400,12 @@ public class MainMenu extends BasicGameState {
UI.updateTooltip(delta, "Next track", false);
else if (musicPrevious.contains(mouseX, mouseY))
UI.updateTooltip(delta, "Previous track", false);
- else if (Updater.get().showButton() && updateButton.contains(mouseX, mouseY))
- UI.updateTooltip(delta, Updater.get().getStatus().getDescription(), true);
+ else if (Updater.get().showButton()) {
+ Updater.Status status = Updater.get().getStatus();
+ if (((status == Updater.Status.UPDATE_AVAILABLE || status == Updater.Status.UPDATE_DOWNLOADING) && updateButton.contains(mouseX, mouseY)) ||
+ (status == Updater.Status.UPDATE_DOWNLOADED && restartButton.contains(mouseX, mouseY)))
+ UI.updateTooltip(delta, status.getDescription(), true);
+ }
}
@Override
@@ -412,8 +443,8 @@ public class MainMenu extends BasicGameState {
musicPrevious.resetHover();
if (repoButton != null && !repoButton.contains(mouseX, mouseY))
repoButton.resetHover();
- if (!updateButton.contains(mouseX, mouseY))
- updateButton.resetHover();
+ updateButton.resetHover();
+ restartButton.resetHover();
if (!downloadsButton.contains(mouseX, mouseY))
downloadsButton.resetHover();
}
@@ -449,71 +480,85 @@ public class MainMenu extends BasicGameState {
MusicController.resume();
UI.sendBarNotification("Play");
}
+ return;
} else if (musicNext.contains(x, y)) {
nextTrack();
UI.sendBarNotification(">> Next");
+ return;
} else if (musicPrevious.contains(x, y)) {
if (!previous.isEmpty()) {
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");
+ return;
}
// downloads button actions
- else if (downloadsButton.contains(x, y)) {
+ if (downloadsButton.contains(x, y)) {
SoundController.playSound(SoundEffect.MENUHIT);
game.enterState(Opsu.STATE_DOWNLOADSMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
+ return;
}
// repository button actions
- else if (repoButton != null && repoButton.contains(x, y)) {
+ if (repoButton != null && repoButton.contains(x, y)) {
try {
Desktop.getDesktop().browse(Options.REPOSITORY_URI);
+ } catch (UnsupportedOperationException e) {
+ UI.sendBarNotification("The repository web page could not be opened.");
} catch (IOException e) {
ErrorHandler.error("Could not browse to repository URI.", e, false);
}
+ return;
}
// update button actions
- else if (Updater.get().showButton() && updateButton.contains(x, y)) {
- switch (Updater.get().getStatus()) {
- case UPDATE_AVAILABLE:
+ if (Updater.get().showButton()) {
+ Updater.Status status = Updater.get().getStatus();
+ if (updateButton.contains(x, y) && status == Updater.Status.UPDATE_AVAILABLE) {
SoundController.playSound(SoundEffect.MENUHIT);
Updater.get().startDownload();
- break;
- case UPDATE_DOWNLOADED:
+ updateButton.removeHoverEffects();
+ updateButton.setHoverAnimationDuration(800);
+ updateButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_QUAD);
+ updateButton.setHoverFade(0.6f);
+ return;
+ } else if (restartButton.contains(x, y) && status == Updater.Status.UPDATE_DOWNLOADED) {
SoundController.playSound(SoundEffect.MENUHIT);
Updater.get().prepareUpdate();
container.setForceExit(false);
container.exit();
- break;
- default:
- break;
+ return;
}
}
// start moving logo (if clicked)
- else if (!logoClicked) {
+ 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);
SoundController.playSound(SoundEffect.MENUHIT);
+ return;
}
}
// 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();
- } else if (exitButton.contains(x, y, 0.25f))
+ return;
+ } else if (exitButton.contains(x, y, 0.25f)) {
container.exit();
+ return;
+ }
}
}
@@ -532,8 +577,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);
@@ -581,8 +627,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();
@@ -594,6 +643,7 @@ public class MainMenu extends BasicGameState {
if (repoButton != null)
repoButton.resetHover();
updateButton.resetHover();
+ restartButton.resetHover();
downloadsButton.resetHover();
}
@@ -611,7 +661,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/OptionsMenu.java b/src/itdelatrisu/opsu/states/OptionsMenu.java
index 6f303701..6cdee288 100644
--- a/src/itdelatrisu/opsu/states/OptionsMenu.java
+++ b/src/itdelatrisu/opsu/states/OptionsMenu.java
@@ -26,6 +26,8 @@ import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
+import itdelatrisu.opsu.ui.Colors;
+import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
@@ -86,14 +88,18 @@ 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,
GameOption.FIXED_HP,
GameOption.FIXED_AR,
GameOption.FIXED_OD,
- GameOption.CHECKPOINT
+ GameOption.CHECKPOINT,
+ GameOption.REPLAY_SEEKING,
+ GameOption.DISABLE_UPDATER,
+ GameOption.ENABLE_WATCH_SERVICE
});
/** Total number of tabs. */
@@ -110,10 +116,10 @@ public class OptionsMenu extends BasicGameState {
private static OptionTab[] values = values();
/** Tab name. */
- private String name;
+ private final String name;
/** Options array. */
- public GameOption[] options;
+ public final GameOption[] options;
/** Associated tab button. */
public MenuButton button;
@@ -163,7 +169,7 @@ public class OptionsMenu extends BasicGameState {
private StateBasedGame game;
private Input input;
private Graphics g;
- private int state;
+ private final int state;
public OptionsMenu(int state) {
this.state = state;
@@ -182,8 +188,8 @@ public class OptionsMenu extends BasicGameState {
// option tabs
Image tabImage = GameImage.MENU_TAB.getImage();
- float tabX = width * 0.032f + Utils.FONT_DEFAULT.getWidth("Change the way opsu! behaves") + (tabImage.getWidth() / 2);
- float tabY = Utils.FONT_XLARGE.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() +
+ float tabX = width * 0.032f + Fonts.DEFAULT.getWidth("Change the way opsu! behaves") + (tabImage.getWidth() / 2);
+ float tabY = Fonts.XLARGE.getLineHeight() + Fonts.DEFAULT.getLineHeight() +
height * 0.015f - (tabImage.getHeight() / 2f);
int tabOffset = Math.min(tabImage.getWidth(), width / OptionTab.SIZE);
for (OptionTab tab : OptionTab.values())
@@ -198,22 +204,19 @@ public class OptionsMenu extends BasicGameState {
@Override
public void render(GameContainer container, StateBasedGame game, Graphics g)
throws SlickException {
- g.setBackground(Utils.COLOR_BLACK_ALPHA);
-
int width = container.getWidth();
int height = container.getHeight();
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
- float lineY = OptionTab.DISPLAY.button.getY() + (GameImage.MENU_TAB.getImage().getHeight() / 2f);
+
+ // background
+ GameImage.OPTIONS_BG.getImage().draw();
// title
float marginX = width * 0.015f, marginY = height * 0.01f;
- Utils.FONT_XLARGE.drawString(marginX, marginY, "Options", Color.white);
- Utils.FONT_DEFAULT.drawString(marginX, marginY + Utils.FONT_XLARGE.getLineHeight() * 0.92f,
+ Fonts.XLARGE.drawString(marginX, marginY, "Options", Color.white);
+ Fonts.DEFAULT.drawString(marginX, marginY + Fonts.XLARGE.getLineHeight() * 0.92f,
"Change the way opsu! behaves", Color.white);
- // background
- GameImage.OPTIONS_BG.getImage().draw(0, lineY);
-
// game options
g.setLineWidth(1f);
GameOption hoverOption = (keyEntryLeft) ? GameOption.KEY_LEFT :
@@ -241,6 +244,7 @@ public class OptionsMenu extends BasicGameState {
currentTab.getName(), true, false);
g.setColor(Color.white);
g.setLineWidth(2f);
+ float lineY = OptionTab.DISPLAY.button.getY() + (GameImage.MENU_TAB.getImage().getHeight() / 2f);
g.drawLine(0, lineY, width, lineY);
g.resetLineWidth();
@@ -248,15 +252,15 @@ public class OptionsMenu extends BasicGameState {
// key entry state
if (keyEntryLeft || keyEntryRight) {
- g.setColor(Utils.COLOR_BLACK_ALPHA);
+ g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height);
g.setColor(Color.white);
String prompt = (keyEntryLeft) ?
"Please press the new left-click key." :
"Please press the new right-click key.";
- Utils.FONT_LARGE.drawString(
- (width / 2) - (Utils.FONT_LARGE.getWidth(prompt) / 2),
- (height / 2) - Utils.FONT_LARGE.getLineHeight(), prompt
+ Fonts.LARGE.drawString(
+ (width / 2) - (Fonts.LARGE.getWidth(prompt) / 2),
+ (height / 2) - Fonts.LARGE.getLineHeight(), prompt
);
}
@@ -413,14 +417,14 @@ public class OptionsMenu extends BasicGameState {
*/
private void drawOption(GameOption option, int pos, boolean focus) {
int width = container.getWidth();
- int textHeight = Utils.FONT_LARGE.getLineHeight();
+ int textHeight = Fonts.LARGE.getLineHeight();
float y = textY + (pos * offsetY);
Color color = (focus) ? Color.cyan : Color.white;
- Utils.FONT_LARGE.drawString(width / 30, y, option.getName(), color);
- Utils.FONT_LARGE.drawString(width / 2, y, option.getValueString(), color);
- Utils.FONT_SMALL.drawString(width / 30, y + textHeight, option.getDescription(), color);
- g.setColor(Utils.COLOR_WHITE_ALPHA);
+ Fonts.LARGE.drawString(width / 30, y, option.getName(), color);
+ Fonts.LARGE.drawString(width / 2, y, option.getValueString(), color);
+ Fonts.SMALL.drawString(width / 30, y + textHeight, option.getDescription(), color);
+ g.setColor(Colors.WHITE_ALPHA);
g.drawLine(0, y + textHeight, width, y + textHeight);
}
@@ -433,7 +437,7 @@ public class OptionsMenu extends BasicGameState {
if (y < textY || y > textY + (offsetY * maxOptionsScreen))
return null;
- int index = (y - textY + Utils.FONT_LARGE.getLineHeight()) / offsetY;
+ int index = (y - textY + Fonts.LARGE.getLineHeight()) / offsetY;
if (index >= currentTab.options.length)
return null;
diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java
index 2db90587..c26ff637 100644
--- a/src/itdelatrisu/opsu/states/SongMenu.java
+++ b/src/itdelatrisu/opsu/states/SongMenu.java
@@ -24,7 +24,6 @@ import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.ScoreData;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MultiClip;
@@ -32,18 +31,32 @@ import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.Beatmap;
+import itdelatrisu.opsu.beatmap.BeatmapDifficultyCalculator;
import itdelatrisu.opsu.beatmap.BeatmapParser;
+import itdelatrisu.opsu.beatmap.BeatmapSet;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.beatmap.BeatmapSortOrder;
+import itdelatrisu.opsu.beatmap.BeatmapWatchService;
+import itdelatrisu.opsu.beatmap.BeatmapWatchService.BeatmapWatchServiceListener;
+import itdelatrisu.opsu.beatmap.LRUCache;
+import itdelatrisu.opsu.beatmap.OszUnpacker;
import itdelatrisu.opsu.db.BeatmapDB;
import itdelatrisu.opsu.db.ScoreDB;
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
import itdelatrisu.opsu.ui.KinecticScrolling;
+import itdelatrisu.opsu.ui.Colors;
+import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton;
+import itdelatrisu.opsu.ui.StarStream;
import itdelatrisu.opsu.ui.UI;
+import itdelatrisu.opsu.ui.animations.AnimatedValue;
+import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent.Kind;
import java.util.Map;
import java.util.Stack;
@@ -145,8 +158,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 BeatmapSetNode hoverIndex = null;
@@ -209,11 +222,52 @@ public class SongMenu extends BasicGameState {
/** The text length of the last string in the search TextField. */
private int lastSearchTextLength = -1;
+ /** Whether the song folder changed (notified via the watch service). */
+ private boolean songFolderChanged = false;
+
+ /** The last background image. */
+ private File lastBackgroundImage;
+
+ /** Background alpha level (for fade-in effect). */
+ private AnimatedValue bgAlpha = new AnimatedValue(800, 0f, 1f, AnimationEquation.OUT_QUAD);
+
+ /** Timer for animations when a new song node is selected. */
+ private AnimatedValue songChangeTimer = new AnimatedValue(900, 0f, 1f, AnimationEquation.LINEAR);
+
+ /** Timer for the music icon animation when a new song node is selected. */
+ private AnimatedValue musicIconBounceTimer = new AnimatedValue(350, 0f, 1f, AnimationEquation.LINEAR);
+
+ /**
+ * Beatmaps whose difficulties were recently computed (if flag is non-null).
+ * Unless the Boolean flag is null, then upon removal, the beatmap's objects will
+ * be cleared (to be garbage collected). If the flag is true, also clear the
+ * beatmap's array fields (timing points, etc.).
+ */
+ @SuppressWarnings("serial")
+ private LRUCache beatmapsCalculated = new LRUCache(12) {
+ @Override
+ public void eldestRemoved(Map.Entry eldest) {
+ Boolean b = eldest.getValue();
+ if (b != null) {
+ Beatmap beatmap = eldest.getKey();
+ beatmap.objects = null;
+ if (b) {
+ beatmap.timingPoints = null;
+ beatmap.breaks = null;
+ beatmap.combo = null;
+ }
+ }
+ }
+ };
+
+ /** The star stream. */
+ private StarStream starStream;
+
// game-related variables
private GameContainer container;
private StateBasedGame game;
private Input input;
- private int state;
+ private final int state;
public SongMenu(int state) {
this.state = state;
@@ -231,8 +285,8 @@ public class SongMenu extends BasicGameState {
// header/footer coordinates
headerY = height * 0.0075f + GameImage.MENU_MUSICNOTE.getImage().getHeight() +
- Utils.FONT_BOLD.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() +
- Utils.FONT_SMALL.getLineHeight();
+ Fonts.BOLD.getLineHeight() + Fonts.DEFAULT.getLineHeight() +
+ Fonts.SMALL.getLineHeight();
footerY = height - GameImage.SELECTION_MODS.getImage().getHeight();
// initialize sorts
@@ -253,11 +307,11 @@ public class SongMenu extends BasicGameState {
buttonOffset = (footerY - headerY - DIVIDER_LINE_WIDTH) / MAX_SONG_BUTTONS;
// search
- int textFieldX = (int) (width * 0.7125f + Utils.FONT_BOLD.getWidth("Search: "));
- int textFieldY = (int) (headerY + Utils.FONT_BOLD.getLineHeight() / 2);
+ int textFieldX = (int) (width * 0.7125f + Fonts.BOLD.getWidth("Search: "));
+ int textFieldY = (int) (headerY + Fonts.BOLD.getLineHeight() / 2);
search = new TextField(
- container, Utils.FONT_BOLD, textFieldX, textFieldY,
- (int) (width * 0.99f) - textFieldX, Utils.FONT_BOLD.getLineHeight()
+ container, Fonts.BOLD, textFieldX, textFieldY,
+ (int) (width * 0.99f) - textFieldX, Fonts.BOLD.getLineHeight()
);
search.setBackgroundColor(Color.transparent);
search.setBorderColor(Color.transparent);
@@ -287,6 +341,22 @@ public class SongMenu extends BasicGameState {
int loaderDim = GameImage.MENU_MUSICNOTE.getImage().getWidth();
SpriteSheet spr = new SpriteSheet(GameImage.MENU_LOADER.getImage(), loaderDim, loaderDim);
loader = new Animation(spr, 50);
+
+ // beatmap watch service listener
+ final StateBasedGame game_ = game;
+ BeatmapWatchService.addListener(new BeatmapWatchServiceListener() {
+ @Override
+ public void eventReceived(Kind> kind, Path child) {
+ if (!songFolderChanged && kind != StandardWatchEventKinds.ENTRY_MODIFY) {
+ songFolderChanged = true;
+ if (game_.getCurrentStateID() == Opsu.STATE_SONGMENU)
+ UI.sendBarNotification("Changes in Songs folder detected. Hit F5 to refresh.");
+ }
+ }
+ });
+
+ // star stream
+ starStream = new StarStream(width, height);
}
@Override
@@ -300,23 +370,30 @@ public class SongMenu extends BasicGameState {
// background
if (focusNode != null) {
- Beatmap focusNodeBeatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex);
- if (!focusNodeBeatmap.drawBG(width, height, 1.0f, true))
+ Beatmap focusNodeBeatmap = focusNode.getSelectedBeatmap();
+ if (!focusNodeBeatmap.drawBackground(width, height, bgAlpha.getValue(), true))
GameImage.PLAYFIELD.getImage().draw();
}
+ // star stream
+ starStream.draw();
+
// song buttons
BeatmapSetNode node = startNode;
+ int starï½”NodeOffsetoffset = 0;
+ if (node.prev != null) {
+ starï½”NodeOffsetoffset = -1;
+ node = node.prev;
+ }
g.setClip(0, (int) (headerY + DIVIDER_LINE_WIDTH / 2), width, (int) (footerY - headerY));
- for (int i = startNodeOffset; i < MAX_SONG_BUTTONS + 1 && node != null; i++, node = node.next) {
+ for (int i = startNodeOffset + starï½”NodeOffsetoffset; i < MAX_SONG_BUTTONS + 1 && node != null; i++, node = node.next) {
// draw the node
- float offset = (node == hoverIndex) ? hoverOffset : 0f;
+ float offset = (node == hoverIndex) ? hoverOffset.getValue() : 0f;
float ypos = buttonY + (i*buttonOffset) ;
float mid = height/2 - ypos - buttonOffset/2;
- final float circleRadi = 1000 * GameImage.getUIscale();
- //finds points along a very large circle
- // x^2 = h^2 - y^2
- float t = circleRadi*circleRadi - (mid*mid);
+ final float circleRadi = 700 * GameImage.getUIscale();
+ //finds points along a very large circle (x^2 = h^2 - y^2)
+ float t = circleRadi * circleRadi - (mid * mid);
float xpos = (float)(t>0?Math.sqrt(t):0) - circleRadi + 50 * GameImage.getUIscale();
ScoreData[] scores = getScoreDataForNode(node, false);
node.draw(buttonX - offset - xpos, ypos,
@@ -336,7 +413,7 @@ public class SongMenu extends BasicGameState {
MAX_SONG_BUTTONS * buttonOffset,
width, headerY + DIVIDER_LINE_WIDTH / 2,
0, MAX_SONG_BUTTONS * buttonOffset,
- Utils.COLOR_BLACK_ALPHA, Color.white, true);
+ Colors.BLACK_ALPHA, Color.white, true);
}
}
@@ -345,14 +422,19 @@ public class SongMenu extends BasicGameState {
ScoreData.clipToDownloadArea(g);
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
- for (int i = 0; i < MAX_SCORE_BUTTONS + 1; i++) {
- int rank = startScore + i;
+
+ int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS + 1);
+ float timerScale = 1f - (1 / 3f) * ((MAX_SCORE_BUTTONS - scoreButtons) / (float) (MAX_SCORE_BUTTONS - 1));
+ int duration = (int) (songChangeTimer.getDuration() * timerScale);
+ int segmentDuration = (int) ((2 / 3f) * songChangeTimer.getDuration());
+ int time = songChangeTimer.getTime();
+ for (int i = 0, rank = startScore; i < scoreButtons; i++, rank++) {
if (rank < 0)
continue;
- if (rank >= focusScores.length)
- break;
long prevScore = (rank + 1 < focusScores.length) ? focusScores[rank + 1].score : -1;
- focusScores[rank].draw(g, offset + i*ScoreData.getButtonOffset(), rank, prevScore, ScoreData.buttonContains(mouseX, mouseY-offset, i));
+ float t = Utils.clamp((time - (i * (duration - segmentDuration) / scoreButtons)) / (float) segmentDuration, 0f, 1f);
+ boolean focus = (t >= 0.9999f && ScoreData.buttonContains(mouseX, mouseY - offset, i));
+ focusScores[rank].draw(g, offset + i*ScoreData.getButtonOffset(), rank, prevScore, focus, t);
}
g.clearClip();
@@ -360,13 +442,12 @@ public class SongMenu extends BasicGameState {
if (focusScores.length > MAX_SCORE_BUTTONS && ScoreData.areaContains(mouseX, mouseY))
ScoreData.drawScrollbar(g, startScorePos.getPosition() , focusScores.length * ScoreData.getButtonOffset());
}
-
-
+
// top/bottom bars
- g.setColor(Utils.COLOR_BLACK_ALPHA);
+ g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, headerY);
g.fillRect(0, footerY, width, height - footerY);
- g.setColor(Utils.COLOR_BLUE_DIVIDER);
+ g.setColor(Colors.BLUE_DIVIDER);
g.setLineWidth(DIVIDER_LINE_WIDTH);
g.drawLine(0, headerY, width, headerY);
g.drawLine(0, footerY, width, footerY);
@@ -379,8 +460,13 @@ public class SongMenu extends BasicGameState {
Image musicNote = GameImage.MENU_MUSICNOTE.getImage();
if (MusicController.isTrackLoading())
loader.draw(marginX, marginY);
- else
- musicNote.draw(marginX, marginY);
+ else {
+ float t = musicIconBounceTimer.getValue() * 2f;
+ if (t > 1)
+ t = 2f - t;
+ float musicNoteScale = 1f + 0.3f * t;
+ musicNote.getScaledCopy(musicNoteScale).drawCentered(marginX + musicNote.getWidth() / 2f, marginY + musicNote.getHeight() / 2f);
+ }
int iconWidth = musicNote.getWidth();
// song info text
@@ -388,26 +474,49 @@ public class SongMenu extends BasicGameState {
songInfo = focusNode.getInfo();
if (Options.useUnicodeMetadata()) { // load glyphs
Beatmap beatmap = focusNode.getBeatmapSet().get(0);
- Utils.loadGlyphs(Utils.FONT_LARGE, beatmap.titleUnicode, beatmap.artistUnicode);
+ Fonts.loadGlyphs(Fonts.LARGE, beatmap.titleUnicode);
+ Fonts.loadGlyphs(Fonts.LARGE, beatmap.artistUnicode);
}
}
marginX += 5;
+ Color c = Colors.WHITE_FADE;
+ float oldAlpha = c.a;
+ float t = AnimationEquation.OUT_QUAD.calc(songChangeTimer.getValue());
float headerTextY = marginY * 0.2f;
- Utils.FONT_LARGE.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[0], Color.white);
- headerTextY += Utils.FONT_LARGE.getLineHeight() - 6;
- Utils.FONT_DEFAULT.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[1], Color.white);
- headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 2;
- float speedModifier = GameMod.getSpeedMultiplier();
- Color color2 = (speedModifier == 1f) ? Color.white :
- (speedModifier > 1f) ? Utils.COLOR_RED_HIGHLIGHT : Utils.COLOR_BLUE_HIGHLIGHT;
- Utils.FONT_BOLD.drawString(marginX, headerTextY, songInfo[2], color2);
- headerTextY += Utils.FONT_BOLD.getLineHeight() - 4;
- Utils.FONT_DEFAULT.drawString(marginX, headerTextY, songInfo[3], Color.white);
- headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 4;
- float multiplier = GameMod.getDifficultyMultiplier();
- Color color4 = (multiplier == 1f) ? Color.white :
- (multiplier > 1f) ? Utils.COLOR_RED_HIGHLIGHT : Utils.COLOR_BLUE_HIGHLIGHT;
- Utils.FONT_SMALL.drawString(marginX, headerTextY, songInfo[4], color4);
+ c.a = Math.min(t * songInfo.length / 1.5f, 1f);
+ if (c.a > 0)
+ Fonts.LARGE.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[0], c);
+ headerTextY += Fonts.LARGE.getLineHeight() - 6;
+ c.a = Math.min((t - 1f / (songInfo.length * 1.5f)) * songInfo.length / 1.5f, 1f);
+ if (c.a > 0)
+ Fonts.DEFAULT.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[1], c);
+ headerTextY += Fonts.DEFAULT.getLineHeight() - 2;
+ c.a = Math.min((t - 2f / (songInfo.length * 1.5f)) * songInfo.length / 1.5f, 1f);
+ if (c.a > 0) {
+ float speedModifier = GameMod.getSpeedMultiplier();
+ Color color2 = (speedModifier == 1f) ? c :
+ (speedModifier > 1f) ? Colors.RED_HIGHLIGHT : Colors.BLUE_HIGHLIGHT;
+ float oldAlpha2 = color2.a;
+ color2.a = c.a;
+ Fonts.BOLD.drawString(marginX, headerTextY, songInfo[2], color2);
+ color2.a = oldAlpha2;
+ }
+ headerTextY += Fonts.BOLD.getLineHeight() - 4;
+ c.a = Math.min((t - 3f / (songInfo.length * 1.5f)) * songInfo.length / 1.5f, 1f);
+ if (c.a > 0)
+ Fonts.DEFAULT.drawString(marginX, headerTextY, songInfo[3], c);
+ headerTextY += Fonts.DEFAULT.getLineHeight() - 4;
+ c.a = Math.min((t - 4f / (songInfo.length * 1.5f)) * songInfo.length / 1.5f, 1f);
+ if (c.a > 0) {
+ float multiplier = GameMod.getDifficultyMultiplier();
+ Color color4 = (multiplier == 1f) ? c :
+ (multiplier > 1f) ? Colors.RED_HIGHLIGHT : Colors.BLUE_HIGHLIGHT;
+ float oldAlpha4 = color4.a;
+ color4.a = c.a;
+ Fonts.SMALL.drawString(marginX, headerTextY, songInfo[4], color4);
+ color4.a = oldAlpha4;
+ }
+ c.a = oldAlpha;
}
// selection buttons
@@ -440,38 +549,41 @@ public class SongMenu extends BasicGameState {
int searchX = search.getX(), searchY = search.getY();
float searchBaseX = width * 0.7f;
float searchTextX = width * 0.7125f;
- float searchRectHeight = Utils.FONT_BOLD.getLineHeight() * 2;
- float searchExtraHeight = Utils.FONT_DEFAULT.getLineHeight() * 0.7f;
+ float searchRectHeight = Fonts.BOLD.getLineHeight() * 2;
+ float searchExtraHeight = Fonts.DEFAULT.getLineHeight() * 0.7f;
float searchProgress = (searchTransitionTimer < SEARCH_TRANSITION_TIME) ?
((float) searchTransitionTimer / SEARCH_TRANSITION_TIME) : 1f;
- float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
+ float oldAlpha = Colors.BLACK_ALPHA.a;
if (searchEmpty) {
searchRectHeight += (1f - searchProgress) * searchExtraHeight;
- Utils.COLOR_BLACK_ALPHA.a = 0.5f - searchProgress * 0.3f;
+ Colors.BLACK_ALPHA.a = 0.5f - searchProgress * 0.3f;
} else {
searchRectHeight += searchProgress * searchExtraHeight;
- Utils.COLOR_BLACK_ALPHA.a = 0.2f + searchProgress * 0.3f;
+ Colors.BLACK_ALPHA.a = 0.2f + searchProgress * 0.3f;
}
- g.setColor(Utils.COLOR_BLACK_ALPHA);
+ g.setColor(Colors.BLACK_ALPHA);
g.fillRect(searchBaseX, headerY + DIVIDER_LINE_WIDTH / 2, width - searchBaseX, searchRectHeight);
- Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
- Utils.FONT_BOLD.drawString(searchTextX, searchY, "Search:", Utils.COLOR_GREEN_SEARCH);
+ Colors.BLACK_ALPHA.a = oldAlpha;
+ Fonts.BOLD.drawString(searchTextX, searchY, "Search:", Colors.GREEN_SEARCH);
if (searchEmpty)
- Utils.FONT_BOLD.drawString(searchX, searchY, "Type to search!", Color.white);
+ Fonts.BOLD.drawString(searchX, searchY, "Type to search!", Color.white);
else {
g.setColor(Color.white);
// TODO: why is this needed to correctly position the TextField?
search.setLocation(searchX - 3, searchY - 1);
search.render(container, g);
search.setLocation(searchX, searchY);
- Utils.FONT_DEFAULT.drawString(searchTextX, searchY + Utils.FONT_BOLD.getLineHeight(),
+ Fonts.DEFAULT.drawString(searchTextX, searchY + Fonts.BOLD.getLineHeight(),
(searchResultString == null) ? "Searching..." : searchResultString, Color.white);
}
+
+
+
// reloading beatmaps
if (reloadThread != null) {
// darken the screen
- g.setColor(Utils.COLOR_BLACK_ALPHA);
+ g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height);
UI.drawLoadingProgress(g);
@@ -510,6 +622,21 @@ public class SongMenu extends BasicGameState {
}
}
+ if (focusNode != null) {
+ // fade in background
+ Beatmap focusNodeBeatmap = focusNode.getSelectedBeatmap();
+ if (!focusNodeBeatmap.isBackgroundLoading())
+ bgAlpha.update(delta);
+
+ // song change timers
+ songChangeTimer.update(delta);
+ if (!MusicController.isTrackLoading())
+ musicIconBounceTimer.update(delta);
+ }
+
+ // star stream
+ starStream.update(delta);
+
// search
search.setFocus(true);
searchTimer += delta;
@@ -574,14 +701,10 @@ public class SongMenu extends BasicGameState {
if ((mouseX > cx && mouseX < cx + buttonWidth) &&
(mouseY > buttonY + (i * buttonOffset) && mouseY < buttonY + (i * buttonOffset) + buttonHeight)) {
if (node == hoverIndex) {
- if (hoverOffset < MAX_HOVER_OFFSET) {
- hoverOffset += delta / 3f;
- if (hoverOffset > MAX_HOVER_OFFSET)
- hoverOffset = MAX_HOVER_OFFSET;
- }
+ hoverOffset.update(delta);
} else {
hoverIndex = node ;
- hoverOffset = 0f;
+ hoverOffset.setTime(0);
}
isHover = true;
break;
@@ -589,21 +712,19 @@ public class SongMenu extends BasicGameState {
}
}
if (!isHover) {
- hoverOffset = 0f;
+ hoverOffset.setTime(0);
hoverIndex = null;
} else
return;
// tooltips
- if (focusScores != null) {
+ if (focusScores != null && ScoreData.areaContains(mouseX, mouseY)) {
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
- for (int i = 0; i < MAX_SCORE_BUTTONS; i++) {
- int rank = startScore + i;
+ int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS);
+ for (int i = 0, rank = startScore; i < scoreButtons; i++, rank++) {
if (rank < 0)
continue;
- if (rank >= focusScores.length)
- break;
if (ScoreData.buttonContains(mouseX, mouseY - offset, i)) {
UI.updateTooltip(delta, focusScores[rank].getTooltipString(), true);
break;
@@ -689,7 +810,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();
BeatmapSetNode oldHoverIndex = hoverIndex;
// clicked node is already expanded
@@ -714,7 +835,7 @@ public class SongMenu extends BasicGameState {
}
// restore hover data
- hoverOffset = oldHoverOffset;
+ hoverOffset.setTime(oldHoverOffsetTime);
hoverIndex = oldHoverIndex;
// open beatmap menu
@@ -728,12 +849,10 @@ public class SongMenu extends BasicGameState {
// score buttons
if (focusScores != null && ScoreData.areaContains(x, y)) {
- for (int i = 0; i < MAX_SCORE_BUTTONS + 1; i++) {
- int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
- int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
- int rank = startScore + i;
- if (rank >= focusScores.length)
- break;
+ int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
+ int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
+ int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS);
+ for (int i = 0, rank = startScore; i < scoreButtons; i++, rank++) {
if (ScoreData.buttonContains(x, y - offset, i)) {
SoundController.playSound(SoundEffect.MENUHIT);
if (button != Input.MOUSE_RIGHT_BUTTON) {
@@ -805,8 +924,12 @@ public class SongMenu extends BasicGameState {
break;
case Input.KEY_F5:
SoundController.playSound(SoundEffect.MENUHIT);
- ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.RELOAD);
- game.enterState(Opsu.STATE_BUTTONMENU);
+ if (songFolderChanged)
+ reloadBeatmaps(false);
+ else {
+ ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.RELOAD);
+ game.enterState(Opsu.STATE_BUTTONMENU);
+ }
break;
case Input.KEY_DELETE:
if (focusNode == null)
@@ -851,11 +974,11 @@ public class SongMenu extends BasicGameState {
if (next != null) {
SoundController.playSound(SoundEffect.MENUCLICK);
BeatmapSetNode oldStartNode = startNode;
- float oldHoverOffset = hoverOffset;
+ int oldHoverOffsetTime = hoverOffset.getTime();
BeatmapSetNode oldHoverIndex = hoverIndex;
setFocus(next, 0, false, true);
if (startNode == oldStartNode) {
- hoverOffset = oldHoverOffset;
+ hoverOffset.setTime(oldHoverOffsetTime);
hoverIndex = oldHoverIndex;
}
}
@@ -867,11 +990,11 @@ public class SongMenu extends BasicGameState {
if (prev != null) {
SoundController.playSound(SoundEffect.MENUCLICK);
BeatmapSetNode oldStartNode = startNode;
- float oldHoverOffset = hoverOffset;
+ int oldHoverOffsetTime = hoverOffset.getTime();
BeatmapSetNode 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;
}
}
@@ -965,18 +1088,26 @@ public class SongMenu extends BasicGameState {
selectRandomButton.resetHover();
selectMapOptionsButton.resetHover();
selectOptionsButton.resetHover();
- hoverOffset = 0f;
+ hoverOffset.setTime(0);
hoverIndex = null;
startScorePos.setPosition(0);
beatmapMenuTimer = -1;
searchTransitionTimer = SEARCH_TRANSITION_TIME;
songInfo = null;
+ bgAlpha.setTime(bgAlpha.getDuration());
+ songChangeTimer.setTime(songChangeTimer.getDuration());
+ musicIconBounceTimer.setTime(musicIconBounceTimer.getDuration());
+ starStream.clear();
// reset song stack
randomStack = new Stack();
+ // reload beatmaps if song folder changed
+ if (songFolderChanged && stateAction != MenuState.RELOAD)
+ reloadBeatmaps(false);
+
// set focus node if not set (e.g. theme song playing)
- if (focusNode == null && BeatmapSetList.get().size() > 0)
+ else if (focusNode == null && BeatmapSetList.get().size() > 0)
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
// reset music track
@@ -1003,13 +1134,13 @@ public class SongMenu extends BasicGameState {
// destroy skin images, if any
for (GameImage img : GameImage.values()) {
- if (img.isSkinnable())
- img.destroySkinImage();
+ if (img.isBeatmapSkinnable())
+ img.destroyBeatmapSkinImage();
}
// reload scores
if (focusNode != null) {
- scoreMap = ScoreDB.getMapSetScores(focusNode.getBeatmapSet().get(focusNode.beatmapIndex));
+ scoreMap = ScoreDB.getMapSetScores(focusNode.getSelectedBeatmap());
focusScores = getScoreDataForNode(focusNode, true);
}
@@ -1022,7 +1153,7 @@ public class SongMenu extends BasicGameState {
case BEATMAP: // clear all scores
if (stateActionNode == null || stateActionNode.beatmapIndex == -1)
break;
- Beatmap beatmap = stateActionNode.getBeatmapSet().get(stateActionNode.beatmapIndex);
+ Beatmap beatmap = stateActionNode.getSelectedBeatmap();
ScoreDB.deleteScore(beatmap);
if (stateActionNode == focusNode) {
focusScores = null;
@@ -1033,7 +1164,7 @@ public class SongMenu extends BasicGameState {
if (stateActionScore == null)
break;
ScoreDB.deleteScore(stateActionScore);
- scoreMap = ScoreDB.getMapSetScores(focusNode.getBeatmapSet().get(focusNode.beatmapIndex));
+ scoreMap = ScoreDB.getMapSetScores(focusNode.getSelectedBeatmap());
focusScores = getScoreDataForNode(focusNode, true);
startScorePos.setPosition(0);
break;
@@ -1095,44 +1226,7 @@ public class SongMenu extends BasicGameState {
}
break;
case RELOAD: // reload beatmaps
- // reset state and node references
- MusicController.reset();
- startNode = focusNode = null;
- scoreMap = null;
- focusScores = null;
- oldFocusNode = null;
- randomStack = new Stack();
- songInfo = null;
- hoverOffset = 0f;
- hoverIndex = null;
- search.setText("");
- searchTimer = SEARCH_DELAY;
- searchTransitionTimer = SEARCH_TRANSITION_TIME;
- searchResultString = null;
-
- // reload songs in new thread
- reloadThread = new Thread() {
- @Override
- public void run() {
- // clear the beatmap cache
- BeatmapDB.clearDatabase();
-
- // invoke unpacker and parser
- File beatmapDir = Options.getBeatmapDir();
- OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir);
- BeatmapParser.parseAllFiles(beatmapDir);
-
- // initialize song list
- if (BeatmapSetList.get().size() > 0) {
- BeatmapSetList.get().init();
- setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
- } else
- MusicController.playThemeSong();
-
- reloadThread = null;
- }
- };
- reloadThread.start();
+ reloadBeatmaps(true);
break;
default:
break;
@@ -1210,15 +1304,25 @@ public class SongMenu extends BasicGameState {
if (node == null)
return null;
- hoverOffset = 0f;
+ hoverOffset.setTime(0);
hoverIndex = null;
songInfo = null;
+ songChangeTimer.setTime(0);
+ musicIconBounceTimer.setTime(0);
BeatmapSetNode oldFocus = focusNode;
// expand node before focusing it
int expandedIndex = BeatmapSetList.get().getExpandedIndex();
if (node.index != expandedIndex) {
node = BeatmapSetList.get().expand(node.index);
+
+
+ // calculate difficulties
+ calculateStarRatings(node.getBeatmapSet());
+
+ // if start node was previously expanded, move it
+ if (startNode != null && startNode.index == expandedIndex)
+ startNode = BeatmapSetList.get().getBaseNode(startNode.index);
}
// check beatmapIndex bounds
@@ -1227,7 +1331,7 @@ public class SongMenu extends BasicGameState {
beatmapIndex = (int) (Math.random() * length);
focusNode = BeatmapSetList.get().getNode(node, beatmapIndex);
- Beatmap beatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex);
+ Beatmap beatmap = focusNode.getSelectedBeatmap();
MusicController.play(beatmap, false, preview);
// load scores
@@ -1286,6 +1390,14 @@ public class SongMenu extends BasicGameState {
songScrolling.scrollToPosition((focusNode.index + focusNode.getBeatmapSet().size() ) * buttonOffset - (footerY - headerY));
//*/
+ // load background image
+ beatmap.loadBackground();
+ boolean isBgNull = lastBackgroundImage == null || beatmap.bg == null;
+ if ((isBgNull && lastBackgroundImage != beatmap.bg) || (!isBgNull && !beatmap.bg.equals(lastBackgroundImage))) {
+ bgAlpha.setTime(0);
+ lastBackgroundImage = beatmap.bg;
+ }
+
return oldFocus;
}
@@ -1346,7 +1458,7 @@ public class SongMenu extends BasicGameState {
if (scoreMap == null || scoreMap.isEmpty() || node.beatmapIndex == -1) // node not expanded
return null;
- Beatmap beatmap = node.getBeatmapSet().get(node.beatmapIndex);
+ Beatmap beatmap = node.getSelectedBeatmap();
ScoreData[] scores = scoreMap.get(beatmap.version);
if (scores == null || scores.length < 1) // no scores
return null;
@@ -1364,6 +1476,86 @@ public class SongMenu extends BasicGameState {
return null; // incorrect map
}
+ /**
+ * Reloads all beatmaps.
+ * @param fullReload if true, also clear the beatmap cache and invoke the unpacker
+ */
+ private void reloadBeatmaps(final boolean fullReload) {
+ songFolderChanged = false;
+
+ // reset state and node references
+ MusicController.reset();
+ startNode = focusNode = null;
+ scoreMap = null;
+ focusScores = null;
+ oldFocusNode = null;
+ randomStack = new Stack();
+ songInfo = null;
+ hoverOffset.setTime(0);
+ hoverIndex = null;
+ search.setText("");
+ searchTimer = SEARCH_DELAY;
+ searchTransitionTimer = SEARCH_TRANSITION_TIME;
+ searchResultString = null;
+ lastBackgroundImage = null;
+
+ // reload songs in new thread
+ reloadThread = new Thread() {
+ @Override
+ public void run() {
+ File beatmapDir = Options.getBeatmapDir();
+
+ if (fullReload) {
+ // clear the beatmap cache
+ BeatmapDB.clearDatabase();
+
+ // invoke unpacker
+ OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir);
+ }
+
+ // invoke parser
+ BeatmapParser.parseAllFiles(beatmapDir);
+
+ // initialize song list
+ if (BeatmapSetList.get().size() > 0) {
+ BeatmapSetList.get().init();
+ setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
+ } else
+ MusicController.playThemeSong();
+
+ reloadThread = null;
+ }
+ };
+ reloadThread.start();
+ }
+
+ /**
+ * Calculates all star ratings for a beatmap set.
+ * @param beatmapSet the set of beatmaps
+ */
+ private void calculateStarRatings(BeatmapSet beatmapSet) {
+ for (Beatmap beatmap : beatmapSet) {
+ if (beatmap.starRating >= 0) { // already calculated
+ beatmapsCalculated.put(beatmap, beatmapsCalculated.get(beatmap));
+ continue;
+ }
+
+ // if timing points are already loaded before this (for whatever reason),
+ // don't clear the array fields to be safe
+ boolean hasTimingPoints = (beatmap.timingPoints != null);
+
+ BeatmapDifficultyCalculator diffCalc = new BeatmapDifficultyCalculator(beatmap);
+ diffCalc.calculate();
+ if (diffCalc.getStarRating() == -1)
+ continue; // calculations failed
+
+ // save star rating
+ beatmap.starRating = diffCalc.getStarRating();
+ BeatmapDB.setStars(beatmap);
+ beatmapsCalculated.put(beatmap, !hasTimingPoints);
+ }
+ }
+
/**
* Starts the game.
*/
@@ -1372,8 +1564,12 @@ public class SongMenu extends BasicGameState {
return;
SoundController.playSound(SoundEffect.MENUHIT);
- MultiClip.destroyExtraClips();
Beatmap beatmap = MusicController.getBeatmap();
+ if (focusNode == null || beatmap != focusNode.getSelectedBeatmap()) {
+ UI.sendBarNotification("Unable to load the beatmap audio.");
+ return;
+ }
+ MultiClip.destroyExtraClips();
Game gameState = (Game) game.getState(Opsu.STATE_GAME);
gameState.loadBeatmap(beatmap);
gameState.setRestart(Game.Restart.NEW);
diff --git a/src/itdelatrisu/opsu/states/Splash.java b/src/itdelatrisu/opsu/states/Splash.java
index 14d8245a..a0ea9963 100644
--- a/src/itdelatrisu/opsu/states/Splash.java
+++ b/src/itdelatrisu/opsu/states/Splash.java
@@ -21,21 +21,23 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
+import itdelatrisu.opsu.beatmap.BeatmapWatchService;
+import itdelatrisu.opsu.beatmap.OszUnpacker;
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 +49,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 = 400;
+
/** Whether or not loading has completed. */
private boolean finished = false;
@@ -59,8 +64,14 @@ public class Splash extends BasicGameState {
/** Whether the skin being loaded is a new skin (for program restarts). */
private boolean newSkin = false;
+ /** Whether the watch service is newly enabled (for program restarts). */
+ private boolean watchServiceChange = false;
+
+ /** Logo alpha level. */
+ private AnimatedValue logoAlpha;
+
// game-related variables
- private int state;
+ private final int state;
private GameContainer container;
private boolean init = false;
@@ -77,9 +88,14 @@ public class Splash extends BasicGameState {
if (Options.getSkin() != null)
this.newSkin = (Options.getSkin().getDirectory() != Options.getSkinDir());
+ // check if watch service newly enabled
+ this.watchServiceChange = Options.isWatchServiceEnabled() && BeatmapWatchService.get() == null;
+
// 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);
}
@@ -99,13 +115,18 @@ public class Splash extends BasicGameState {
// resources already loaded (from application restart)
if (BeatmapSetList.get() != null) {
- // reload sounds if skin changed
- if (newSkin) {
+ if (newSkin || watchServiceChange) { // need to reload resources
thread = new Thread() {
@Override
public void run() {
+ // reload beatmaps if watch service newly enabled
+ if (watchServiceChange)
+ BeatmapParser.parseAllFiles(Options.getBeatmapDir());
+
+ // reload sounds if skin changed
// TODO: only reload each sound if actually needed?
- SoundController.init();
+ if (newSkin)
+ SoundController.init();
finished = true;
thread = null;
@@ -144,13 +165,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();
diff --git a/src/itdelatrisu/opsu/ui/Colors.java b/src/itdelatrisu/opsu/ui/Colors.java
new file mode 100644
index 00000000..2f79efb9
--- /dev/null
+++ b/src/itdelatrisu/opsu/ui/Colors.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+import org.newdawn.slick.Color;
+
+/**
+ * Colors used for drawing.
+ */
+public class Colors {
+ public static final Color
+ BLACK_ALPHA = new Color(0, 0, 0, 0.5f),
+ WHITE_ALPHA = new Color(255, 255, 255, 0.5f),
+ BLUE_DIVIDER = new Color(49, 94, 237),
+ BLUE_BACKGROUND = new Color(74, 130, 255),
+ BLUE_BUTTON = new Color(40, 129, 237),
+ ORANGE_BUTTON = new Color(200, 90, 3),
+ YELLOW_ALPHA = new Color(255, 255, 0, 0.4f),
+ WHITE_FADE = new Color(255, 255, 255, 1f),
+ RED_HOVER = new Color(255, 112, 112),
+ GREEN = new Color(137, 201, 79),
+ LIGHT_ORANGE = new Color(255, 192, 128),
+ LIGHT_GREEN = new Color(128, 255, 128),
+ LIGHT_BLUE = new Color(128, 128, 255),
+ GREEN_SEARCH = new Color(173, 255, 47),
+ DARK_GRAY = new Color(0.3f, 0.3f, 0.3f, 1f),
+ RED_HIGHLIGHT = new Color(246, 154, 161),
+ BLUE_HIGHLIGHT = new Color(173, 216, 230),
+ BLACK_BG_NORMAL = new Color(0, 0, 0, 0.25f),
+ BLACK_BG_HOVER = new Color(0, 0, 0, 0.5f),
+ BLACK_BG_FOCUS = new Color(0, 0, 0, 0.75f);
+
+ // This class should not be instantiated.
+ private Colors() {}
+}
diff --git a/src/itdelatrisu/opsu/ui/Cursor.java b/src/itdelatrisu/opsu/ui/Cursor.java
index 252a9c2c..d0382aae 100644
--- a/src/itdelatrisu/opsu/ui/Cursor.java
+++ b/src/itdelatrisu/opsu/ui/Cursor.java
@@ -24,9 +24,10 @@ import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.skins.Skin;
+import itdelatrisu.opsu.ui.animations.AnimationEquation;
+import java.awt.Point;
import java.nio.IntBuffer;
-import java.util.Iterator;
import java.util.LinkedList;
import org.lwjgl.BufferUtils;
@@ -45,13 +46,25 @@ public class Cursor {
private static org.lwjgl.input.Cursor emptyCursor;
/** Last cursor coordinates. */
- private int lastX = -1, lastY = -1;
+ private Point lastPosition;
/** Cursor rotation angle. */
private float cursorAngle = 0f;
+ /** The time in milliseconds when the cursor was last pressed, used for the scaling animation. */
+ private long lastCursorPressTime = 0L;
+
+ /** Whether or not the cursor was pressed in the last frame, used for the scaling animation. */
+ private boolean lastCursorPressState = false;
+
+ /** The amount the cursor scale increases, if enabled, when pressed. */
+ private static final float CURSOR_SCALE_CHANGE = 0.25f;
+
+ /** The time it takes for the cursor to scale, in milliseconds. */
+ private static final float CURSOR_SCALE_TIME = 125;
+
/** Stores all previous cursor locations to display a trail. */
- private LinkedList cursorX, cursorY;
+ private LinkedList trail = new LinkedList();
// game-related variables
private static GameContainer container;
@@ -81,10 +94,7 @@ public class Cursor {
/**
* Constructor.
*/
- public Cursor() {
- cursorX = new LinkedList();
- cursorY = new LinkedList();
- }
+ public Cursor() {}
/**
* Draws the cursor.
@@ -105,85 +115,90 @@ 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 skinned = GameImage.CURSOR.hasSkinImage();
+ boolean beatmapSkinned = GameImage.CURSOR.hasBeatmapSkinImage();
boolean newStyle, hasMiddle;
- if (skinned) {
+ Skin skin = Options.getSkin();
+ if (beatmapSkinned) {
newStyle = true; // osu! currently treats all beatmap cursors as new-style cursors
- hasMiddle = GameImage.CURSOR_MIDDLE.hasSkinImage();
+ hasMiddle = GameImage.CURSOR_MIDDLE.hasBeatmapSkinImage();
} else
newStyle = hasMiddle = Options.isNewCursorEnabled();
- if (skinned || newStyle) {
+ if (newStyle || beatmapSkinned) {
cursor = GameImage.CURSOR.getImage();
cursorTrail = GameImage.CURSOR_TRAIL.getImage();
} else {
- cursor = GameImage.CURSOR_OLD.getImage();
- cursorTrail = GameImage.CURSOR_TRAIL_OLD.getImage();
+ cursor = GameImage.CURSOR.hasGameSkinImage() ? GameImage.CURSOR.getImage() : GameImage.CURSOR_OLD.getImage();
+ cursorTrail = GameImage.CURSOR_TRAIL.hasGameSkinImage() ? GameImage.CURSOR_TRAIL.getImage() : GameImage.CURSOR_TRAIL_OLD.getImage();
}
if (hasMiddle)
cursorMiddle = GameImage.CURSOR_MIDDLE.getImage();
- int removeCount = 0;
- int FPSmod = (Options.getTargetFPS() / 60);
- Skin skin = Options.getSkin();
-
// scale cursor
- float cursorScale = Options.getCursorScale();
- if (mousePressed && skin.isCursorExpanded())
- cursorScale *= 1.25f; // increase the cursor size if pressed
+ float cursorScaleAnimated = 1f;
+ if (skin.isCursorExpanded()) {
+ if (lastCursorPressState != mousePressed) {
+ lastCursorPressState = mousePressed;
+ lastCursorPressTime = System.currentTimeMillis();
+ }
+
+ float cursorScaleChange = CURSOR_SCALE_CHANGE * AnimationEquation.IN_OUT_CUBIC.calc(
+ Utils.clamp(System.currentTimeMillis() - lastCursorPressTime, 0, CURSOR_SCALE_TIME) / CURSOR_SCALE_TIME);
+ cursorScaleAnimated = 1f + ((mousePressed) ? cursorScaleChange : CURSOR_SCALE_CHANGE - cursorScaleChange);
+ }
+ float cursorScale = cursorScaleAnimated * Options.getCursorScale();
if (cursorScale != 1f) {
cursor = cursor.getScaledCopy(cursorScale);
cursorTrail = cursorTrail.getScaledCopy(cursorScale);
- if (hasMiddle)
- cursorMiddle = cursorMiddle.getScaledCopy(cursorScale);
}
// TODO: use an image buffer
+ int removeCount = 0;
+ float FPSmod = Math.max(container.getFPS(), 1) / 60f;
if (newStyle) {
// new style: add all points between cursor movements
- if (lastX < 0) {
- lastX = mouseX;
- lastY = mouseY;
+ if (lastPosition == null) {
+ lastPosition = new Point(mouseX, mouseY);
return;
}
- addCursorPoints(lastX, lastY, mouseX, mouseY);
- lastX = mouseX;
- lastY = mouseY;
+ addCursorPoints(lastPosition.x, lastPosition.y, mouseX, mouseY);
+ lastPosition.move(mouseX, mouseY);
- removeCount = (cursorX.size() / (6 * FPSmod)) + 1;
+ removeCount = (int) (trail.size() / (6 * FPSmod)) + 1;
} else {
// old style: sample one point at a time
- cursorX.add(mouseX);
- cursorY.add(mouseY);
+ trail.add(new Point(mouseX, mouseY));
- int max = 10 * FPSmod;
- if (cursorX.size() > max)
- removeCount = cursorX.size() - max;
+ int max = (int) (10 * FPSmod);
+ if (trail.size() > max)
+ removeCount = trail.size() - max;
}
// remove points from the lists
- for (int i = 0; i < removeCount && !cursorX.isEmpty(); i++) {
- cursorX.remove();
- cursorY.remove();
- }
+ for (int i = 0; i < removeCount && !trail.isEmpty(); i++)
+ trail.remove();
// draw a fading trail
float alpha = 0f;
- float t = 2f / cursorX.size();
- if (skin.isCursorTrailRotated())
- cursorTrail.setRotation(cursorAngle);
- Iterator iterX = cursorX.iterator();
- Iterator iterY = cursorY.iterator();
- while (iterX.hasNext()) {
- int cx = iterX.next();
- int cy = iterY.next();
+ float t = 2f / trail.size();
+ int cursorTrailWidth = cursorTrail.getWidth(), cursorTrailHeight = cursorTrail.getHeight();
+ float cursorTrailRotation = (skin.isCursorTrailRotated()) ? cursorAngle : 0;
+ cursorTrail.startUse();
+ for (Point p : trail) {
alpha += t;
- cursorTrail.setAlpha(alpha);
-// if (cx != x || cy != y)
- cursorTrail.drawCentered(cx, cy);
+ cursorTrail.setImageColor(1f, 1f, 1f, alpha);
+ cursorTrail.drawEmbedded(
+ p.x - (cursorTrailWidth / 2f), p.y - (cursorTrailHeight / 2f),
+ cursorTrailWidth, cursorTrailHeight, cursorTrailRotation);
}
- cursorTrail.drawCentered(mouseX, mouseY);
+ cursorTrail.drawEmbedded(
+ mouseX - (cursorTrailWidth / 2f), mouseY - (cursorTrailHeight / 2f),
+ cursorTrailWidth, cursorTrailHeight, cursorTrailRotation);
+ cursorTrail.endUse();
// draw the other components
if (newStyle && skin.isCursorRotated())
@@ -212,8 +227,7 @@ public class Cursor {
if (dy <= dx) {
for (int i = 0; ; i++) {
if (i == k) {
- cursorX.add(x1);
- cursorY.add(y1);
+ trail.add(new Point(x1, y1));
i = 0;
}
if (x1 == x2)
@@ -228,8 +242,7 @@ public class Cursor {
} else {
for (int i = 0; ; i++) {
if (i == k) {
- cursorX.add(x1);
- cursorY.add(y1);
+ trail.add(new Point(x1, y1));
i = 0;
}
if (y1 == y2)
@@ -255,13 +268,13 @@ public class Cursor {
}
/**
- * Resets all cursor data and skins.
+ * Resets all cursor data and beatmap skins.
*/
public void reset() {
// destroy skin images
- GameImage.CURSOR.destroySkinImage();
- GameImage.CURSOR_MIDDLE.destroySkinImage();
- GameImage.CURSOR_TRAIL.destroySkinImage();
+ GameImage.CURSOR.destroyBeatmapSkinImage();
+ GameImage.CURSOR_MIDDLE.destroyBeatmapSkinImage();
+ GameImage.CURSOR_TRAIL.destroyBeatmapSkinImage();
// reset locations
resetLocations();
@@ -276,18 +289,17 @@ public class Cursor {
* Resets all cursor location data.
*/
public void resetLocations() {
- lastX = lastY = -1;
- cursorX.clear();
- cursorY.clear();
+ lastPosition = null;
+ trail.clear();
}
/**
* Returns whether or not the cursor is skinned.
*/
- public boolean isSkinned() {
- return (GameImage.CURSOR.hasSkinImage() ||
- GameImage.CURSOR_MIDDLE.hasSkinImage() ||
- GameImage.CURSOR_TRAIL.hasSkinImage());
+ public boolean isBeatmapSkinned() {
+ return (GameImage.CURSOR.hasBeatmapSkinImage() ||
+ GameImage.CURSOR_MIDDLE.hasBeatmapSkinImage() ||
+ GameImage.CURSOR_TRAIL.hasBeatmapSkinImage());
}
/**
diff --git a/src/itdelatrisu/opsu/ui/DropdownMenu.java b/src/itdelatrisu/opsu/ui/DropdownMenu.java
new file mode 100644
index 00000000..059751a0
--- /dev/null
+++ b/src/itdelatrisu/opsu/ui/DropdownMenu.java
@@ -0,0 +1,424 @@
+/*
+ * 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;
+
+import itdelatrisu.opsu.GameImage;
+import itdelatrisu.opsu.ui.animations.AnimatedValue;
+import itdelatrisu.opsu.ui.animations.AnimationEquation;
+
+import org.newdawn.slick.Color;
+import org.newdawn.slick.Font;
+import org.newdawn.slick.Graphics;
+import org.newdawn.slick.Image;
+import org.newdawn.slick.Input;
+import org.newdawn.slick.SlickException;
+import org.newdawn.slick.UnicodeFont;
+import org.newdawn.slick.gui.AbstractComponent;
+import org.newdawn.slick.gui.GUIContext;
+
+/**
+ * Simple dropdown menu.
+ *
+ * Basic usage:
+ *
+ * - Override {@link #menuClicked(int)} to perform actions when the menu is clicked
+ * (e.g. play a sound effect, block input under certain conditions).
+ *
- Override {@link #itemSelected(int, Object)} to perform actions when a new item is selected.
+ *
- 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. */
+ private static final float PADDING_Y = 0.1f, CHEVRON_X = 0.03f;
+
+ /** Whether this component is active. */
+ private boolean active;
+
+ /** The menu items. */
+ private E[] items;
+
+ /** The menu item names. */
+ private String[] itemNames;
+
+ /** The index of the selected item. */
+ private int itemIndex = 0;
+
+ /** Whether the menu is expanded. */
+ private boolean expanded = false;
+
+ /** The expanding animation progress. */
+ private AnimatedValue expandProgress = new AnimatedValue(300, 0f, 1f, AnimationEquation.LINEAR);
+
+ /** The last update time, in milliseconds. */
+ private long lastUpdateTime;
+
+ /** The top-left coordinates. */
+ private float x, y;
+
+ /** The width and height of the dropdown menu. */
+ private int width, height;
+
+ /** The height of the base item. */
+ private int baseHeight;
+
+ /** The vertical offset between items. */
+ private float offsetY;
+
+ /** The colors to use. */
+ private Color
+ textColor = Color.white, backgroundColor = Color.black,
+ highlightColor = Colors.BLUE_DIVIDER, borderColor = Colors.BLUE_DIVIDER,
+ chevronDownColor = textColor, chevronRightColor = backgroundColor;
+
+ /** The fonts to use. */
+ private UnicodeFont fontNormal = Fonts.MEDIUM, fontSelected = Fonts.MEDIUMBOLD;
+
+ /** The chevron images. */
+ private Image chevronDown, chevronRight;
+
+ /**
+ * Creates a new dropdown menu.
+ * @param container the container rendering this menu
+ * @param items the list of items (with names given as their {@code toString()} methods)
+ * @param x the top-left x coordinate
+ * @param y the top-left y coordinate
+ */
+ public DropdownMenu(GUIContext container, E[] items, float x, float y) {
+ this(container, items, x, y, 0);
+ }
+
+ /**
+ * Creates a new dropdown menu with the given fonts.
+ * @param container the container rendering this menu
+ * @param items the list of items (with names given as their {@code toString()} methods)
+ * @param x the top-left x coordinate
+ * @param y the top-left y coordinate
+ * @param normal the normal font
+ * @param selected the font for the selected item
+ */
+ public DropdownMenu(GUIContext container, E[] items, float x, float y, UnicodeFont normal, UnicodeFont selected) {
+ this(container, items, x, y, 0, normal, selected);
+ }
+
+ /**
+ * Creates a new dropdown menu with the given width.
+ * @param container the container rendering this menu
+ * @param items the list of items (with names given as their {@code toString()} methods)
+ * @param x the top-left x coordinate
+ * @param y the top-left y coordinate
+ * @param width the menu width
+ */
+ public DropdownMenu(GUIContext container, E[] items, float x, float y, int width) {
+ super(container);
+ init(items, x, y, width);
+ }
+
+ /**
+ * Creates a new dropdown menu with the given width and fonts.
+ * @param container the container rendering this menu
+ * @param items the list of items (with names given as their {@code toString()} methods)
+ * @param x the top-left x coordinate
+ * @param y the top-left y coordinate
+ * @param width the menu width
+ * @param normal the normal font
+ * @param selected the font for the selected item
+ */
+ public DropdownMenu(GUIContext container, E[] items, float x, float y, int width, UnicodeFont normal, UnicodeFont selected) {
+ super(container);
+ this.fontNormal = normal;
+ this.fontSelected = selected;
+ init(items, x, y, width);
+ }
+
+ /**
+ * Returns the maximum item width from the list.
+ */
+ private int getMaxItemWidth() {
+ int maxWidth = 0;
+ for (int i = 0; i < itemNames.length; i++) {
+ int w = fontSelected.getWidth(itemNames[i]);
+ if (w > maxWidth)
+ maxWidth = w;
+ }
+ return maxWidth;
+ }
+
+ /**
+ * Initializes the component.
+ */
+ private void init(E[] items, float x, float y, int width) {
+ this.items = items;
+ this.itemNames = new String[items.length];
+ for (int i = 0; i < itemNames.length; i++)
+ itemNames[i] = items[i].toString();
+ this.x = x;
+ this.y = y;
+ this.baseHeight = fontNormal.getLineHeight();
+ this.offsetY = baseHeight + baseHeight * PADDING_Y;
+ this.height = (int) (offsetY * (items.length + 1));
+ int chevronDownSize = baseHeight * 4 / 5;
+ this.chevronDown = GameImage.CHEVRON_DOWN.getImage().getScaledCopy(chevronDownSize, chevronDownSize);
+ int chevronRightSize = baseHeight * 2 / 3;
+ this.chevronRight = GameImage.CHEVRON_RIGHT.getImage().getScaledCopy(chevronRightSize, chevronRightSize);
+ int maxItemWidth = getMaxItemWidth();
+ int minWidth = maxItemWidth + chevronRight.getWidth() * 2;
+ this.width = Math.max(width, minWidth);
+ }
+
+ @Override
+ public void setLocation(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ @Override
+ public int getX() { return (int) x; }
+
+ @Override
+ public int getY() { return (int) y; }
+
+ @Override
+ public int getWidth() { return width; }
+
+ @Override
+ public int getHeight() { return (expanded) ? height : baseHeight; }
+
+ /** Activates the component. */
+ public void activate() { this.active = true; }
+
+ /** Deactivates the component. */
+ public void deactivate() { this.active = false; }
+
+ /**
+ * Returns whether the dropdown menu is currently open.
+ * @return true if open, false otherwise
+ */
+ public boolean isOpen() { return expanded; }
+
+ /**
+ * Opens or closes the dropdown menu.
+ * @param flag true to open, false to close
+ */
+ public void open(boolean flag) { this.expanded = flag; }
+
+ /**
+ * Returns true if the coordinates are within the menu bounds.
+ * @param cx the x coordinate
+ * @param cy the y coordinate
+ */
+ public boolean contains(float cx, float cy) {
+ return (cx > x && cx < x + width && (
+ (cy > y && cy < y + baseHeight) ||
+ (expanded && cy > y + offsetY && cy < y + height)));
+ }
+
+ /**
+ * Returns true if the coordinates are within the base item bounds.
+ * @param cx the x coordinate
+ * @param cy the y coordinate
+ */
+ public boolean baseContains(float cx, float cy) {
+ return (cx > x && cx < x + width && cy > y && cy < y + baseHeight);
+ }
+
+ @Override
+ public void render(GUIContext container, Graphics g) throws SlickException {
+ // update animation
+ long time = container.getTime();
+ if (lastUpdateTime > 0) {
+ int delta = (int) (time - lastUpdateTime);
+ expandProgress.update((expanded) ? delta : -delta * 2);
+ }
+ this.lastUpdateTime = time;
+
+ // get parameters
+ Input input = container.getInput();
+ int idx = getIndexAt(input.getMouseX(), input.getMouseY());
+ float t = expandProgress.getValue();
+ if (expanded)
+ t = AnimationEquation.OUT_CUBIC.calc(t);
+
+ // background and border
+ Color oldGColor = g.getColor();
+ float oldLineWidth = g.getLineWidth();
+ final int cornerRadius = 6;
+ g.setLineWidth(1f);
+ g.setColor((idx == -1) ? highlightColor : backgroundColor);
+ g.fillRoundRect(x, y, width, baseHeight, cornerRadius);
+ g.setColor(borderColor);
+ g.drawRoundRect(x, y, width, baseHeight, cornerRadius);
+ if (expanded || t >= 0.0001) {
+ float oldBackgroundAlpha = backgroundColor.a;
+ backgroundColor.a *= t;
+ g.setColor(backgroundColor);
+ g.fillRoundRect(x, y + offsetY, width, (height - offsetY) * t, cornerRadius);
+ backgroundColor.a = oldBackgroundAlpha;
+ }
+ if (idx >= 0 && t >= 0.9999) {
+ g.setColor(highlightColor);
+ float yPos = y + offsetY + (offsetY * idx);
+ int yOff = 0, hOff = 0;
+ if (idx == 0 || idx == items.length - 1) {
+ g.fillRoundRect(x, yPos, width, offsetY, cornerRadius);
+ if (idx == 0)
+ yOff = cornerRadius;
+ hOff = cornerRadius;
+ }
+ g.fillRect(x, yPos + yOff, width, offsetY - hOff);
+ }
+ g.setColor(oldGColor);
+ g.setLineWidth(oldLineWidth);
+
+ // text
+ chevronDown.draw(x + width - chevronDown.getWidth() - width * CHEVRON_X, y + (baseHeight - chevronDown.getHeight()) / 2f, chevronDownColor);
+ fontNormal.drawString(x + (width * 0.03f), y + (fontNormal.getPaddingTop() + fontNormal.getPaddingBottom()) / 2f, itemNames[itemIndex], textColor);
+ float oldTextAlpha = textColor.a;
+ textColor.a *= t;
+ if (expanded || t >= 0.0001) {
+ for (int i = 0; i < itemNames.length; i++) {
+ Font f = (i == itemIndex) ? fontSelected : fontNormal;
+ if (i == idx && t >= 0.999)
+ chevronRight.draw(x, y + offsetY + (offsetY * i) + (offsetY - chevronRight.getHeight()) / 2f, chevronRightColor);
+ f.drawString(x + chevronRight.getWidth(), y + offsetY + (offsetY * i * t), itemNames[i], textColor);
+ }
+ }
+ textColor.a = oldTextAlpha;
+ }
+
+ /**
+ * Returns the index of the item at the given location, -1 for the base item,
+ * and -2 if there is no item at the location.
+ * @param cx the x coordinate
+ * @param cy the y coordinate
+ */
+ private int getIndexAt(float cx, float cy) {
+ if (!contains(cx, cy))
+ return -2;
+ if (cy <= y + baseHeight)
+ return -1;
+ if (!expanded)
+ return -2;
+ return (int) ((cy - (y + offsetY)) / offsetY);
+ }
+
+ /**
+ * Resets the menu state.
+ */
+ public void reset() {
+ this.expanded = false;
+ this.lastUpdateTime = 0;
+ expandProgress.setTime(0);
+ }
+
+ @Override
+ public void mousePressed(int button, int x, int y) {
+ if (!active)
+ return;
+
+ if (button == Input.MOUSE_MIDDLE_BUTTON)
+ return;
+
+ int idx = getIndexAt(x, y);
+ if (idx == -2) {
+ this.expanded = false;
+ return;
+ }
+ if (!menuClicked(idx))
+ return;
+ this.expanded = (idx == -1) ? !expanded : false;
+ if (idx >= 0 && itemIndex != idx) {
+ this.itemIndex = idx;
+ itemSelected(idx, items[idx]);
+ }
+ consumeEvent();
+ }
+
+ /**
+ * Notification that a new item was selected (via override).
+ * @param index the index of the item selected
+ * @param item the item selected
+ */
+ public void itemSelected(int index, E item) {}
+
+ /**
+ * Notification that the menu was clicked (via override).
+ * @param index the index of the item clicked, or -1 for the base item
+ * @return true to process the click, or false to block/intercept it
+ */
+ public boolean menuClicked(int index) { return true; }
+
+ @Override
+ public void setFocus(boolean focus) { /* does not currently use the "focus" concept */ }
+
+ @Override
+ public void mouseReleased(int button, int x, int y) { /* does not currently use the "focus" concept */ }
+
+ /**
+ * Selects the item at the given index.
+ * @param index the list item index
+ * @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)
+ throw new IllegalArgumentException();
+ this.itemIndex = index;
+ }
+
+ /**
+ * Returns the index of the selected item.
+ */
+ public int getSelectedIndex() { return itemIndex; }
+
+ /**
+ * Returns the selected item.
+ */
+ public E getSelectedItem() { return items[itemIndex]; }
+
+ /**
+ * Returns the item at the given index.
+ * @param index the list item index
+ */
+ public E getItemAt(int index) { return items[index]; }
+
+ /**
+ * Returns the number of items in the list.
+ */
+ public int getItemCount() { return items.length; }
+
+ /** Sets the text color. */
+ public void setTextColor(Color c) { this.textColor = c; }
+
+ /** Sets the background color. */
+ public void setBackgroundColor(Color c) { this.backgroundColor = c; }
+
+ /** Sets the highlight color. */
+ public void setHighlightColor(Color c) { this.highlightColor = c; }
+
+ /** Sets the border color. */
+ public void setBorderColor(Color c) { this.borderColor = c; }
+
+ /** Sets the down chevron color. */
+ public void setChevronDownColor(Color c) { this.chevronDownColor = c; }
+
+ /** Sets the right chevron color. */
+ public void setChevronRightColor(Color c) { this.chevronRightColor = c; }
+}
diff --git a/src/itdelatrisu/opsu/ui/Fonts.java b/src/itdelatrisu/opsu/ui/Fonts.java
new file mode 100644
index 00000000..14ecbdb0
--- /dev/null
+++ b/src/itdelatrisu/opsu/ui/Fonts.java
@@ -0,0 +1,156 @@
+/*
+ * 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;
+
+import itdelatrisu.opsu.GameImage;
+import itdelatrisu.opsu.Options;
+
+import java.awt.Font;
+import java.awt.FontFormatException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+import org.newdawn.slick.SlickException;
+import org.newdawn.slick.UnicodeFont;
+import org.newdawn.slick.font.effects.ColorEffect;
+import org.newdawn.slick.font.effects.Effect;
+import org.newdawn.slick.util.Log;
+import org.newdawn.slick.util.ResourceLoader;
+
+/**
+ * Fonts used for drawing.
+ */
+public class Fonts {
+ public static UnicodeFont DEFAULT, BOLD, XLARGE, LARGE, MEDIUM, MEDIUMBOLD, SMALL;
+
+ /** Set of all Unicode strings already loaded per font. */
+ private static HashMap> loadedGlyphs = new HashMap>();
+
+ // This class should not be instantiated.
+ private Fonts() {}
+
+ /**
+ * Initializes all fonts.
+ * @throws SlickException if ASCII glyphs could not be loaded
+ * @throws FontFormatException if any font stream data does not contain the required font tables
+ * @throws IOException if a font stream cannot be completely read
+ */
+ public static void init() throws SlickException, FontFormatException, IOException {
+ float fontBase = 12f * GameImage.getUIscale();
+ Font javaFont = Font.createFont(Font.TRUETYPE_FONT, ResourceLoader.getResourceAsStream(Options.FONT_NAME));
+ Font font = javaFont.deriveFont(Font.PLAIN, (int) (fontBase * 4 / 3));
+ DEFAULT = new UnicodeFont(font);
+ BOLD = new UnicodeFont(font.deriveFont(Font.BOLD));
+ XLARGE = new UnicodeFont(font.deriveFont(fontBase * 3));
+ LARGE = new UnicodeFont(font.deriveFont(fontBase * 2));
+ MEDIUM = new UnicodeFont(font.deriveFont(fontBase * 3 / 2));
+ MEDIUMBOLD = new UnicodeFont(font.deriveFont(Font.BOLD, fontBase * 3 / 2));
+ SMALL = new UnicodeFont(font.deriveFont(fontBase));
+ ColorEffect colorEffect = new ColorEffect();
+ loadFont(DEFAULT, colorEffect);
+ loadFont(BOLD, colorEffect);
+ loadFont(XLARGE, colorEffect);
+ loadFont(LARGE, colorEffect);
+ loadFont(MEDIUM, colorEffect);
+ loadFont(MEDIUMBOLD, colorEffect);
+ loadFont(SMALL, colorEffect);
+ }
+
+ /**
+ * Loads a Unicode font and its ASCII glyphs.
+ * @param font the font to load
+ * @param effect the font effect
+ * @throws SlickException if the glyphs could not be loaded
+ */
+ @SuppressWarnings("unchecked")
+ private static void loadFont(UnicodeFont font, Effect effect) throws SlickException {
+ font.addAsciiGlyphs();
+ font.getEffects().add(effect);
+ font.loadGlyphs();
+ }
+
+ /**
+ * Adds and loads glyphs for a font.
+ * @param font the font to add the glyphs to
+ * @param s the string containing the glyphs to load
+ */
+ public static void loadGlyphs(UnicodeFont font, String s) {
+ if (s == null || s.isEmpty())
+ return;
+
+ // get set of added strings
+ HashSet set = loadedGlyphs.get(font);
+ if (set == null) {
+ set = new HashSet();
+ loadedGlyphs.put(font, set);
+ } else if (set.contains(s))
+ return; // string already in set
+
+ // load glyphs
+ font.addGlyphs(s);
+ set.add(s);
+ try {
+ font.loadGlyphs();
+ } catch (SlickException e) {
+ Log.warn(String.format("Failed to load glyphs for string '%s'.", s), e);
+ }
+ }
+
+ /**
+ * Wraps the given string into a list of split lines based on the width.
+ * @param font the font used to draw the string
+ * @param text the text to split
+ * @param width the maximum width of a line
+ * @return the list of split strings
+ * @author davedes (http://slick.ninjacave.com/forum/viewtopic.php?t=3778)
+ */
+ public static List wrap(org.newdawn.slick.Font font, String text, int width) {
+ List list = new ArrayList();
+ String str = text;
+ String line = "";
+ int i = 0;
+ int lastSpace = -1;
+ while (i < str.length()) {
+ char c = str.charAt(i);
+ if (Character.isWhitespace(c))
+ lastSpace = i;
+ String append = line + c;
+ if (font.getWidth(append) > width) {
+ int split = (lastSpace != -1) ? lastSpace : i;
+ int splitTrimmed = split;
+ if (lastSpace != -1 && split < str.length() - 1)
+ splitTrimmed++;
+ list.add(str.substring(0, split));
+ str = str.substring(splitTrimmed);
+ line = "";
+ i = 0;
+ lastSpace = -1;
+ } else {
+ line = append;
+ i++;
+ }
+ }
+ if (str.length() != 0)
+ list.add(str);
+ return list;
+ }
+}
diff --git a/src/itdelatrisu/opsu/ui/MenuButton.java b/src/itdelatrisu/opsu/ui/MenuButton.java
index 7370d0bc..1aa524dc 100644
--- a/src/itdelatrisu/opsu/ui/MenuButton.java
+++ b/src/itdelatrisu/opsu/ui/MenuButton.java
@@ -19,6 +19,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 +65,26 @@ 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;
+
+ /** Whether the animation is advancing forwards (if advancing automatically). */
+ private boolean autoAnimationForward = true;
+
+ /** 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 +92,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.
@@ -126,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; }
@@ -192,15 +214,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 +239,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 +290,63 @@ 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);
+ autoAnimationForward = true;
}
/**
* 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;
+ autoAnimationForward = true;
+ }
+
+ /**
+ * 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 +354,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 +429,53 @@ 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);
+ }
+
+ /**
+ * Automatically advances the hover animation in a loop.
+ * @param delta the delta interval
+ * @param reverseAtEnd whether to reverse or restart the animation upon reaching the end
+ */
+ public void autoHoverUpdate(int delta, boolean reverseAtEnd) {
+ if (hoverEffect == 0)
+ return;
+
+ int time = ((hoverEffect & EFFECT_EXPAND) > 0) ? scale.getTime() :
+ ((hoverEffect & EFFECT_FADE) > 0) ? alpha.getTime() :
+ ((hoverEffect & EFFECT_ROTATE) > 0) ? angle.getTime() : -1;
+ if (time == -1)
+ return;
+
+ int d = delta * (autoAnimationForward ? 1 : -1);
+ if (Utils.clamp(time + d, 0, animationDuration) == time) {
+ if (reverseAtEnd)
+ autoAnimationForward = !autoAnimationForward;
+ else {
+ if ((hoverEffect & EFFECT_EXPAND) > 0)
+ scale.setTime(0);
+ if ((hoverEffect & EFFECT_FADE) > 0)
+ alpha.setTime(0);
+ if ((hoverEffect & EFFECT_ROTATE) > 0)
+ angle.setTime(0);
}
}
+
+ hoverUpdate(delta, autoAnimationForward);
}
/**
@@ -422,10 +488,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 +502,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/StarStream.java b/src/itdelatrisu/opsu/ui/StarStream.java
new file mode 100644
index 00000000..ec42c2aa
--- /dev/null
+++ b/src/itdelatrisu/opsu/ui/StarStream.java
@@ -0,0 +1,164 @@
+/*
+ * 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;
+
+import itdelatrisu.opsu.GameImage;
+import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.ui.animations.AnimatedValue;
+import itdelatrisu.opsu.ui.animations.AnimationEquation;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Random;
+
+import org.newdawn.slick.Image;
+
+/**
+ * Horizontal star stream.
+ */
+public class StarStream {
+ /** The container dimensions. */
+ private final int containerWidth, containerHeight;
+
+ /** The star image. */
+ private final Image starImg;
+
+ /** The current list of stars. */
+ private final List stars;
+
+ /** The maximum number of stars to draw at once. */
+ private static final int MAX_STARS = 20;
+
+ /** Random number generator instance. */
+ private final Random random;
+
+ /** Contains data for a single star. */
+ private class Star {
+ /** The star animation progress. */
+ private final AnimatedValue animatedValue;
+
+ /** The star properties. */
+ private final int distance, yOffset, angle;
+
+ /**
+ * Creates a star with the given properties.
+ * @param duration the time, in milliseconds, to show the star
+ * @param distance the distance for the star to travel in {@code duration}
+ * @param yOffset the vertical offset from the center of the container
+ * @param angle the rotation angle
+ * @param eqn the animation equation to use
+ */
+ public Star(int duration, int distance, int yOffset, int angle, AnimationEquation eqn) {
+ this.animatedValue = new AnimatedValue(duration, 0f, 1f, eqn);
+ this.distance = distance;
+ this.yOffset = yOffset;
+ this.angle = angle;
+ }
+
+ /**
+ * Draws the star.
+ */
+ public void draw() {
+ float t = animatedValue.getValue();
+ starImg.setImageColor(1f, 1f, 1f, Math.min((1 - t) * 5f, 1f));
+ starImg.drawEmbedded(
+ containerWidth - (distance * t), ((containerHeight - starImg.getHeight()) / 2) + yOffset,
+ starImg.getWidth(), starImg.getHeight(), angle);
+ }
+
+ /**
+ * 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) { return animatedValue.update(delta); }
+ }
+
+ /**
+ * Initializes the star stream.
+ * @param width the container width
+ * @param height the container height
+ */
+ public StarStream(int width, int height) {
+ this.containerWidth = width;
+ this.containerHeight = height;
+ this.starImg = GameImage.STAR2.getImage().copy();
+ this.stars = new ArrayList();
+ this.random = new Random();
+ }
+
+ /**
+ * Draws the star stream.
+ */
+ public void draw() {
+ if (stars.isEmpty())
+ return;
+
+ starImg.startUse();
+ for (Star star : stars)
+ star.draw();
+ starImg.endUse();
+ }
+
+ /**
+ * Updates the stars in the stream by a delta interval.
+ * @param delta the delta interval since the last call
+ */
+ public void update(int delta) {
+ // update current stars
+ Iterator iter = stars.iterator();
+ while (iter.hasNext()) {
+ Star star = iter.next();
+ if (!star.update(delta))
+ iter.remove();
+ }
+
+ // create new stars
+ for (int i = stars.size(); i < MAX_STARS; i++) {
+ if (Math.random() < ((i < 5) ? 0.25 : 0.66))
+ break;
+
+ // generate star properties
+ float distanceRatio = Utils.clamp((float) getGaussian(0.65, 0.25), 0.2f, 0.925f);
+ int distance = (int) (containerWidth * distanceRatio);
+ int duration = (int) (distanceRatio * getGaussian(1300, 300));
+ int yOffset = (int) getGaussian(0, containerHeight / 20);
+ int angle = (int) getGaussian(0, 22.5);
+ AnimationEquation eqn = random.nextBoolean() ? AnimationEquation.IN_OUT_QUAD : AnimationEquation.OUT_QUAD;
+
+ stars.add(new Star(duration, distance, angle, yOffset, eqn));
+ }
+ }
+
+ /**
+ * Clears the stars currently in the stream.
+ */
+ public void clear() { stars.clear(); }
+
+ /**
+ * Returns the next pseudorandom, Gaussian ("normally") distributed {@code double} value
+ * with the given mean and standard deviation.
+ * @param mean the mean
+ * @param stdDev the standard deviation
+ */
+ private double getGaussian(double mean, double stdDev) {
+ return mean + random.nextGaussian() * stdDev;
+ }
+}
diff --git a/src/itdelatrisu/opsu/ui/UI.java b/src/itdelatrisu/opsu/ui/UI.java
index 1f132618..b58b7002 100644
--- a/src/itdelatrisu/opsu/ui/UI.java
+++ b/src/itdelatrisu/opsu/ui/UI.java
@@ -21,11 +21,13 @@ package itdelatrisu.opsu.ui;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.beatmap.BeatmapParser;
+import itdelatrisu.opsu.beatmap.OszUnpacker;
import itdelatrisu.opsu.replay.ReplayImporter;
+import itdelatrisu.opsu.ui.animations.AnimatedValue;
+import itdelatrisu.opsu.ui.animations.AnimationEquation;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
@@ -36,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;
/**
@@ -62,7 +63,7 @@ public class UI {
private static int barNotifTimer = -1;
/** Duration, in milliseconds, to display bar notifications. */
- private static final int BAR_NOTIFICATION_TIME = 1250;
+ private static final int BAR_NOTIFICATION_TIME = 1500;
/** The current tooltip. */
private static String tooltip;
@@ -70,11 +71,8 @@ public class UI {
/** Whether or not to check the current tooltip for line breaks. */
private static boolean tooltipNewlines;
- /** The current tooltip timer. */
- private static int tooltipTimer = -1;
-
- /** Duration, in milliseconds, to fade tooltips. */
- private static final int TOOLTIP_FADE_TIME = 200;
+ /** The alpha level of the current tooltip (if any). */
+ private static AnimatedValue tooltipAlpha = new AnimatedValue(200, 0f, 1f, AnimationEquation.LINEAR);
// game-related variables
private static GameContainer container;
@@ -87,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();
@@ -106,6 +102,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);
}
@@ -117,12 +115,11 @@ public class UI {
cursor.update(delta);
updateVolumeDisplay(delta);
updateBarNotification(delta);
- if (tooltipTimer > 0)
- tooltipTimer -= delta;
+ tooltipAlpha.update(-delta);
}
/**
- * 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) {
@@ -134,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
@@ -178,18 +175,18 @@ public class UI {
*/
public static void drawTab(float x, float y, String text, boolean selected, boolean isHover) {
Image tabImage = GameImage.MENU_TAB.getImage();
- float tabTextX = x - (Utils.FONT_MEDIUM.getWidth(text) / 2);
+ float tabTextX = x - (Fonts.MEDIUM.getWidth(text) / 2);
float tabTextY = y - (tabImage.getHeight() / 2);
Color filter, textColor;
if (selected) {
filter = Color.white;
textColor = Color.black;
} else {
- filter = (isHover) ? Utils.COLOR_RED_HOVER : Color.red;
+ filter = (isHover) ? Colors.RED_HOVER : Color.red;
textColor = Color.white;
}
tabImage.drawCentered(x, y, filter);
- Utils.FONT_MEDIUM.drawString(tabTextX, tabTextY, text, textColor);
+ Fonts.MEDIUM.drawString(tabTextX, tabTextY, text, textColor);
}
/**
@@ -201,14 +198,14 @@ public class UI {
return;
String fps = String.format("%dFPS", container.getFPS());
- Utils.FONT_BOLD.drawString(
- container.getWidth() * 0.997f - Utils.FONT_BOLD.getWidth(fps),
- container.getHeight() * 0.997f - Utils.FONT_BOLD.getHeight(fps),
+ Fonts.BOLD.drawString(
+ container.getWidth() * 0.997f - Fonts.BOLD.getWidth(fps),
+ container.getHeight() * 0.997f - Fonts.BOLD.getHeight(fps),
Integer.toString(container.getFPS()), Color.white
);
- Utils.FONT_DEFAULT.drawString(
- container.getWidth() * 0.997f - Utils.FONT_BOLD.getWidth("FPS"),
- container.getHeight() * 0.997f - Utils.FONT_BOLD.getHeight("FPS"),
+ Fonts.DEFAULT.drawString(
+ container.getWidth() * 0.997f - Fonts.BOLD.getWidth("FPS"),
+ container.getHeight() * 0.997f - Fonts.BOLD.getHeight("FPS"),
"FPS", Color.white
);
}
@@ -263,7 +260,7 @@ public class UI {
*/
public static void changeVolume(int units) {
final float UNIT_OFFSET = 0.05f;
- Options.setMasterVolume(container, Utils.getBoundedValue(Options.getMasterVolume(), UNIT_OFFSET * units, 0f, 1f));
+ Options.setMasterVolume(container, Utils.clamp(Options.getMasterVolume() + (UNIT_OFFSET * units), 0f, 1f));
if (volumeDisplay == -1)
volumeDisplay = 0;
else if (volumeDisplay >= VOLUME_DISPLAY_TIME / 10)
@@ -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;
@@ -298,16 +296,16 @@ public class UI {
// draw loading info
float marginX = container.getWidth() * 0.02f, marginY = container.getHeight() * 0.02f;
float lineY = container.getHeight() - marginY;
- int lineOffsetY = Utils.FONT_MEDIUM.getLineHeight();
+ int lineOffsetY = Fonts.MEDIUM.getLineHeight();
if (Options.isLoadVerbose()) {
// verbose: display percentages and file names
- Utils.FONT_MEDIUM.drawString(
+ Fonts.MEDIUM.drawString(
marginX, lineY - (lineOffsetY * 2),
String.format("%s (%d%%)", text, progress), Color.white);
- Utils.FONT_MEDIUM.drawString(marginX, lineY - lineOffsetY, file, Color.white);
+ Fonts.MEDIUM.drawString(marginX, lineY - lineOffsetY, file, Color.white);
} else {
// draw loading bar
- Utils.FONT_MEDIUM.drawString(marginX, lineY - (lineOffsetY * 2), text, Color.white);
+ Fonts.MEDIUM.drawString(marginX, lineY - (lineOffsetY * 2), text, Color.white);
g.setColor(Color.white);
g.fillRoundRect(marginX, lineY - (lineOffsetY / 2f),
(container.getWidth() - (marginX * 2f)) * progress / 100f, lineOffsetY / 4f, 4
@@ -357,12 +355,7 @@ public class UI {
if (s != null) {
tooltip = s;
tooltipNewlines = newlines;
- if (tooltipTimer <= 0)
- tooltipTimer = delta;
- else
- tooltipTimer += delta * 2;
- if (tooltipTimer > TOOLTIP_FADE_TIME)
- tooltipTimer = TOOLTIP_FADE_TIME;
+ tooltipAlpha.update(delta * 2);
}
}
@@ -372,26 +365,26 @@ public class UI {
* @param g the graphics context
*/
public static void drawTooltip(Graphics g) {
- if (tooltipTimer <= 0 || tooltip == null)
+ if (tooltipAlpha.getTime() == 0 || tooltip == null)
return;
int containerWidth = container.getWidth(), containerHeight = container.getHeight();
int margin = containerWidth / 100, textMarginX = 2;
int offset = GameImage.CURSOR_MIDDLE.getImage().getWidth() / 2;
- int lineHeight = Utils.FONT_SMALL.getLineHeight();
+ int lineHeight = Fonts.SMALL.getLineHeight();
int textWidth = textMarginX * 2, textHeight = lineHeight;
if (tooltipNewlines) {
String[] lines = tooltip.split("\\n");
- int maxWidth = Utils.FONT_SMALL.getWidth(lines[0]);
+ int maxWidth = Fonts.SMALL.getWidth(lines[0]);
for (int i = 1; i < lines.length; i++) {
- int w = Utils.FONT_SMALL.getWidth(lines[i]);
+ int w = Fonts.SMALL.getWidth(lines[i]);
if (w > maxWidth)
maxWidth = w;
}
textWidth += maxWidth;
textHeight += lineHeight * (lines.length - 1);
} else
- textWidth += Utils.FONT_SMALL.getWidth(tooltip);
+ textWidth += Fonts.SMALL.getWidth(tooltip);
// get drawing coordinates
int x = input.getMouseX() + offset, y = input.getMouseY() + offset;
@@ -405,29 +398,29 @@ public class UI {
y = margin;
// draw tooltip text inside a filled rectangle
- float alpha = (float) tooltipTimer / TOOLTIP_FADE_TIME;
- float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
- Utils.COLOR_BLACK_ALPHA.a = alpha;
- g.setColor(Utils.COLOR_BLACK_ALPHA);
- Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
+ float alpha = tooltipAlpha.getValue();
+ float oldAlpha = Colors.BLACK_ALPHA.a;
+ Colors.BLACK_ALPHA.a = alpha;
+ g.setColor(Colors.BLACK_ALPHA);
+ Colors.BLACK_ALPHA.a = oldAlpha;
g.fillRect(x, y, textWidth, textHeight);
- oldAlpha = Utils.COLOR_DARK_GRAY.a;
- Utils.COLOR_DARK_GRAY.a = alpha;
- g.setColor(Utils.COLOR_DARK_GRAY);
+ oldAlpha = Colors.DARK_GRAY.a;
+ Colors.DARK_GRAY.a = alpha;
+ g.setColor(Colors.DARK_GRAY);
g.setLineWidth(1);
g.drawRect(x, y, textWidth, textHeight);
- Utils.COLOR_DARK_GRAY.a = oldAlpha;
- oldAlpha = Utils.COLOR_WHITE_ALPHA.a;
- Utils.COLOR_WHITE_ALPHA.a = alpha;
- Utils.FONT_SMALL.drawString(x + textMarginX, y, tooltip, Utils.COLOR_WHITE_ALPHA);
- Utils.COLOR_WHITE_ALPHA.a = oldAlpha;
+ Colors.DARK_GRAY.a = oldAlpha;
+ oldAlpha = Colors.WHITE_ALPHA.a;
+ Colors.WHITE_ALPHA.a = alpha;
+ Fonts.SMALL.drawString(x + textMarginX, y, tooltip, Colors.WHITE_ALPHA);
+ Colors.WHITE_ALPHA.a = oldAlpha;
}
/**
* Resets the tooltip.
*/
public static void resetTooltip() {
- tooltipTimer = -1;
+ tooltipAlpha.setTime(0);
tooltip = null;
}
@@ -475,18 +468,18 @@ public class UI {
if (barNotifTimer >= BAR_NOTIFICATION_TIME * 0.9f)
alpha -= 1 - ((BAR_NOTIFICATION_TIME - barNotifTimer) / (BAR_NOTIFICATION_TIME * 0.1f));
int midX = container.getWidth() / 2, midY = container.getHeight() / 2;
- float barHeight = Utils.FONT_LARGE.getLineHeight() * (1f + 0.6f * Math.min(barNotifTimer * 15f / BAR_NOTIFICATION_TIME, 1f));
- float oldAlphaB = Utils.COLOR_BLACK_ALPHA.a, oldAlphaW = Utils.COLOR_WHITE_ALPHA.a;
- Utils.COLOR_BLACK_ALPHA.a *= alpha;
- Utils.COLOR_WHITE_ALPHA.a = alpha;
- g.setColor(Utils.COLOR_BLACK_ALPHA);
+ float barHeight = Fonts.LARGE.getLineHeight() * (1f + 0.6f * Math.min(barNotifTimer * 15f / BAR_NOTIFICATION_TIME, 1f));
+ float oldAlphaB = Colors.BLACK_ALPHA.a, oldAlphaW = Colors.WHITE_ALPHA.a;
+ Colors.BLACK_ALPHA.a *= alpha;
+ Colors.WHITE_ALPHA.a = alpha;
+ g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, midY - barHeight / 2f, container.getWidth(), barHeight);
- Utils.FONT_LARGE.drawString(
- midX - Utils.FONT_LARGE.getWidth(barNotif) / 2f,
- midY - Utils.FONT_LARGE.getLineHeight() / 2.2f,
- barNotif, Utils.COLOR_WHITE_ALPHA);
- Utils.COLOR_BLACK_ALPHA.a = oldAlphaB;
- Utils.COLOR_WHITE_ALPHA.a = oldAlphaW;
+ Fonts.LARGE.drawString(
+ midX - Fonts.LARGE.getWidth(barNotif) / 2f,
+ midY - Fonts.LARGE.getLineHeight() / 2.2f,
+ barNotif, Colors.WHITE_ALPHA);
+ Colors.BLACK_ALPHA.a = oldAlphaB;
+ Colors.WHITE_ALPHA.a = oldAlphaW;
}
/**
diff --git a/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java b/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java
new file mode 100644
index 00000000..da8bd8d5
--- /dev/null
+++ b/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java
@@ -0,0 +1,134 @@
+/*
+ * 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();
+ }
+
+ /**
+ * Returns the total animation duration, in milliseconds.
+ */
+ public int getDuration() { return duration; }
+
+ /**
+ * 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();
+ }
+ }
+
+ /**
+ * Returns the animation equation being used.
+ */
+ public AnimationEquation getEquation() { return eqn; }
+
+ /**
+ * 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.clamp(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..3953b436
--- /dev/null
+++ b/src/itdelatrisu/opsu/ui/animations/AnimationEquation.java
@@ -0,0 +1,308 @@
+/*
+ * 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);
+}
diff --git a/src/org/newdawn/slick/Image.java b/src/org/newdawn/slick/Image.java
index 1737a808..8989fb92 100644
--- a/src/org/newdawn/slick/Image.java
+++ b/src/org/newdawn/slick/Image.java
@@ -38,6 +38,7 @@ import org.newdawn.slick.opengl.TextureImpl;
import org.newdawn.slick.opengl.pbuffer.GraphicsFactory;
import org.newdawn.slick.opengl.renderer.Renderer;
import org.newdawn.slick.opengl.renderer.SGL;
+import org.newdawn.slick.util.FastTrig;
import org.newdawn.slick.util.Log;
/**
@@ -104,7 +105,7 @@ public class Image implements Renderable {
/** The colours for each of the corners */
protected Color[] corners;
/** The OpenGL max filter */
- private int filter = SGL.GL_LINEAR;
+ private int filter = FILTER_LINEAR;
/** True if the image should be flipped vertically */
private boolean flipped;
@@ -562,7 +563,7 @@ public class Image implements Renderable {
* @param y The y coordinate to place the image's center at
*/
public void drawCentered(float x, float y) {
- draw(x-(getWidth()/2),y-(getHeight()/2));
+ draw(x - (getWidth() / 2f), y - (getHeight() / 2f));
}
/**
@@ -595,11 +596,22 @@ 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);
}
+ /**
+ * Draw this image as part of a collection of images
+ *
+ * @param x The x location to draw the image at
+ * @param y The y location to draw the image at
+ */
+ public void drawEmbedded(float x,float y) {
+ drawEmbedded(x, y, getWidth(), getHeight());
+ }
+
/**
* Draw this image as part of a collection of images
*
@@ -719,6 +731,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 +810,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) {
@@ -1159,6 +1173,83 @@ public class Image implements Renderable {
newTextureOffsetY);
GL.glVertex3f((x + mywidth),y, 0.0f);
}
+
+ /**
+ * Unlike the other drawEmbedded methods, this allows for the embedded image
+ * to be rotated. This is done by applying a rotation transform to each
+ * vertex of the image. This ignores getRotation but depends on the
+ * center x/y (scaled accordingly to the new width/height).
+ *
+ * @param x the x to render the image at
+ * @param y the y to render the image at
+ * @param width the new width to render the image
+ * @param height the new height to render the image
+ * @param rotation the rotation to render the image, using getCenterOfRotationX/Y
+ *
+ * @author davedes
+ */
+ public void drawEmbedded(float x, float y, float width, float height, float rotation) {
+ if (rotation==0) {
+ drawEmbedded(x, y, width, height);
+ return;
+ }
+ init();
+ float scaleX = width/this.width;
+ float scaleY = height/this.height;
+
+ float cx = getCenterOfRotationX()*scaleX;
+ float cy = getCenterOfRotationY()*scaleY;
+
+ float p1x = -cx;
+ float p1y = -cy;
+ float p2x = width - cx;
+ float p2y = -cy;
+ float p3x = width - cx;
+ float p3y = height - cy;
+ float p4x = -cx;
+ float p4y = height - cy;
+
+ double rad = Math.toRadians(rotation);
+ final float cos = (float) FastTrig.cos(rad);
+ final float sin = (float) FastTrig.sin(rad);
+
+ float tx = getTextureOffsetX();
+ float ty = getTextureOffsetY();
+ float tw = getTextureWidth();
+ float th = getTextureHeight();
+
+ float x1 = (cos * p1x - sin * p1y) + cx; // TOP LEFT
+ float y1 = (sin * p1x + cos * p1y) + cy;
+ float x2 = (cos * p4x - sin * p4y) + cx; // BOTTOM LEFT
+ float y2 = (sin * p4x + cos * p4y) + cy;
+ float x3 = (cos * p3x - sin * p3y) + cx; // BOTTOM RIGHT
+ float y3 = (sin * p3x + cos * p3y) + cy;
+ float x4 = (cos * p2x - sin * p2y) + cx; // TOP RIGHT
+ float y4 = (sin * p2x + cos * p2y) + cy;
+ if (corners == null) {
+ GL.glTexCoord2f(tx, ty);
+ GL.glVertex3f(x+x1, y+y1, 0);
+ GL.glTexCoord2f(tx, ty + th);
+ GL.glVertex3f(x+x2, y+y2, 0);
+ GL.glTexCoord2f(tx + tw, ty + th);
+ GL.glVertex3f(x+x3, y+y3, 0);
+ GL.glTexCoord2f(tx + tw, ty);
+ GL.glVertex3f(x+x4, y+y4, 0);
+ } else {
+ corners[TOP_LEFT].bind();
+ GL.glTexCoord2f(tx, ty);
+ GL.glVertex3f(x+x1, y+y1, 0);
+ corners[BOTTOM_LEFT].bind();
+ GL.glTexCoord2f(tx, ty + th);
+ GL.glVertex3f(x+x2, y+y2, 0);
+ corners[BOTTOM_RIGHT].bind();
+ GL.glTexCoord2f(tx + tw, ty + th);
+ GL.glVertex3f(x+x3, y+y3, 0);
+ corners[TOP_RIGHT].bind();
+ GL.glTexCoord2f(tx + tw, ty);
+ GL.glVertex3f(x+x4, y+y4, 0);
+ }
+ }
/**
* Draw the image in a warper rectangle. The effects this can
@@ -1457,7 +1548,7 @@ public class Image implements Renderable {
if (isDestroyed()) {
return;
}
-
+ flushPixelData();
destroyed = true;
texture.release();
GraphicsFactory.releaseGraphicsForImage(this);
diff --git a/src/org/newdawn/slick/Input.java b/src/org/newdawn/slick/Input.java
index 1066ca34..696fe1dc 100644
--- a/src/org/newdawn/slick/Input.java
+++ b/src/org/newdawn/slick/Input.java
@@ -1078,10 +1078,10 @@ public class Input {
throw new SlickException("Unable to create controller - no jinput found - add jinput.jar to your classpath");
}
throw new SlickException("Unable to create controllers");
- } catch (NoClassDefFoundError e) {
+ } catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
// forget it, no jinput availble
}
- }
+ }
/**
* Notification from an event handle that an event has been consumed
@@ -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;
diff --git a/src/org/newdawn/slick/openal/Mp3InputStream.java b/src/org/newdawn/slick/openal/Mp3InputStream.java
index 8d8cb6df..19dd105f 100644
--- a/src/org/newdawn/slick/openal/Mp3InputStream.java
+++ b/src/org/newdawn/slick/openal/Mp3InputStream.java
@@ -76,8 +76,9 @@ public class Mp3InputStream extends InputStream implements AudioInputStream {
/**
* Create a new stream to decode MP3 data.
* @param input the input stream from which to read the MP3 file
+ * @throws IOException failure to read the header from the input stream
*/
- public Mp3InputStream(InputStream input) {
+ public Mp3InputStream(InputStream input) throws IOException {
decoder = new Decoder();
bitstream = new Bitstream(input);
try {
@@ -85,6 +86,10 @@ public class Mp3InputStream extends InputStream implements AudioInputStream {
} catch (BitstreamException e) {
Log.error(e);
}
+ if (header == null) {
+ close();
+ throw new IOException("Failed to read header from MP3 input stream.");
+ }
channels = (header.mode() == Header.SINGLE_CHANNEL) ? 1 : 2;
sampleRate = header.frequency();
diff --git a/src/org/newdawn/slick/openal/OpenALStreamPlayer.java b/src/org/newdawn/slick/openal/OpenALStreamPlayer.java
index a6e6c3f4..cc13a3e5 100644
--- a/src/org/newdawn/slick/openal/OpenALStreamPlayer.java
+++ b/src/org/newdawn/slick/openal/OpenALStreamPlayer.java
@@ -152,16 +152,30 @@ public class OpenALStreamPlayer {
if (url != null) {
audio = new OggInputStream(url.openStream());
} else {
- if (ref.toLowerCase().endsWith(".mp3"))
- audio = new Mp3InputStream(ResourceLoader.getResourceAsStream(ref));
- else
- audio = new OggInputStream(ResourceLoader.getResourceAsStream(ref));
-
- if (audio.getRate() == 0 && audio.getChannels() == 0) {
- if (ref.toLowerCase().endsWith(".mp3"))
- audio = new OggInputStream(ResourceLoader.getResourceAsStream(ref));
- else
+ if (ref.toLowerCase().endsWith(".mp3")) {
+ try {
audio = new Mp3InputStream(ResourceLoader.getResourceAsStream(ref));
+ } catch (IOException e) {
+ // invalid MP3: check if file is actually OGG
+ try {
+ audio = new OggInputStream(ResourceLoader.getResourceAsStream(ref));
+ } catch (IOException e1) {
+ throw e; // invalid OGG: re-throw original MP3 exception
+ }
+ if (audio.getRate() == 0 && audio.getChannels() == 0)
+ throw e; // likely not OGG: re-throw original MP3 exception
+ }
+ } else {
+ audio = new OggInputStream(ResourceLoader.getResourceAsStream(ref));
+ if (audio.getRate() == 0 && audio.getChannels() == 0) {
+ // invalid OGG: check if file is actually MP3
+ AudioInputStream audioOGG = audio;
+ try {
+ audio = new Mp3InputStream(ResourceLoader.getResourceAsStream(ref));
+ } catch (IOException e) {
+ audio = audioOGG; // invalid MP3: keep OGG stream
+ }
+ }
}
}
diff --git a/tools/JarSplicePlus.jar b/tools/JarSplicePlus.jar
deleted file mode 100644
index b6dacdae..00000000
Binary files a/tools/JarSplicePlus.jar and /dev/null differ