diff --git a/CREDITS.md b/CREDITS.md
index 9f622802..6cee6517 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -31,5 +31,6 @@ The following projects were referenced in creating opsu!:
Theme Song
----------
-The theme song is "On the Bach" by Jingle Punks, from the [YouTube Audio Library]
-(https://www.youtube.com/audiolibrary/music).
+Rainbows - Kevin MacLeod (incompetech.com)
+Licensed under Creative Commons: By Attribution 3.0 License
+http://creativecommons.org/licenses/by/3.0/
diff --git a/res/theme.mp3 b/res/theme.mp3
new file mode 100644
index 00000000..b75ad2bf
Binary files /dev/null and b/res/theme.mp3 differ
diff --git a/res/theme.ogg b/res/theme.ogg
deleted file mode 100644
index 8057f70d..00000000
Binary files a/res/theme.ogg and /dev/null differ
diff --git a/src/itdelatrisu/opsu/Container.java b/src/itdelatrisu/opsu/Container.java
index 00de84dd..56be0ad6 100644
--- a/src/itdelatrisu/opsu/Container.java
+++ b/src/itdelatrisu/opsu/Container.java
@@ -21,7 +21,9 @@ package itdelatrisu.opsu;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.beatmap.Beatmap;
+import itdelatrisu.opsu.beatmap.BeatmapGroup;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
+import itdelatrisu.opsu.beatmap.BeatmapSortOrder;
import itdelatrisu.opsu.beatmap.BeatmapWatchService;
import itdelatrisu.opsu.downloads.DownloadList;
import itdelatrisu.opsu.downloads.Updater;
@@ -38,12 +40,8 @@ import org.newdawn.slick.opengl.InternalTextureLoader;
* AppGameContainer extension that sends critical errors to ErrorHandler.
*/
public class Container extends AppGameContainer {
- /** SlickException causing game failure. */
- protected SlickException e = null;
-
- private Exception anyException = null;
-
- public static Container instance;
+ /** Exception causing game failure. */
+ protected Exception e = null;
/**
* Create a new container wrapping a game
@@ -53,9 +51,19 @@ public class Container extends AppGameContainer {
*/
public Container(Game game) throws SlickException {
super(game);
- instance = this;
- width = this.getWidth();
- height = this.getHeight();
+ }
+
+ /**
+ * Create a new container wrapping a game
+ *
+ * @param game The game to be wrapped
+ * @param width The width of the display required
+ * @param height The height of the display required
+ * @param fullscreen True if we want fullscreen mode
+ * @throws SlickException Indicates a failure to initialise the display
+ */
+ public Container(Game game, int width, int height, boolean fullscreen) throws SlickException {
+ super(game, width, height, fullscreen);
}
@Override
@@ -66,20 +74,24 @@ public class Container extends AppGameContainer {
getDelta();
while (running())
gameLoop();
- } catch(Exception e) {
- anyException = e;
- } finally {
- // destroy the game container
- close_sub();
- destroy();
+ } catch (Exception e) {
+ this.e = e;
+ }
- if (anyException != null) {
- ErrorHandler.error("Something bad happend while playing", anyException, true);
- anyException = null;
- } else if (e != null) {
- ErrorHandler.error(null, e, true);
- e = null;
- }
+ // destroy the game container
+ try {
+ close_sub();
+ } catch (Exception e) {
+ if (this.e == null) // suppress if caused by a previous exception
+ this.e = e;
+ }
+ destroy();
+
+ // report any critical errors
+ if (e != null) {
+ ErrorHandler.error(null, e, true);
+ e = null;
+ forceExit = true;
}
if (forceExit) {
@@ -118,9 +130,7 @@ public class Container extends AppGameContainer {
Options.saveOptions();
// reset cursor
- if (UI.getCursor() != null) {
- UI.getCursor().reset();
- }
+ UI.getCursor().reset();
// destroy images
InternalTextureLoader.get().clear();
@@ -137,6 +147,8 @@ public class Container extends AppGameContainer {
SoundController.stopTrack();
// reset BeatmapSetList data
+ BeatmapGroup.set(BeatmapGroup.ALL);
+ BeatmapSortOrder.set(BeatmapSortOrder.TITLE);
if (BeatmapSetList.get() != null)
BeatmapSetList.get().reset();
diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java
index 231bdd54..bac8ca8e 100644
--- a/src/itdelatrisu/opsu/GameData.java
+++ b/src/itdelatrisu/opsu/GameData.java
@@ -141,23 +141,23 @@ public class GameData {
/** Hit result types. */
public static final int
- HIT_MISS = 0,
- HIT_50 = 1,
- HIT_100 = 2,
- HIT_300 = 3,
- HIT_100K = 4, // 100-Katu
- HIT_300K = 5, // 300-Katu
- HIT_300G = 6, // Geki
- HIT_SLIDER10 = 7,
- HIT_SLIDER30 = 8,
- HIT_MAX = 9, // not a hit result
- HIT_SLIDER_INITIAL = 10, // not a hit result
- HIT_SLIDER_REPEAT = 11; // not a hit result
+ HIT_MISS = 0,
+ HIT_50 = 1,
+ HIT_100 = 2,
+ HIT_300 = 3,
+ HIT_100K = 4, // 100-Katu
+ HIT_300K = 5, // 300-Katu
+ HIT_300G = 6, // Geki
+ HIT_SLIDER10 = 7,
+ HIT_SLIDER30 = 8,
+ HIT_MAX = 9, // not a hit result
+ HIT_SLIDER_REPEAT = 10, // not a hit result
+ HIT_ANIMATION_RESULT = 11; // not a hit result
- /** Hit result-related images (indexed by HIT_* constants). */
+ /** Hit result-related images (indexed by HIT_* constants to HIT_MAX). */
private Image[] hitResults;
- /** Counts of each hit result so far. */
+ /** Counts of each hit result so far (indexed by HIT_* constants to HIT_MAX). */
private int[] hitResultCount;
/** Total objects including slider hits/ticks (for determining Full Combo status). */
@@ -193,7 +193,7 @@ public class GameData {
/** Current x coordinate of the combo burst image (for sliding animation). */
private float comboBurstX;
- /** Time offsets for obtaining each hit result (indexed by HIT_* constants). */
+ /** Time offsets for obtaining each hit result (indexed by HIT_* constants to HIT_MAX). */
private int[] hitResultOffset;
/** List of hit result objects associated with hit objects. */
@@ -557,10 +557,11 @@ public class GameData {
* @param x the starting x coordinate
* @param y the y coordinate
* @param scale the scale to apply
+ * @param alpha the alpha level
* @param fixedsize the width to use for all symbols
* @param rightAlign align right (true) or left (false)
*/
- public void drawFixedSizeSymbolString(String str, float x, float y, float scale, float fixedsize, boolean rightAlign) {
+ public void drawFixedSizeSymbolString(String str, float x, float y, float scale, float alpha, float fixedsize, boolean rightAlign) {
char[] c = str.toCharArray();
float cx = x;
if (rightAlign) {
@@ -569,14 +570,18 @@ public class GameData {
if (scale != 1.0f)
digit = digit.getScaledCopy(scale);
cx -= fixedsize;
+ digit.setAlpha(alpha);
digit.draw(cx + (fixedsize - digit.getWidth()) / 2, y);
+ digit.setAlpha(1f);
}
} else {
for (int i = 0; i < c.length; i++) {
Image digit = getScoreSymbolImage(c[i]);
if (scale != 1.0f)
digit = digit.getScaledCopy(scale);
+ digit.setAlpha(alpha);
digit.draw(cx + (fixedsize - digit.getWidth()) / 2, y);
+ digit.setAlpha(1f);
cx += fixedsize;
}
}
@@ -589,9 +594,10 @@ public class GameData {
* @param g the graphics context
* @param breakPeriod if true, will not draw scorebar and combo elements, and will draw grade
* @param firstObject true if the first hit object's start time has not yet passed
+ * @param alpha the alpha level at which to render all elements (except the hit error bar)
*/
@SuppressWarnings("deprecation")
- public void drawGameElements(Graphics g, boolean breakPeriod, boolean firstObject) {
+ public void drawGameElements(Graphics g, boolean breakPeriod, boolean firstObject, float alpha) {
boolean relaxAutoPilot = (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive());
int margin = (int) (width * 0.008f);
float uiScale = GameImage.getUIscale();
@@ -599,14 +605,14 @@ public class GameData {
// score
if (!relaxAutoPilot)
drawFixedSizeSymbolString((scoreDisplay < 100000000) ? String.format("%08d", scoreDisplay) : Long.toString(scoreDisplay),
- width - margin, 0, 1.0f, getScoreSymbolImage('0').getWidth() - 2, true);
+ width - margin, 0, 1f, alpha, getScoreSymbolImage('0').getWidth() - 2, true);
// score percentage
int symbolHeight = getScoreSymbolImage('0').getHeight();
if (!relaxAutoPilot)
drawSymbolString(
String.format((scorePercentDisplay < 10f) ? "0%.2f%%" : "%.2f%%", scorePercentDisplay),
- width - margin, symbolHeight, 0.60f, 1f, true);
+ width - margin, symbolHeight, 0.60f, alpha, true);
// map progress circle
Beatmap beatmap = MusicController.getBeatmap();
@@ -620,23 +626,27 @@ public class GameData {
getScoreSymbolImage('%').getWidth()
) * 0.60f - circleDiameter);
if (!relaxAutoPilot) {
+ float oldWhiteAlpha = Colors.WHITE_ALPHA.a;
+ Colors.WHITE_ALPHA.a = alpha;
g.setAntiAlias(true);
g.setLineWidth(2f);
- g.setColor(Color.white);
+ g.setColor(Colors.WHITE_ALPHA);
g.drawOval(circleX, symbolHeight, circleDiameter, circleDiameter);
if (trackPosition > firstObjectTime) {
// map progress (white)
- g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter,
- -90, -90 + (int) (360f * (trackPosition - firstObjectTime) / (beatmap.endTime - firstObjectTime))
- );
+ float progress = Math.min((float) (trackPosition - firstObjectTime) / (beatmap.endTime - firstObjectTime), 1f);
+ g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter, -90, -90 + (int) (360f * progress));
} else {
// lead-in time (yellow)
+ float progress = (float) trackPosition / firstObjectTime;
+ float oldYellowAlpha = Colors.YELLOW_ALPHA.a;
+ Colors.YELLOW_ALPHA.a *= alpha;
g.setColor(Colors.YELLOW_ALPHA);
- g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter,
- -90 + (int) (360f * trackPosition / firstObjectTime), -90
- );
+ g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter, -90 + (int) (360f * progress), -90);
+ Colors.YELLOW_ALPHA.a = oldYellowAlpha;
}
g.setAntiAlias(false);
+ Colors.WHITE_ALPHA.a = oldWhiteAlpha;
}
// mod icons
@@ -646,10 +656,12 @@ public class GameData {
int modCount = 0;
for (GameMod mod : GameMod.VALUES_REVERSED) {
if (mod.isActive()) {
+ mod.getImage().setAlpha(alpha);
mod.getImage().draw(
modX - (modCount * (modWidth / 2f)),
symbolHeight + circleDiameter + 10
);
+ mod.getImage().setAlpha(1f);
modCount++;
}
}
@@ -697,8 +709,8 @@ public class GameData {
float tickWidth = 2 * uiScale;
for (HitErrorInfo info : hitErrorList) {
int time = info.time;
- float alpha = 1 - ((float) (trackPosition - time) / HIT_ERROR_FADE_TIME);
- white.a = alpha * hitErrorAlpha;
+ float tickAlpha = 1 - ((float) (trackPosition - time) / HIT_ERROR_FADE_TIME);
+ white.a = tickAlpha * hitErrorAlpha;
g.setColor(white);
g.fillRect((hitErrorX + info.timeDiff - 1) * uiScale, tickY, tickWidth, tickHeight);
}
@@ -721,9 +733,12 @@ public class GameData {
float colourX = 4 * uiScale, colourY = 15 * uiScale;
Image colourCropped = colour.getSubImage(0, 0, (int) (645 * uiScale * healthRatio), colour.getHeight());
- scorebar.setAlpha(1f);
+ scorebar.setAlpha(alpha);
scorebar.draw(0, 0);
+ scorebar.setAlpha(1f);
+ colourCropped.setAlpha(alpha);
colourCropped.draw(colourX, colourY);
+ colourCropped.setAlpha(1f);
Image ki = null;
if (health >= 50f)
@@ -734,7 +749,9 @@ public class GameData {
ki = GameImage.SCOREBAR_KI_DANGER2.getImage();
if (comboPopTime < COMBO_POP_TIME)
ki = ki.getScaledCopy(1f + (0.45f * (1f - (float) comboPopTime / COMBO_POP_TIME)));
+ ki.setAlpha(alpha);
ki.drawCentered(colourX + colourCropped.getWidth(), colourY);
+ ki.setAlpha(1f);
// combo burst
if (comboBurstIndex != -1 && comboBurstAlpha > 0f) {
@@ -750,8 +767,8 @@ public class GameData {
float comboPopFront = 1 + comboPop * 0.08f;
String comboString = String.format("%dx", combo);
if (comboPopTime != COMBO_POP_TIME)
- drawSymbolString(comboString, margin, height - margin - (symbolHeight * comboPopBack), comboPopBack, 0.5f, false);
- drawSymbolString(comboString, margin, height - margin - (symbolHeight * comboPopFront), comboPopFront, 1f, false);
+ drawSymbolString(comboString, margin, height - margin - (symbolHeight * comboPopBack), comboPopBack, 0.5f * alpha, false);
+ drawSymbolString(comboString, margin, height - margin - (symbolHeight * comboPopFront), comboPopFront, alpha, false);
}
} else if (!relaxAutoPilot) {
// grade
@@ -759,9 +776,9 @@ public class GameData {
if (grade != Grade.NULL) {
Image gradeImage = grade.getSmallImage();
float gradeScale = symbolHeight * 0.75f / gradeImage.getHeight();
- gradeImage.getScaledCopy(gradeScale).draw(
- circleX - gradeImage.getWidth(), symbolHeight
- );
+ gradeImage = gradeImage.getScaledCopy(gradeScale);
+ gradeImage.setAlpha(alpha);
+ gradeImage.draw(circleX - gradeImage.getWidth(), symbolHeight);
}
}
}
@@ -786,7 +803,7 @@ public class GameData {
drawFixedSizeSymbolString(
(score < 100000000) ? String.format("%08d", score) : Long.toString(score),
210 * uiScale, (rankingHeight + 50) * uiScale,
- scoreTextScale, getScoreSymbolImage('0').getWidth() * scoreTextScale - 2, false
+ scoreTextScale, 1f, getScoreSymbolImage('0').getWidth() * scoreTextScale - 2, false
);
// result counts
@@ -897,63 +914,17 @@ public class GameData {
lighting.drawCentered(hitResult.x, hitResult.y, hitResult.color);
}
- // hit animation
- if (Options.isHitAnimationEnabled() &&
- hitResult.result != HIT_MISS && (
- hitResult.hitResultType == null || // null => initial slider circle
- hitResult.hitResultType == HitObjectType.CIRCLE ||
- hitResult.hitResultType == HitObjectType.SLIDER_FIRST ||
- hitResult.hitResultType == HitObjectType.SLIDER_LAST)) {
- float progress = AnimationEquation.OUT_CUBIC.calc(
- (float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME) / HITCIRCLE_FADE_TIME);
- float scale = (!hitResult.expand) ? 1f : 1f + (HITCIRCLE_ANIM_SCALE - 1f) * progress;
- float alpha = 1f - progress;
-
- if (hitResult.result == HIT_SLIDER_REPEAT) {
- // repeats
- Image scaledRepeat = GameImage.REVERSEARROW.getImage().getScaledCopy(scale);
- scaledRepeat.setAlpha(alpha);
- float ang;
- if (hitResult.hitResultType == HitObjectType.SLIDER_FIRST) {
- ang = hitResult.curve.getStartAngle();
- } else {
- ang = hitResult.curve.getEndAngle();
- }
- scaledRepeat.rotate(ang);
- scaledRepeat.drawCentered(hitResult.x, hitResult.y, hitResult.color);
- }
- // "hidden" mod: circle and slider animations not drawn
- else if (!GameMod.HIDDEN.isActive()) {
- // slider curve
- if (hitResult.curve != null) {
- float oldWhiteAlpha = Colors.WHITE_FADE.a;
- float oldColorAlpha = hitResult.color.a;
- Colors.WHITE_FADE.a = alpha;
- hitResult.color.a = alpha;
- if (!Options.isShrinkingSliders()) {
- hitResult.curve.draw(hitResult.color);
- }
- Colors.WHITE_FADE.a = oldWhiteAlpha;
- hitResult.color.a = oldColorAlpha;
- }
-
- if (hitResult.hitResultType == null || hitResult.hitResultType == HitObjectType.CIRCLE) {
- // hit circles
- Image scaledHitCircle = GameImage.HITCIRCLE.getImage().getScaledCopy(scale);
- Image scaledHitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(scale);
- scaledHitCircle.setAlpha(alpha);
- scaledHitCircleOverlay.setAlpha(alpha);
- scaledHitCircle.drawCentered(hitResult.x, hitResult.y, hitResult.color);
- scaledHitCircleOverlay.drawCentered(hitResult.x, hitResult.y);
- }
- }
+ // hit animations (only draw when the "Hidden" mod is not enabled)
+ if (!GameMod.HIDDEN.isActive()) {
+ drawHitAnimations(hitResult, trackPosition);
}
// hit result
if (!hitResult.hideResult && (
- hitResult.hitResultType == HitObjectType.CIRCLE ||
- hitResult.hitResultType == HitObjectType.SPINNER ||
- hitResult.curve != null)) {
+ hitResult.hitResultType == HitObjectType.CIRCLE ||
+ hitResult.hitResultType == HitObjectType.SLIDER_FIRST ||
+ hitResult.hitResultType == HitObjectType.SLIDER_LAST ||
+ hitResult.hitResultType == HitObjectType.SPINNER)) {
float scaleProgress = AnimationEquation.IN_OUT_BOUNCE.calc(
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_TEXT_BOUNCE_TIME) / HITCIRCLE_TEXT_BOUNCE_TIME);
float scale = 1f + (HITCIRCLE_TEXT_ANIM_SCALE - 1f) * scaleProgress;
@@ -974,6 +945,64 @@ public class GameData {
}
}
+ /**
+ * Draw the hit animations:
+ * circles, reverse arrows, slider curves (fading out and/or expanding).
+ * @param hitResult the hit result
+ * @param trackPosition the current track position (in ms)
+ */
+ private void drawHitAnimations(HitObjectResult hitResult, int trackPosition) {
+ // fade out slider curve
+ if (hitResult.result != HIT_SLIDER_REPEAT && hitResult.curve != null) {
+ float progress = AnimationEquation.OUT_CUBIC.calc(
+ (float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME) / HITCIRCLE_FADE_TIME);
+ float alpha = 1f - progress;
+ float oldWhiteAlpha = Colors.WHITE_FADE.a;
+ float oldColorAlpha = hitResult.color.a;
+ Colors.WHITE_FADE.a = hitResult.color.a = alpha;
+ hitResult.curve.draw(hitResult.color);
+ Colors.WHITE_FADE.a = oldWhiteAlpha;
+ hitResult.color.a = oldColorAlpha;
+ }
+
+ // miss, don't draw an animation
+ if (hitResult.result == HIT_MISS) {
+ return;
+ }
+
+ // not a circle?
+ if (hitResult.hitResultType != HitObjectType.CIRCLE &&
+ hitResult.hitResultType != HitObjectType.SLIDER_FIRST &&
+ hitResult.hitResultType != HitObjectType.SLIDER_LAST) {
+ return;
+ }
+
+ // hit circles
+ float progress = AnimationEquation.OUT_CUBIC.calc(
+ (float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME) / HITCIRCLE_FADE_TIME);
+ float scale = (!hitResult.expand) ? 1f : 1f + (HITCIRCLE_ANIM_SCALE - 1f) * progress;
+ float alpha = 1f - progress;
+ if (hitResult.result == HIT_SLIDER_REPEAT) {
+ // repeats
+ Image scaledRepeat = GameImage.REVERSEARROW.getImage().getScaledCopy(scale);
+ scaledRepeat.setAlpha(alpha);
+ float ang;
+ if (hitResult.hitResultType == HitObjectType.SLIDER_FIRST) {
+ ang = hitResult.curve.getStartAngle();
+ } else {
+ ang = hitResult.curve.getEndAngle();
+ }
+ scaledRepeat.rotate(ang);
+ scaledRepeat.drawCentered(hitResult.x, hitResult.y, hitResult.color);
+ }
+ Image scaledHitCircle = GameImage.HITCIRCLE.getImage().getScaledCopy(scale);
+ Image scaledHitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(scale);
+ scaledHitCircle.setAlpha(alpha);
+ scaledHitCircleOverlay.setAlpha(alpha);
+ scaledHitCircle.drawCentered(hitResult.x, hitResult.y, hitResult.color);
+ scaledHitCircleOverlay.drawCentered(hitResult.x, hitResult.y);
+ }
+
/**
* Changes health by a given percentage, modified by drainRate.
* @param percent the health percentage
@@ -1202,16 +1231,16 @@ public class GameData {
health = 0f;
}
- public void sendInitialSliderResult(int time, float x, float y, Color color, Color mirrorcolor) {
- hitResultList.add(new HitObjectResult(time, HIT_SLIDER_INITIAL, x, y, color, null, null, true, false));
- if (!Options.isMirror() || !GameMod.AUTO.isActive()) {
- return;
- }
- float[] m = Utils.mirrorPoint(x, y);
- hitResultList.add(new HitObjectResult(time, HIT_SLIDER_INITIAL, m[0], m[1], mirrorcolor, null, null, true, false));
- }
-
- public void sendRepeatSliderResult(int time, float x, float y, Color color, Curve curve, HitObjectType type) {
+ /**
+ * Handles a slider repeat result (animation only: arrow).
+ * @param time the repeat time
+ * @param x the x coordinate
+ * @param y the y coordinate
+ * @param color the arrow color
+ * @param curve the slider curve
+ * @param type the hit object type
+ */
+ public void sendSliderRepeatResult(int time, float x, float y, Color color, Curve curve, HitObjectType type) {
hitResultList.add(new HitObjectResult(time, HIT_SLIDER_REPEAT, x, y, color, type, curve, true, true));
if (!Options.isMirror()) {
return;
@@ -1220,6 +1249,18 @@ public class GameData {
hitResultList.add(new HitObjectResult(time, HIT_SLIDER_REPEAT, m[0], m[1], color, type, curve, true, true));
}
+ /**
+ * Handles a slider start result (animation only: initial circle).
+ * @param time the hit time
+ * @param x the x coordinate
+ * @param y the y coordinate
+ * @param color the slider color
+ * @param expand whether or not the hit result animation should expand
+ */
+ public void sendSliderStartResult(int time, float x, float y, Color color, boolean expand) {
+ hitResultList.add(new HitObjectResult(time, HIT_ANIMATION_RESULT, x, y, color, HitObjectType.CIRCLE, null, expand, true));
+ }
+
/**
* Handles a slider tick result.
* @param time the tick start time
@@ -1229,7 +1270,7 @@ public class GameData {
* @param hitObject the hit object
* @param repeat the current repeat number
*/
- public void sliderTickResult(int time, int result, float x, float y, HitObject hitObject, int repeat) {
+ public void sendSliderTickResult(int time, int result, float x, float y, HitObject hitObject, int repeat) {
int hitValue = 0;
switch (result) {
case HIT_SLIDER30:
@@ -1415,6 +1456,23 @@ public class GameData {
boolean expand, int repeat, Curve curve, boolean sliderHeldToEnd) {
hitResult(time, result, x, y, color, end, hitObject, hitResultType, expand, repeat, curve, sliderHeldToEnd, true);
}
+
+ /**
+ * Handles a hit result.
+ * @param time the object start time
+ * @param result the hit result (HIT_* constants)
+ * @param x the x coordinate
+ * @param y the y coordinate
+ * @param color the combo color
+ * @param end true if this is the last hit object in the combo
+ * @param hitObject the hit object
+ * @param hitResultType the type of hit object for the result
+ * @param expand whether or not the hit result animation should expand (if applicable)
+ * @param repeat the current repeat number (for sliders, or 0 otherwise)
+ * @param curve the slider curve (or null if not applicable)
+ * @param sliderHeldToEnd whether or not the slider was held to the end (if applicable)
+ * @param handleResult whether or not to send a score result
+ */
public void hitResult(int time, int result, float x, float y, Color color,
boolean end, HitObject hitObject, HitObjectType hitResultType,
boolean expand, int repeat, Curve curve, boolean sliderHeldToEnd, boolean handleResult) {
@@ -1431,16 +1489,6 @@ public class GameData {
boolean hideResult = (hitResult == HIT_300 || hitResult == HIT_300G || hitResult == HIT_300K) && !Options.isPerfectHitBurstEnabled();
hitResultList.add(new HitObjectResult(time, hitResult, x, y, color, hitResultType, curve, expand, hideResult));
-
- /*
- // sliders: add the other curve endpoint for the hit animation
- if (curve != null) {
- boolean isFirst = (hitResultType == HitObjectType.SLIDER_FIRST);
- Vec2f p = curve.pointAt((isFirst) ? 1f : 0f);
- HitObjectType type = (isFirst) ? HitObjectType.SLIDER_LAST : HitObjectType.SLIDER_FIRST;
- hitResultList.add(new HitObjectResult(time, hitResult, p.x, p.y, color, type, null, expand, hideResult));
- }
- */
}
/**
diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java
index 756c8004..21d10812 100644
--- a/src/itdelatrisu/opsu/GameImage.java
+++ b/src/itdelatrisu/opsu/GameImage.java
@@ -655,7 +655,7 @@ public enum GameImage {
* If the default image has already been loaded, this will do nothing.
*/
public void setDefaultImage() {
- if (defaultImage != null || defaultImages != null)
+ if (defaultImage != null || defaultImages != null || Options.getSkin() == null)
return;
// try to load multiple images
diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java
index 78872003..485210a9 100644
--- a/src/itdelatrisu/opsu/Opsu.java
+++ b/src/itdelatrisu/opsu/Opsu.java
@@ -249,9 +249,12 @@ public class Opsu extends StateBasedGame {
} else
songMenu.resetTrackOnLoad();
}
+
+ // reset game data
if (UI.getCursor().isBeatmapSkinned())
UI.getCursor().reset();
songMenu.resetGameDataOnLoad();
+
this.enterState(Opsu.STATE_SONGMENU, new EasedFadeOutTransition(), new FadeInTransition());
return false;
}
diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java
index e30be1ab..880305d5 100644
--- a/src/itdelatrisu/opsu/Options.java
+++ b/src/itdelatrisu/opsu/Options.java
@@ -143,6 +143,12 @@ public class Options {
private static boolean noSingleInstance;
+ /** The theme song string: {@code filename,title,artist,length(ms)} */
+ private static String themeString = "theme.mp3,Rainbows,Kevin MacLeod,219350";
+
+ /** The theme song timing point string (for computing beats to pulse the logo) . */
+ private static String themeTimingPoint = "1080,545.454545454545,4,1,0,100,0,0";
+
/**
* Returns whether the XDG flag in the manifest (if any) is set to "true".
* @return true if XDG directories are enabled, false otherwise
@@ -171,8 +177,11 @@ public class Options {
* @return the XDG base directory, or the working directory if unavailable
*/
private static File getXDGBaseDir(String env, String fallback) {
+ File workingDir = Utils.isJarRunning() ?
+ Utils.getRunningDirectory().getParentFile() : Utils.getWorkingDirectory();
+
if (!USE_XDG)
- return new File("./");
+ return workingDir;
String OS = System.getProperty("os.name").toLowerCase();
if (OS.indexOf("nix") >= 0 || OS.indexOf("nux") >= 0 || OS.indexOf("aix") > 0) {
@@ -188,7 +197,7 @@ public class Options {
ErrorHandler.error(String.format("Failed to create configuration folder at '%s/opsu'.", rootPath), null, false);
return dir;
} else
- return new File("./");
+ return workingDir;
}
/**
@@ -219,12 +228,6 @@ public class Options {
return (dir.isDirectory()) ? dir : null;
}
- /**
- * The theme song string:
- * {@code filename,title,artist,length(ms)}
- */
- private static String themeString = "theme.ogg,On the Bach,Jingle Punks,66000";
-
/** Game options. */
public enum GameOption {
// internal options (not displayed in-game)
@@ -275,7 +278,32 @@ public class Options {
public String write() { return themeString; }
@Override
- public void read(String s) { themeString = s; }
+ public void read(String s) {
+ String oldThemeString = themeString;
+ themeString = s;
+ Beatmap beatmap = getThemeBeatmap();
+ if (beatmap == null) {
+ themeString = oldThemeString;
+ Log.warn(String.format("The theme song string [%s] is malformed.", s));
+ } else if (!beatmap.audioFilename.isFile()) {
+ themeString = oldThemeString;
+ Log.warn(String.format("Cannot find theme song [%s].", beatmap.audioFilename.getAbsolutePath()));
+ }
+ }
+ },
+ THEME_SONG_TIMINGPOINT ("ThemeSongTiming") {
+ @Override
+ public String write() { return themeTimingPoint; }
+
+ @Override
+ public void read(String s) {
+ try {
+ new TimingPoint(s);
+ themeTimingPoint = s;
+ } catch (Exception e) {
+ Log.warn(String.format("The theme song timing point [%s] is malformed.", s));
+ }
+ }
},
PORT ("Port") {
@Override
@@ -1876,25 +1904,26 @@ public class Options {
/**
* Returns a dummy Beatmap containing the theme song.
- * @return the theme song beatmap
+ * @return the theme song beatmap, or {@code null} if the theme string is malformed
*/
public static Beatmap getThemeBeatmap() {
String[] tokens = themeString.split(",");
- if (tokens.length != 4) {
- ErrorHandler.error("Theme song string is malformed.", null, false);
+ if (tokens.length != 4)
return null;
- }
Beatmap beatmap = new Beatmap(null);
beatmap.audioFilename = new File(tokens[0]);
beatmap.title = tokens[1];
beatmap.artist = tokens[2];
- beatmap.timingPoints = new ArrayList<>(1);
- beatmap.timingPoints.add(new TimingPoint("-44,631.578947368421,4,1,0,100,1,0"));
try {
beatmap.endTime = Integer.parseInt(tokens[3]);
} catch (NumberFormatException e) {
- ErrorHandler.error("Theme song length is not a valid integer", e, false);
+ return null;
+ }
+ try {
+ beatmap.timingPoints = new ArrayList<>(1);
+ beatmap.timingPoints.add(new TimingPoint(themeTimingPoint));
+ } catch (Exception e) {
return null;
}
diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java
index 507818df..5c90b836 100644
--- a/src/itdelatrisu/opsu/Utils.java
+++ b/src/itdelatrisu/opsu/Utils.java
@@ -41,6 +41,7 @@ import java.net.SocketTimeoutException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
+import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
@@ -158,6 +159,14 @@ public class Utils {
anim.draw(x - (anim.getWidth() / 2f), y - (anim.getHeight() / 2f));
}
+ /**
+ * Returns the luminance of a color.
+ * @param c the color
+ */
+ public static float getLuminance(Color c) {
+ return 0.299f*c.r + 0.587f*c.g + 0.114f*c.b;
+ }
+
/**
* Clamps a value between a lower and upper bound.
* @param val the value to clamp
@@ -554,6 +563,14 @@ public class Utils {
}
}
+ /**
+ * Returns the current working directory.
+ * @return the directory
+ */
+ public static File getWorkingDirectory() {
+ return Paths.get(".").toAbsolutePath().normalize().toFile();
+ }
+
/**
* Parses the integer string argument as a boolean:
* {@code 1} is {@code true}, and all other values are {@code false}.
diff --git a/src/itdelatrisu/opsu/audio/MusicController.java b/src/itdelatrisu/opsu/audio/MusicController.java
index 9cda69b1..c5e24e7b 100644
--- a/src/itdelatrisu/opsu/audio/MusicController.java
+++ b/src/itdelatrisu/opsu/audio/MusicController.java
@@ -78,6 +78,12 @@ public class MusicController {
/** The track dim level, if dimmed. */
private static float dimLevel = 1f;
+ /** Current timing point index in the track, advanced by {@link #getBeatProgress()}. */
+ private static int timingPointIndex;
+
+ /** Last non-inherited timing point. */
+ private static TimingPoint lastTimingPoint;
+
// This class should not be instantiated.
private MusicController() {}
@@ -94,7 +100,6 @@ public class MusicController {
final File audioFile = beatmap.audioFilename;
if (!audioFile.isFile() && !ResourceLoader.resourceExists(audioFile.getPath())) {
UI.sendBarNotification(String.format("Could not find track '%s'.", audioFile.getName()));
- System.out.println(beatmap);
return;
}
@@ -136,8 +141,10 @@ public class MusicController {
player.addListener(new MusicListener() {
@Override
public void musicEnded(Music music) {
- if (music == player) // don't fire if music swapped
+ if (music == player) { // don't fire if music swapped
trackEnded = true;
+ resetTimingPoint();
+ }
}
@Override
@@ -159,6 +166,7 @@ public class MusicController {
setVolume(Options.getMusicVolume() * Options.getMasterVolume());
trackEnded = false;
pauseTime = 0f;
+ resetTimingPoint();
if (loop)
player.loop();
else
@@ -187,34 +195,81 @@ public class MusicController {
/**
* Gets the progress of the current beat.
- * @return progress as a value in [0, 1], where 0 means a beat just happend and 1 means the next beat is coming now.
+ * @return a beat progress value [0,1) where 0 marks the current beat and
+ * 1 marks the next beat, or {@code null} if no timing information
+ * is available (e.g. music paused, no timing points)
*/
- public static Double getBeatProgress() {
- if (!isPlaying() || getBeatmap() == null) {
+ public static Float getBeatProgress() {
+ if (!updateTimingPoint())
return null;
- }
+
+ // calculate beat progress
+ int trackPosition = Math.max(0, getPosition());
+ double beatLength = lastTimingPoint.getBeatLength() * 100.0;
+ int beatTime = lastTimingPoint.getTime();
+ if (trackPosition < beatTime)
+ trackPosition += (beatLength / 100.0) * (beatTime / lastTimingPoint.getBeatLength());
+ return (float) ((((trackPosition - beatTime) * 100.0) % beatLength) / beatLength);
+ }
+
+ /**
+ * Gets the progress of the current measure.
+ * @return a measure progress value [0,1) where 0 marks the start of the measure and
+ * 1 marks the start of the next measure, or {@code null} if no timing information
+ * is available (e.g. music paused, no timing points)
+ */
+ public static Float getMeasureProgress() { return getMeasureProgress(1); }
+
+ /**
+ * Gets the progress of the current measure.
+ * @param k the meter multiplier
+ * @return a measure progress value [0,1) where 0 marks the start of the measure and
+ * 1 marks the start of the next measure, or {@code null} if no timing information
+ * is available (e.g. music paused, no timing points)
+ */
+ public static Float getMeasureProgress(int k) {
+ if (!updateTimingPoint())
+ return null;
+
+ // calculate measure progress
+ int trackPosition = Math.max(0, getPosition());
+ double measureLength = lastTimingPoint.getBeatLength() * lastTimingPoint.getMeter() * k * 100.0;
+ int beatTime = lastTimingPoint.getTime();
+ if (trackPosition < beatTime)
+ trackPosition += (measureLength / 100.0) * (beatTime / lastTimingPoint.getBeatLength());
+ return (float) ((((trackPosition - beatTime) * 100.0) % measureLength) / measureLength);
+ }
+
+ /**
+ * Updates the timing point information for the current track position.
+ * @return {@code false} if timing point information is not available, {@code true} otherwise
+ */
+ private static boolean updateTimingPoint() {
Beatmap map = getBeatmap();
- if (map.timingPoints == null) {
- return null;
+ if (!isPlaying() || map == null || map.timingPoints == null || map.timingPoints.isEmpty())
+ return false;
+
+ // initialization
+ if (timingPointIndex == 0 && lastTimingPoint == null && !map.timingPoints.isEmpty()) {
+ TimingPoint timingPoint = map.timingPoints.get(0);
+ if (!timingPoint.isInherited())
+ lastTimingPoint = timingPoint;
}
- int trackposition = getPosition();
- TimingPoint p = null;
- float beatlen = 0f;
- int time = 0;
- for (TimingPoint pts : map.timingPoints) {
- if (p == null || pts.getTime() < getPosition()) {
- p = pts;
- if (!p.isInherited() && p.getBeatLength() > 0) {
- beatlen = p.getBeatLength();
- time = p.getTime();
- }
- }
+
+ // advance timing point index, record last non-inherited timing point
+ int trackPosition = getPosition();
+ for (int i = timingPointIndex + 1; i < map.timingPoints.size(); i++) {
+ TimingPoint timingPoint = map.timingPoints.get(i);
+ if (trackPosition < timingPoint.getTime())
+ break;
+ timingPointIndex = i;
+ if (!timingPoint.isInherited() && timingPoint.getBeatLength() > 0)
+ lastTimingPoint = timingPoint;
}
- if (p == null) {
- return null;
- }
- double beatLength = beatlen * 100;
- return (((trackposition * 100 - time * 100) % beatLength) / beatLength);
+ if (lastTimingPoint == null)
+ return false; // no timing info
+
+ return true;
}
/**
@@ -258,8 +313,10 @@ public class MusicController {
public static void stop() {
if (isPlaying())
player.stop();
- if (trackExists())
+ if (trackExists()) {
pauseTime = 0f;
+ resetTimingPoint();
+ }
}
/**
@@ -298,7 +355,11 @@ public class MusicController {
* @param position the new track position (in ms)
*/
public static boolean setPosition(int position) {
- return (trackExists() && position >= 0 && player.setPosition(position / 1000f));
+ if (!trackExists() || position < 0)
+ return false;
+
+ resetTimingPoint();
+ return (player.setPosition(position / 1000f));
}
/**
@@ -339,6 +400,7 @@ public class MusicController {
public static void play(boolean loop) {
if (trackExists()) {
trackEnded = false;
+ resetTimingPoint();
if (loop)
player.loop();
else
@@ -409,6 +471,14 @@ public class MusicController {
setVolume(volume);
}
+ /**
+ * Resets timing point information.
+ */
+ private static void resetTimingPoint() {
+ timingPointIndex = 0;
+ lastTimingPoint = null;
+ }
+
/**
* Completely resets MusicController state.
*
@@ -427,7 +497,7 @@ public class MusicController {
try {
trackLoader.join();
} catch (InterruptedException e) {
- e.printStackTrace();
+ ErrorHandler.error(null, e, true);
}
}
trackLoader = null;
@@ -439,6 +509,7 @@ public class MusicController {
themePlaying = false;
pauseTime = 0f;
trackDimmed = false;
+ resetTimingPoint();
// releases all sources from previous tracks
destroyOpenAL();
diff --git a/src/itdelatrisu/opsu/beatmap/Beatmap.java b/src/itdelatrisu/opsu/beatmap/Beatmap.java
index 922aedde..a87cc236 100644
--- a/src/itdelatrisu/opsu/beatmap/Beatmap.java
+++ b/src/itdelatrisu/opsu/beatmap/Beatmap.java
@@ -68,6 +68,18 @@ public class Beatmap implements Comparable {
/** The star rating. */
public double starRating = -1;
+ /** The timestamp this beatmap was first loaded. */
+ public long dateAdded = 0;
+
+ /** Whether this beatmap is marked as a "favorite". */
+ public boolean favorite = false;
+
+ /** Total number of times this beatmap has been played. */
+ public int playCount = 0;
+
+ /** The last time this beatmap was played (timestamp). */
+ public long lastPlayed = 0;
+
/**
* [General]
*/
@@ -501,4 +513,12 @@ public class Beatmap implements Comparable {
String[] rgb = s.split(",");
this.sliderBorder = new Color(new Color(Integer.parseInt(rgb[0]), Integer.parseInt(rgb[1]), Integer.parseInt(rgb[2])));
}
+
+ /**
+ * Increments the play counter and last played time.
+ */
+ public void incrementPlayCounter() {
+ this.playCount++;
+ this.lastPlayed = System.currentTimeMillis();
+ }
}
\ No newline at end of file
diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapDifficultyCalculator.java b/src/itdelatrisu/opsu/beatmap/BeatmapDifficultyCalculator.java
index 6cf29528..45075b55 100644
--- a/src/itdelatrisu/opsu/beatmap/BeatmapDifficultyCalculator.java
+++ b/src/itdelatrisu/opsu/beatmap/BeatmapDifficultyCalculator.java
@@ -81,7 +81,7 @@ public class BeatmapDifficultyCalculator {
*/
public BeatmapDifficultyCalculator(Beatmap beatmap) {
this.beatmap = beatmap;
- if (beatmap.timingPoints == null)
+ if (beatmap.breaks == null)
BeatmapDB.load(beatmap, BeatmapDB.LOAD_ARRAY);
BeatmapParser.parseHitObjects(beatmap);
}
diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapGroup.java b/src/itdelatrisu/opsu/beatmap/BeatmapGroup.java
new file mode 100644
index 00000000..204f121d
--- /dev/null
+++ b/src/itdelatrisu/opsu/beatmap/BeatmapGroup.java
@@ -0,0 +1,179 @@
+package itdelatrisu.opsu.beatmap;
+
+import itdelatrisu.opsu.GameImage;
+import itdelatrisu.opsu.ui.MenuButton;
+import itdelatrisu.opsu.ui.UI;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+
+import org.newdawn.slick.Image;
+
+/**
+ * Beatmap groups.
+ */
+public enum BeatmapGroup {
+ /** All beatmaps (no filter). */
+ ALL (0, "All Songs", null),
+
+ /** Most recently played beatmaps. */
+ RECENT (1, "Last Played", "Your recently played beatmaps will appear in this list!") {
+ /** Number of elements to show. */
+ private static final int K = 20;
+
+ /** Returns the latest "last played" time in a beatmap set. */
+ private long lastPlayed(BeatmapSet set) {
+ long max = 0;
+ for (Beatmap beatmap : set) {
+ if (beatmap.lastPlayed > max)
+ max = beatmap.lastPlayed;
+ }
+ return max;
+ }
+
+ @Override
+ public ArrayList filter(ArrayList list) {
+ // find top K elements
+ PriorityQueue pq = new PriorityQueue(K, new Comparator() {
+ @Override
+ public int compare(BeatmapSetNode v, BeatmapSetNode w) {
+ return Long.compare(lastPlayed(v.getBeatmapSet()), lastPlayed(w.getBeatmapSet()));
+ }
+ });
+ for (BeatmapSetNode node : list) {
+ long timestamp = lastPlayed(node.getBeatmapSet());
+ if (timestamp == 0)
+ continue; // skip unplayed beatmaps
+ if (pq.size() < K || timestamp > lastPlayed(pq.peek().getBeatmapSet())) {
+ if (pq.size() == K)
+ pq.poll();
+ pq.add(node);
+ }
+ }
+
+ // return as list
+ ArrayList filteredList = new ArrayList();
+ for (BeatmapSetNode node : pq)
+ filteredList.add(node);
+ return filteredList;
+ }
+ },
+
+ /** "Favorite" beatmaps. */
+ FAVORITE (2, "Favorites", "Right-click a beatmap to add it to your Favorites!") {
+ @Override
+ public ArrayList filter(ArrayList list) {
+ // find "favorite" beatmaps
+ ArrayList filteredList = new ArrayList();
+ for (BeatmapSetNode node : list) {
+ if (node.getBeatmapSet().isFavorite())
+ filteredList.add(node);
+ }
+ return filteredList;
+ }
+ };
+
+ /** The ID of the group (used for tab positioning). */
+ private final int id;
+
+ /** The name of the group. */
+ private final String name;
+
+ /** The message to display if this list is empty. */
+ private final String emptyMessage;
+
+ /** The tab associated with the group (displayed in Song Menu screen). */
+ private MenuButton tab;
+
+ /** Total number of groups. */
+ private static final int SIZE = values().length;
+
+ /** Array of BeatmapGroup objects in reverse order. */
+ public static final BeatmapGroup[] VALUES_REVERSED;
+ static {
+ VALUES_REVERSED = values();
+ Collections.reverse(Arrays.asList(VALUES_REVERSED));
+ }
+
+ /** Current group. */
+ private static BeatmapGroup currentGroup = ALL;
+
+ /**
+ * Returns the current group.
+ * @return the current group
+ */
+ public static BeatmapGroup current() { return currentGroup; }
+
+ /**
+ * Sets a new group.
+ * @param group the new group
+ */
+ public static void set(BeatmapGroup group) { currentGroup = group; }
+
+ /**
+ * Constructor.
+ * @param id the ID of the group (for tab positioning)
+ * @param name the group name
+ * @param emptyMessage the message to display if this list is empty
+ */
+ BeatmapGroup(int id, String name, String emptyMessage) {
+ this.id = id;
+ this.name = name;
+ this.emptyMessage = emptyMessage;
+ }
+
+ /**
+ * Returns the message to display if this list is empty.
+ * @return the message, or null if none
+ */
+ public String getEmptyMessage() { return emptyMessage; }
+
+ /**
+ * Returns a filtered list of beatmap set nodes.
+ * @param list the unfiltered list
+ * @return the filtered list
+ */
+ public ArrayList filter(ArrayList list) {
+ return list;
+ }
+
+ /**
+ * Initializes the tab.
+ * @param containerWidth the container width
+ * @param bottomY the bottom y coordinate
+ */
+ public void init(int containerWidth, float bottomY) {
+ Image tab = GameImage.MENU_TAB.getImage();
+ int tabWidth = tab.getWidth();
+ float buttonX = containerWidth / 2f;
+ float tabOffset = (containerWidth - buttonX - tabWidth) / (SIZE - 1);
+ if (tabOffset > tabWidth) { // prevent tabs from being spaced out
+ tabOffset = tabWidth;
+ buttonX = (containerWidth * 0.99f) - (tabWidth * SIZE);
+ }
+ this.tab = new MenuButton(tab,
+ (buttonX + (tabWidth / 2f)) + (id * tabOffset),
+ bottomY - (tab.getHeight() / 2f)
+ );
+ }
+
+ /**
+ * Checks if the coordinates are within the image bounds.
+ * @param x the x coordinate
+ * @param y the y coordinate
+ * @return true if within bounds
+ */
+ public boolean contains(float x, float y) { return tab.contains(x, y); }
+
+ /**
+ * Draws the tab.
+ * @param selected whether the tab is selected (white) or not (red)
+ * @param isHover whether to include a hover effect (unselected only)
+ */
+ public void draw(boolean selected, boolean isHover) {
+ UI.drawTab(tab.getX(), tab.getY(), name, selected, isHover);
+ }
+}
diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapParser.java b/src/itdelatrisu/opsu/beatmap/BeatmapParser.java
index 0f293ead..3e3fa4f1 100644
--- a/src/itdelatrisu/opsu/beatmap/BeatmapParser.java
+++ b/src/itdelatrisu/opsu/beatmap/BeatmapParser.java
@@ -112,6 +112,7 @@ public class BeatmapParser {
// parse directories
BeatmapSetNode lastNode = null;
+ long timestamp = System.currentTimeMillis();
for (File dir : dirs) {
currentDirectoryIndex++;
if (!dir.isDirectory())
@@ -160,6 +161,7 @@ public class BeatmapParser {
// add to parsed beatmap list
if (beatmap != null) {
+ beatmap.dateAdded = timestamp;
beatmaps.add(beatmap);
parsedBeatmaps.add(beatmap);
}
@@ -547,21 +549,7 @@ public class BeatmapParser {
break;
try {
- // parse timing point
- TimingPoint timingPoint = new TimingPoint(line);
-
- // calculate BPM
- if (!timingPoint.isInherited()) {
- int bpm = Math.round(60000 / timingPoint.getBeatLength());
- if (beatmap.bpmMin == 0)
- beatmap.bpmMin = beatmap.bpmMax = bpm;
- else if (bpm < beatmap.bpmMin)
- beatmap.bpmMin = bpm;
- else if (bpm > beatmap.bpmMax)
- beatmap.bpmMax = bpm;
- }
-
- beatmap.timingPoints.add(timingPoint);
+ parseTimingPoint(beatmap, line);
} catch (Exception e) {
Log.warn(String.format("Failed to read timing point '%s' for file '%s'.",
line, file.getAbsolutePath()), e);
@@ -678,6 +666,71 @@ public class BeatmapParser {
return beatmap;
}
+ /**
+ * Parses a timing point and adds it to the beatmap.
+ * @param beatmap the beatmap
+ * @param line the line containing the unparsed timing point
+ */
+ private static void parseTimingPoint(Beatmap beatmap, String line) {
+ // parse timing point
+ TimingPoint timingPoint = new TimingPoint(line);
+ beatmap.timingPoints.add(timingPoint);
+
+ // calculate BPM
+ if (!timingPoint.isInherited()) {
+ int bpm = Math.round(60000 / timingPoint.getBeatLength());
+ if (beatmap.bpmMin == 0) {
+ beatmap.bpmMin = beatmap.bpmMax = bpm;
+ } else if (bpm < beatmap.bpmMin) {
+ beatmap.bpmMin = bpm;
+ } else if (bpm > beatmap.bpmMax) {
+ beatmap.bpmMax = bpm;
+ }
+ }
+ }
+
+ /**
+ * Parses all timing points in a beatmap.
+ * @param beatmap the beatmap to parse
+ */
+ public static void parseTimingPoints(Beatmap beatmap) {
+ if (beatmap.timingPoints != null) // already parsed
+ return;
+
+ beatmap.timingPoints = new ArrayList();
+
+ try (BufferedReader in = new BufferedReader(new FileReader(beatmap.getFile()))) {
+ String line = in.readLine();
+ while (line != null) {
+ line = line.trim();
+ if (!line.equals("[TimingPoints]"))
+ line = in.readLine();
+ else
+ break;
+ }
+ if (line == null) // no timing points
+ return;
+
+ while ((line = in.readLine()) != null) {
+ line = line.trim();
+ if (!isValidLine(line))
+ continue;
+ if (line.charAt(0) == '[')
+ break;
+
+ try {
+ parseTimingPoint(beatmap, line);
+ } catch (Exception e) {
+ Log.warn(String.format("Failed to read timing point '%s' for file '%s'.",
+ line, beatmap.getFile().getAbsolutePath()), e);
+ }
+ }
+ beatmap.timingPoints.trimToSize();
+ } catch (IOException e) {
+ ErrorHandler.error(String.format("Failed to read file '%s'.", beatmap.getFile().getAbsolutePath()), e, false);
+ }
+ }
+
/**
* Parses all hit objects in a beatmap.
* @param beatmap the beatmap to parse
diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapSet.java b/src/itdelatrisu/opsu/beatmap/BeatmapSet.java
index 62491ae6..3c5e7e6c 100644
--- a/src/itdelatrisu/opsu/beatmap/BeatmapSet.java
+++ b/src/itdelatrisu/opsu/beatmap/BeatmapSet.java
@@ -19,6 +19,7 @@
package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.GameMod;
+import itdelatrisu.opsu.db.BeatmapDB;
import java.text.DecimalFormat;
import java.text.NumberFormat;
@@ -185,4 +186,37 @@ public class BeatmapSet implements Iterable {
return false;
}
+
+ /**
+ * Returns whether this beatmap set is a "favorite".
+ */
+ public boolean isFavorite() {
+ for (Beatmap map : beatmaps) {
+ if (map.favorite)
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Sets the "favorite" status of this beatmap set.
+ * @param flag whether this beatmap set should have "favorite" status
+ */
+ public void setFavorite(boolean flag) {
+ for (Beatmap map : beatmaps) {
+ map.favorite = flag;
+ BeatmapDB.updateFavoriteStatus(map);
+ }
+ }
+
+ /**
+ * Returns whether any beatmap in this set has been played.
+ */
+ public boolean isPlayed() {
+ for (Beatmap map : beatmaps) {
+ if (map.playCount > 0)
+ return true;
+ }
+ return false;
+ }
}
diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java b/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java
index 7a0efdd9..dc9471ce 100644
--- a/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java
+++ b/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java
@@ -54,6 +54,9 @@ public class BeatmapSetList {
/** Total number of beatmaps (i.e. Beatmap objects). */
private int mapCount = 0;
+ /** List containing all nodes in the current group. */
+ private ArrayList groupNodes;
+
/** Current list of nodes (subset of parsedNodes, used for searches). */
private ArrayList nodes;
@@ -97,7 +100,7 @@ public class BeatmapSetList {
* This does not erase any parsed nodes.
*/
public void reset() {
- nodes = parsedNodes;
+ nodes = groupNodes = BeatmapGroup.current().filter(parsedNodes);
expandedIndex = -1;
expandedStartNode = expandedEndNode = null;
lastQuery = "";
@@ -168,6 +171,7 @@ public class BeatmapSetList {
Beatmap beatmap = beatmapSet.get(0);
nodes.remove(index);
parsedNodes.remove(eCur);
+ groupNodes.remove(eCur);
mapCount -= beatmapSet.size();
if (beatmap.beatmapSetID > 0)
MSIDdb.remove(beatmap.beatmapSetID);
@@ -407,7 +411,7 @@ public class BeatmapSetList {
return;
// sort the list
- Collections.sort(nodes, BeatmapSortOrder.getSort().getComparator());
+ Collections.sort(nodes, BeatmapSortOrder.current().getComparator());
expandedIndex = -1;
expandedStartNode = expandedEndNode = null;
@@ -444,7 +448,7 @@ public class BeatmapSetList {
// if empty query, reset to original list
if (query.isEmpty() || terms.isEmpty()) {
- nodes = parsedNodes;
+ nodes = groupNodes;
return true;
}
@@ -472,14 +476,14 @@ public class BeatmapSetList {
String type = condType.remove();
String operator = condOperator.remove();
float value = condValue.remove();
- for (BeatmapSetNode node : parsedNodes) {
+ for (BeatmapSetNode node : groupNodes) {
if (node.getBeatmapSet().matches(type, operator, value))
nodes.add(node);
}
} else {
// normal term
String term = terms.remove();
- for (BeatmapSetNode node : parsedNodes) {
+ for (BeatmapSetNode node : groupNodes) {
if (node.getBeatmapSet().matches(term))
nodes.add(node);
}
diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java b/src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java
index 947631c5..1430162a 100644
--- a/src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java
+++ b/src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java
@@ -75,7 +75,7 @@ public class BeatmapSetNode {
public void draw(float x, float y, Grade grade, boolean focus) {
Image bg = GameImage.MENU_BUTTON_BG.getImage();
boolean expanded = (beatmapIndex > -1);
- Beatmap beatmap;
+ Beatmap beatmap = beatmapSet.get(expanded ? beatmapIndex : 0);
bg.setAlpha(0.9f);
Color bgColor;
Color textColor = Options.getSkin().getSongSelectInactiveTextColor();
@@ -88,11 +88,10 @@ public class BeatmapSetNode {
textColor = Options.getSkin().getSongSelectActiveTextColor();
} else
bgColor = Colors.BLUE_BUTTON;
- beatmap = beatmapSet.get(beatmapIndex);
- } else {
+ } else if (beatmapSet.isPlayed())
bgColor = Colors.ORANGE_BUTTON;
- beatmap = beatmapSet.get(0);
- }
+ else
+ bgColor = Colors.PINK_BUTTON;
bg.draw(x, y, bgColor);
float cx = x + (bg.getWidth() * 0.043f);
diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapSortOrder.java b/src/itdelatrisu/opsu/beatmap/BeatmapSortOrder.java
index 78ca593c..93f4dca8 100644
--- a/src/itdelatrisu/opsu/beatmap/BeatmapSortOrder.java
+++ b/src/itdelatrisu/opsu/beatmap/BeatmapSortOrder.java
@@ -18,28 +18,19 @@
package itdelatrisu.opsu.beatmap;
-import itdelatrisu.opsu.GameImage;
-import itdelatrisu.opsu.ui.MenuButton;
-import itdelatrisu.opsu.ui.UI;
-
-import java.util.Arrays;
-import java.util.Collections;
import java.util.Comparator;
-import org.newdawn.slick.Image;
-
/**
* Beatmap sorting orders.
*/
public enum BeatmapSortOrder {
- TITLE (0, "Title", new TitleOrder()),
- ARTIST (1, "Artist", new ArtistOrder()),
- CREATOR (2, "Creator", new CreatorOrder()),
- BPM (3, "BPM", new BPMOrder()),
- LENGTH (4, "Length", new LengthOrder());
-
- /** The ID of the sort (used for tab positioning). */
- private final int id;
+ TITLE ("Title", new TitleOrder()),
+ ARTIST ("Artist", new ArtistOrder()),
+ CREATOR ("Creator", new CreatorOrder()),
+ BPM ("BPM", new BPMOrder()),
+ LENGTH ("Length", new LengthOrder()),
+ DATE ("Date Added", new DateOrder()),
+ PLAYS ("Most Played", new PlayOrder());
/** The name of the sort. */
private final String name;
@@ -47,19 +38,6 @@ public enum BeatmapSortOrder {
/** The comparator for the sort. */
private final Comparator comparator;
- /** The tab associated with the sort (displayed in Song Menu screen). */
- private MenuButton tab;
-
- /** Total number of sorts. */
- private static final int SIZE = values().length;
-
- /** Array of BeatmapSortOrder objects in reverse order. */
- public static final BeatmapSortOrder[] VALUES_REVERSED;
- static {
- VALUES_REVERSED = values();
- Collections.reverse(Arrays.asList(VALUES_REVERSED));
- }
-
/** Current sort. */
private static BeatmapSortOrder currentSort = TITLE;
@@ -67,13 +45,13 @@ public enum BeatmapSortOrder {
* Returns the current sort.
* @return the current sort
*/
- public static BeatmapSortOrder getSort() { return currentSort; }
+ public static BeatmapSortOrder current() { return currentSort; }
/**
* Sets a new sort.
* @param sort the new sort
*/
- public static void setSort(BeatmapSortOrder sort) { BeatmapSortOrder.currentSort = sort; }
+ public static void set(BeatmapSortOrder sort) { currentSort = sort; }
/**
* Compares two BeatmapSetNode objects by title.
@@ -135,37 +113,57 @@ public enum BeatmapSortOrder {
}
}
+ /**
+ * Compares two BeatmapSetNode objects by date added.
+ * Uses the latest beatmap added in each set for comparison.
+ */
+ private static class DateOrder implements Comparator {
+ @Override
+ public int compare(BeatmapSetNode v, BeatmapSetNode w) {
+ long vMax = 0, wMax = 0;
+ for (Beatmap beatmap : v.getBeatmapSet()) {
+ if (beatmap.dateAdded > vMax)
+ vMax = beatmap.dateAdded;
+ }
+ for (Beatmap beatmap : w.getBeatmapSet()) {
+ if (beatmap.dateAdded > wMax)
+ wMax = beatmap.dateAdded;
+ }
+ return Long.compare(vMax, wMax);
+ }
+ }
+
+ /**
+ * Compares two BeatmapSetNode objects by total plays
+ * (summed across all beatmaps in each set).
+ */
+ private static class PlayOrder implements Comparator {
+ @Override
+ public int compare(BeatmapSetNode v, BeatmapSetNode w) {
+ int vTotal = 0, wTotal = 0;
+ for (Beatmap beatmap : v.getBeatmapSet())
+ vTotal += beatmap.playCount;
+ for (Beatmap beatmap : w.getBeatmapSet())
+ wTotal += beatmap.playCount;
+ return Integer.compare(vTotal, wTotal);
+ }
+ }
+
/**
* Constructor.
- * @param id the ID of the sort (for tab positioning)
* @param name the sort name
* @param comparator the comparator for the sort
*/
- BeatmapSortOrder(int id, String name, Comparator comparator) {
- this.id = id;
+ BeatmapSortOrder(String name, Comparator comparator) {
this.name = name;
this.comparator = comparator;
}
/**
- * Initializes the sort tab.
- * @param containerWidth the container width
- * @param bottomY the bottom y coordinate
+ * Returns the sort name.
+ * @return the name
*/
- public void init(int containerWidth, float bottomY) {
- Image tab = GameImage.MENU_TAB.getImage();
- int tabWidth = tab.getWidth();
- float buttonX = containerWidth / 2f;
- float tabOffset = (containerWidth - buttonX - tabWidth) / (SIZE - 1);
- if (tabOffset > tabWidth) { // prevent tabs from being spaced out
- tabOffset = tabWidth;
- buttonX = (containerWidth * 0.99f) - (tabWidth * SIZE);
- }
- this.tab = new MenuButton(tab,
- (buttonX + (tabWidth / 2f)) + (id * tabOffset),
- bottomY - (tab.getHeight() / 2f)
- );
- }
+ public String getName() { return name; }
/**
* Returns the comparator for the sort.
@@ -173,20 +171,6 @@ public enum BeatmapSortOrder {
*/
public Comparator getComparator() { return comparator; }
- /**
- * Checks if the coordinates are within the image bounds.
- * @param x the x coordinate
- * @param y the y coordinate
- * @return true if within bounds
- */
- public boolean contains(float x, float y) { return tab.contains(x, y); }
-
- /**
- * Draws the sort tab.
- * @param selected whether the tab is selected (white) or not (red)
- * @param isHover whether to include a hover effect (unselected only)
- */
- public void draw(boolean selected, boolean isHover) {
- UI.drawTab(tab.getX(), tab.getY(), name, selected, isHover);
- }
+ @Override
+ public String toString() { return name; }
}
\ No newline at end of file
diff --git a/src/itdelatrisu/opsu/beatmap/TimingPoint.java b/src/itdelatrisu/opsu/beatmap/TimingPoint.java
index 91597d4a..2fb26f50 100644
--- a/src/itdelatrisu/opsu/beatmap/TimingPoint.java
+++ b/src/itdelatrisu/opsu/beatmap/TimingPoint.java
@@ -58,6 +58,14 @@ public class TimingPoint {
* @param line the line to be parsed
*/
public TimingPoint(String line) {
+ /**
+ * [TIMING POINT FORMATS]
+ * Non-inherited:
+ * offset,msPerBeat,meter,sampleType,sampleSet,volume,inherited,kiai
+ *
+ * Inherited:
+ * offset,velocity,meter,sampleType,sampleSet,volume,inherited,kiai
+ */
// TODO: better support for old formats
String[] tokens = line.split(",");
try {
diff --git a/src/itdelatrisu/opsu/db/BeatmapDB.java b/src/itdelatrisu/opsu/db/BeatmapDB.java
index 5d54ab4d..2dbb1e2f 100644
--- a/src/itdelatrisu/opsu/db/BeatmapDB.java
+++ b/src/itdelatrisu/opsu/db/BeatmapDB.java
@@ -30,6 +30,7 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -42,8 +43,30 @@ public class BeatmapDB {
/**
* Current database version.
* This value should be changed whenever the database format changes.
+ * Add any update queries to the {@link #getUpdateQueries(int)} method.
*/
- private static final String DATABASE_VERSION = "2015-09-02";
+ private static final int DATABASE_VERSION = 20161222;
+
+ /**
+ * Returns a list of SQL queries to apply, in order, to update from
+ * the given database version to the latest version.
+ * @param version the current version
+ * @return a list of SQL queries
+ */
+ private static List getUpdateQueries(int version) {
+ List list = new LinkedList();
+ if (version < 20161222) {
+ list.add("ALTER TABLE beatmaps ADD COLUMN dateAdded INTEGER");
+ list.add("ALTER TABLE beatmaps ADD COLUMN favorite BOOLEAN");
+ list.add("ALTER TABLE beatmaps ADD COLUMN playCount INTEGER");
+ list.add("ALTER TABLE beatmaps ADD COLUMN lastPlayed INTEGER");
+ list.add("UPDATE beatmaps SET dateAdded = 0, favorite = 0, playCount = 0, lastPlayed = 0");
+ }
+
+ /* add future updates here */
+
+ return list;
+ }
/** Minimum batch size ratio ({@code batchSize/cacheSize}) to invoke batch loading. */
private static final float LOAD_BATCH_MIN_RATIO = 0.2f;
@@ -58,7 +81,9 @@ public class BeatmapDB {
private static Connection connection;
/** Query statements. */
- private static PreparedStatement insertStmt, selectStmt, deleteMapStmt, deleteGroupStmt, setStarsStmt, updateSizeStmt;
+ private static PreparedStatement
+ insertStmt, selectStmt, deleteMapStmt, deleteGroupStmt,
+ setStarsStmt, updatePlayStatsStmt, setFavoriteStmt, updateSizeStmt;
/** Current size of beatmap cache table. */
private static int cacheSize = -1;
@@ -75,6 +100,9 @@ public class BeatmapDB {
if (connection == null)
return;
+ // run any database updates
+ updateDatabase();
+
// create the database
createDatabase();
@@ -88,20 +116,21 @@ public class BeatmapDB {
// retrieve the cache size
getCacheSize();
- // check the database version
- checkVersion();
-
// prepare sql statements (not used here)
try {
insertStmt = connection.prepareStatement(
"INSERT INTO beatmaps VALUES (" +
- "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," +
- "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
+ "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," +
+ "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," +
+ "?, ?, ?, ?, ?, ?" +
+ ")"
);
selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?");
deleteMapStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?");
deleteGroupStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ?");
setStarsStmt = connection.prepareStatement("UPDATE beatmaps SET stars = ? WHERE dir = ? AND file = ?");
+ updatePlayStatsStmt = connection.prepareStatement("UPDATE beatmaps SET playCount = ?, lastPlayed = ? WHERE dir = ? AND file = ?");
+ setFavoriteStmt = connection.prepareStatement("UPDATE beatmaps SET favorite = ? WHERE dir = ? AND file = ?");
} catch (SQLException e) {
ErrorHandler.error("Failed to prepare beatmap statements.", e, true);
}
@@ -124,7 +153,8 @@ public class BeatmapDB {
"audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " +
"mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " +
"bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT, " +
- "md5hash TEXT, stars REAL" +
+ "md5hash TEXT, stars REAL, " +
+ "dateAdded INTEGER, favorite BOOLEAN, playCount INTEGER, lastPlayed INTEGER" +
"); " +
"CREATE TABLE IF NOT EXISTS info (" +
"key TEXT NOT NULL UNIQUE, value TEXT" +
@@ -145,29 +175,54 @@ public class BeatmapDB {
}
/**
- * Checks the stored table version, clears the beatmap database if different
- * from the current version, then updates the version field.
+ * Applies any database updates by comparing the current version to the
+ * stored version. Does nothing if tables have not been created.
*/
- private static void checkVersion() {
+ private static void updateDatabase() {
try (Statement stmt = connection.createStatement()) {
- // get the stored version
- String sql = "SELECT value FROM info WHERE key = 'version'";
+ int version = 0;
+
+ // if 'info' table does not exist, assume version 0 and apply all updates
+ String sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'info'";
ResultSet rs = stmt.executeQuery(sql);
- String version = (rs.next()) ? rs.getString(1) : "";
+ boolean infoExists = rs.isBeforeFirst();
rs.close();
+ if (!infoExists) {
+ // if 'beatmaps' table also does not exist, databases not yet created
+ sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'beatmaps'";
+ ResultSet beatmapsRS = stmt.executeQuery(sql);
+ boolean beatmapsExists = beatmapsRS.isBeforeFirst();
+ beatmapsRS.close();
+ if (!beatmapsExists)
+ return;
+ } else {
+ // try to retrieve stored version
+ sql = "SELECT value FROM info WHERE key = 'version'";
+ ResultSet versionRS = stmt.executeQuery(sql);
+ String versionString = (versionRS.next()) ? versionRS.getString(1) : "0";
+ versionRS.close();
+ try {
+ version = Integer.parseInt(versionString);
+ } catch (NumberFormatException e) {}
+ }
- // if different from current version, clear the database
- if (!version.equals(DATABASE_VERSION)) {
- clearDatabase();
+ // database versions match
+ if (version >= DATABASE_VERSION)
+ return;
- // update version
+ // apply updates
+ for (String query : getUpdateQueries(version))
+ stmt.executeUpdate(query);
+
+ // update version
+ if (infoExists) {
PreparedStatement ps = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('version', ?)");
- ps.setString(1, DATABASE_VERSION);
+ ps.setString(1, Integer.toString(DATABASE_VERSION));
ps.executeUpdate();
ps.close();
}
} catch (SQLException e) {
- ErrorHandler.error("Beatmap database version checks failed.", e, true);
+ ErrorHandler.error("Failed to update beatmap database.", e, true);
}
}
@@ -344,6 +399,10 @@ public class BeatmapDB {
stmt.setString(40, beatmap.comboToString());
stmt.setString(41, beatmap.md5Hash);
stmt.setDouble(42, beatmap.starRating);
+ stmt.setLong(43, beatmap.dateAdded);
+ stmt.setBoolean(44, beatmap.favorite);
+ stmt.setInt(45, beatmap.playCount);
+ stmt.setLong(46, beatmap.lastPlayed);
} catch (SQLException e) {
throw e;
} catch (Exception e) {
@@ -487,6 +546,10 @@ public class BeatmapDB {
beatmap.sliderBorderFromString(rs.getString(37));
beatmap.md5Hash = rs.getString(41);
beatmap.starRating = rs.getDouble(42);
+ beatmap.dateAdded = rs.getLong(43);
+ beatmap.favorite = rs.getBoolean(44);
+ beatmap.playCount = rs.getInt(45);
+ beatmap.lastPlayed = rs.getLong(46);
} catch (SQLException e) {
throw e;
} catch (Exception e) {
@@ -593,6 +656,45 @@ public class BeatmapDB {
}
}
+ /**
+ * Updates the play statistics for a beatmap in the database.
+ * @param beatmap the beatmap
+ */
+ public static void updatePlayStatistics(Beatmap beatmap) {
+ if (connection == null)
+ return;
+
+ try {
+ updatePlayStatsStmt.setInt(1, beatmap.playCount);
+ updatePlayStatsStmt.setLong(2, beatmap.lastPlayed);
+ updatePlayStatsStmt.setString(3, beatmap.getFile().getParentFile().getName());
+ updatePlayStatsStmt.setString(4, beatmap.getFile().getName());
+ updatePlayStatsStmt.executeUpdate();
+ } catch (SQLException e) {
+ ErrorHandler.error(String.format("Failed to update play statistics for beatmap '%s' in database.",
+ beatmap.toString()), e, true);
+ }
+ }
+
+ /**
+ * Updates the "favorite" status for a beatmap in the database.
+ * @param beatmap the beatmap
+ */
+ public static void updateFavoriteStatus(Beatmap beatmap) {
+ if (connection == null)
+ return;
+
+ try {
+ setFavoriteStmt.setBoolean(1, beatmap.favorite);
+ setFavoriteStmt.setString(2, beatmap.getFile().getParentFile().getName());
+ setFavoriteStmt.setString(3, beatmap.getFile().getName());
+ setFavoriteStmt.executeUpdate();
+ } catch (SQLException e) {
+ ErrorHandler.error(String.format("Failed to update favorite status for beatmap '%s' in database.",
+ beatmap.toString()), e, true);
+ }
+ }
+
/**
* Closes the connection to the database.
*/
diff --git a/src/itdelatrisu/opsu/db/ScoreDB.java b/src/itdelatrisu/opsu/db/ScoreDB.java
index c6dc4771..babf1841 100644
--- a/src/itdelatrisu/opsu/db/ScoreDB.java
+++ b/src/itdelatrisu/opsu/db/ScoreDB.java
@@ -360,7 +360,7 @@ public class ScoreDB {
ResultSet rs = selectMapSetStmt.executeQuery();
List list = null;
- String version = ""; // sorted by version, so pass through and check for differences
+ String version = null; // sorted by version, so pass through and check for differences
while (rs.next()) {
ScoreData s = new ScoreData(rs);
if (!s.version.equals(version)) {
diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java
index 67c00210..7c0d5756 100644
--- a/src/itdelatrisu/opsu/objects/Circle.java
+++ b/src/itdelatrisu/opsu/objects/Circle.java
@@ -179,7 +179,7 @@ public class Circle extends GameObject {
if (result > -1) {
data.addHitError(hitObject.getTime(), x, y, timeDiff);
- data.hitResult(trackPosition, result, this.x, this.y, color, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false);
+ data.sendHitResult(trackPosition, result, this.x, this.y, color, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false);
return true;
}
}
@@ -195,25 +195,25 @@ public class Circle extends GameObject {
if (trackPosition > time + hitResultOffset[GameData.HIT_50]) {
if (isAutoMod) {// "auto" mod: catch any missed notes due to lag
- data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false);
+ data.sendHitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false);
if (Options.isMirror() && GameMod.AUTO.isActive()) {
float[] m = Utils.mirrorPoint(x, y);
- data.hitResult(time, GameData.HIT_300, m[0], m[1], mirrorColor, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false, false);
+ data.sendHitResult(time, GameData.HIT_300, m[0], m[1], mirrorColor, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false, false);
}
}
else // no more points can be scored, so send a miss
- data.hitResult(trackPosition, GameData.HIT_MISS, x, y, null, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false);
+ data.sendHitResult(trackPosition, GameData.HIT_MISS, x, y, null, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false);
return true;
}
// "auto" mod: send a perfect hit result
else if (isAutoMod) {
if (Math.abs(trackPosition - time) < hitResultOffset[GameData.HIT_300]) {
- data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false);
+ data.sendHitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false);
if (Options.isMirror() && GameMod.AUTO.isActive()) {
float[] m = Utils.mirrorPoint(x, y);
- data.hitResult(time, GameData.HIT_300, m[0], m[1], mirrorColor, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false, false);
+ data.sendHitResult(time, GameData.HIT_300, m[0], m[1], mirrorColor, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false, false);
}
return true;
}
diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java
index 7cfa0181..78870133 100644
--- a/src/itdelatrisu/opsu/objects/Slider.java
+++ b/src/itdelatrisu/opsu/objects/Slider.java
@@ -205,6 +205,7 @@ public class Slider extends GameObject {
}
int timeDiff = hitObject.getTime() - trackPosition;
+ final int repeatCount = hitObject.getRepeatCount();
final int approachTime = game.getApproachTime();
final int fadeInTime = game.getFadeInTime();
float scale = timeDiff / (float) approachTime;
@@ -219,9 +220,12 @@ public class Slider extends GameObject {
Image hitCircle = GameImage.HITCIRCLE.getImage();
Vec2f endPos = curve.pointAt(1);
- float curveAlpha = 1f;
- if (GameMod.HIDDEN.isActive() && trackPosition > getTime()) {
- curveAlpha = Math.max(0f, 1f - ((float) (trackPosition - getTime()) / (getEndTime() - getTime())) * 1.05f);
+ float oldWhiteFadeAlpha = Colors.WHITE_FADE.a;
+ float sliderAlpha = 1f;
+ if (GameMod.HIDDEN.isActive() && trackPosition > hitObject.getTime()) {
+ // "Hidden" mod: fade out sliders
+ Colors.WHITE_FADE.a = color.a = sliderAlpha =
+ Math.max(0f, 1f - ((float) (trackPosition - hitObject.getTime()) / (getEndTime() - hitObject.getTime())) * 1.05f);
}
curveColor.a = curveAlpha;
@@ -232,19 +236,37 @@ public class Slider extends GameObject {
if (mirror) {
g.rotate(x, y, -180f);
}
+ // end circle (only draw if ball still has to go there)
+ if (curveInterval == 1f && currentRepeats < repeatCount - (repeatCount % 2 == 0 ? 1 : 0)) {
+ Color circleColor = new Color(color);
+ Color overlayColor = new Color(Colors.WHITE_FADE);
+ if (currentRepeats == 0) {
+ if (Options.isSliderSnaking()) {
+ // fade in end circle using decorationsAlpha when snaking sliders are enabled
+ circleColor.a = overlayColor.a = sliderAlpha * decorationsAlpha;
+ }
+ } else {
+ // fade in end circle after repeats
+ circleColor.a = overlayColor.a = sliderAlpha * getCircleAlphaAfterRepeat(trackPosition, true);
+ }
+ Vec2f endCircPos = curve.pointAt(curveInterval);
+ hitCircle.drawCentered(endCircPos.x, endCircPos.y, circleColor);
+ hitCircleOverlay.drawCentered(endCircPos.x, endCircPos.y, overlayColor);
+ }
- /*
- // end circle
- Vec2f endCircPos = curve.pointAt(curveInterval);
- hitCircle.drawCentered(endCircPos.x, endCircPos.y, color);
- hitCircleOverlay.drawCentered(endCircPos.x, endCircPos.y, Colors.WHITE_FADE);
- */
+ // set first circle colors to fade in after repeats
+ Color firstCircleColor = new Color(color);
+ Color startCircleOverlayColor = new Color(Colors.WHITE_FADE);
+ if (sliderClickedInitial) {
+ // fade in first circle after repeats
+ firstCircleColor.a = startCircleOverlayColor.a = sliderAlpha * getCircleAlphaAfterRepeat(trackPosition, false);
+ }
- // start circle, don't draw if already clicked
- if (!sliderClickedInitial) {
- hitCircle.drawCentered(x, y, color);
- if (!overlayAboveNumber)
- hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE);
+ // start circle, only draw if ball still has to go there
+ if (!sliderClickedInitial || currentRepeats < repeatCount - (repeatCount % 2 == 1 ? 1 : 0)) {
+ hitCircle.drawCentered(x, y, firstCircleColor);
+ if (!overlayAboveNumber || sliderClickedInitial)
+ hitCircleOverlay.drawCentered(x, y, startCircleOverlayColor);
}
g.popTransform();
@@ -252,7 +274,7 @@ public class Slider extends GameObject {
// ticks
if (ticksT != null) {
drawSliderTicks(g, trackPosition, alpha, decorationsAlpha, mirror);
- Colors.WHITE_FADE.a = alpha;
+ Colors.WHITE_FADE.a = oldWhiteFadeAlpha;
}
g.pushTransform();
@@ -269,38 +291,40 @@ public class Slider extends GameObject {
}
}
+ // draw combo number and overlay if not initially clicked
if (!sliderClickedInitial) {
data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
hitCircle.getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
- if (overlayAboveNumber)
- hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE);
+
+ if (overlayAboveNumber) {
+ startCircleOverlayColor.a = sliderAlpha;
+ hitCircleOverlay.drawCentered(x, y, startCircleOverlayColor);
+ }
}
g.popTransform();
// repeats
if (isCurveCompletelyDrawn) {
- for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) {
- if (hitObject.getRepeatCount() - 1 > tcurRepeat) {
- Image arrow = GameImage.REVERSEARROW.getImage();
- arrow = arrow.getScaledCopy((float) (1 + 0.2d * ((trackPosition + sliderTime * tcurRepeat) % 292) / 292));
- Color arrowColor = Color.white;
- if (tcurRepeat != currentRepeats) {
- if (sliderTime == 0)
- continue;
- float t = Math.max(getT(trackPosition, true), 0);
- arrow.setAlpha((float) (t - Math.floor(t)));
- } else
- arrow.setAlpha(Options.isSliderSnaking() ? decorationsAlpha : 1f);
- if (tcurRepeat % 2 == 0) {
- // last circle
- arrow.setRotation(curve.getEndAngle());
- arrow.drawCentered(endPos.x, endPos.y, arrowColor);
- } else {
- // first circle
- arrow.setRotation(curve.getStartAngle());
- arrow.drawCentered(x, y, arrowColor);
+ for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1 && tcurRepeat < repeatCount - 1; tcurRepeat++) {
+ Image arrow = GameImage.REVERSEARROW.getImage();
+ arrow = arrow.getScaledCopy((float) (1 + 0.2d * ((trackPosition + sliderTime * tcurRepeat) % 292) / 292));
+ if (tcurRepeat == 0) {
+ arrow.setAlpha(Options.isSliderSnaking() ? decorationsAlpha : 1f);
+ } else {
+ if (!sliderClickedInitial) {
+ continue;
}
+ arrow.setAlpha(getCircleAlphaAfterRepeat(trackPosition, tcurRepeat % 2 == 0));
+ }
+ if (tcurRepeat % 2 == 0) {
+ // last circle
+ arrow.setRotation(curve.getEndAngle());
+ arrow.drawCentered(endPos.x, endPos.y);
+ } else {
+ // first circle
+ arrow.setRotation(curve.getStartAngle());
+ arrow.drawCentered(x, y);
}
}
}
@@ -447,6 +471,24 @@ public class Slider extends GameObject {
return curveIntervalTo == 1d;
}
+ /**
+ * Get the alpha level used to fade in circles & reversearrows after repeat
+ * @param trackPosition current trackposition, in ms
+ * @param endCircle request alpha for end circle (true) or start circle (false)?
+ * @return alpha level as float in interval [0, 1]
+ */
+ private float getCircleAlphaAfterRepeat(int trackPosition, boolean endCircle) {
+ int ticksN = ticksT == null ? 0 : ticksT.length;
+ float t = getT(trackPosition, false);
+ if (endCircle) {
+ t = 1f - t;
+ }
+ if (currentRepeats % 2 == (endCircle ? 0 : 1)) {
+ t = 1f;
+ }
+ return Utils.clamp(t * (ticksN + 1), 0f, 1f);
+ }
+
/**
* Calculates the slider hit result.
* @return the hit result (GameData.HIT_* constants)
@@ -513,17 +555,19 @@ public class Slider extends GameObject {
float cx, cy;
HitObjectType type;
- if (currentRepeats % 2 == 0) { // last circle
+ if (currentRepeats % 2 == 0) {
+ // last circle
Vec2f lastPos = curve.pointAt(1);
cx = lastPos.x;
cy = lastPos.y;
type = HitObjectType.SLIDER_LAST;
- } else { // first circle
+ } else {
+ // first circle
cx = x;
cy = y;
type = HitObjectType.SLIDER_FIRST;
}
- data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result,
+ data.sendHitResult(hitObject.getTime() + (int) sliderTimeTotal, result,
cx, cy, color, comboEnd, hitObject, type, sliderHeldToEnd,
currentRepeats + 1, curve, sliderHeldToEnd);
if (Options.isMirror() && GameMod.AUTO.isActive()) {
@@ -550,15 +594,18 @@ public class Slider extends GameObject {
if (timeDiff < hitResultOffset[GameData.HIT_50]) {
result = GameData.HIT_SLIDER30;
ticksHit++;
- } else if (timeDiff < hitResultOffset[GameData.HIT_MISS])
+ data.sendSliderStartResult(trackPosition, this.x, this.y, color, true);
+ } else if (timeDiff < hitResultOffset[GameData.HIT_MISS]) {
result = GameData.HIT_MISS;
+ data.sendSliderStartResult(trackPosition, this.x, this.y, color, false);
+ }
//else not a hit
if (result > -1) {
data.sendInitialSliderResult(trackPosition, this.x, this.y, color, mirrorColor);
data.addHitError(hitObject.getTime(), x,y,trackPosition - hitObject.getTime());
sliderClickedInitial = true;
- data.sliderTickResult(hitObject.getTime(), result, this.x, this.y, hitObject, currentRepeats);
+ data.sendSliderTickResult(hitObject.getTime(), result, this.x, this.y, hitObject, currentRepeats);
return true;
}
}
@@ -579,9 +626,12 @@ public class Slider extends GameObject {
sliderClickedInitial = true;
if (isAutoMod) { // "auto" mod: catch any missed notes due to lag
ticksHit++;
- data.sliderTickResult(time, GameData.HIT_SLIDER30, x, y, hitObject, currentRepeats);
- } else
- data.sliderTickResult(time, GameData.HIT_MISS, x, y, hitObject, currentRepeats);
+ data.sendSliderTickResult(time, GameData.HIT_SLIDER30, x, y, hitObject, currentRepeats);
+ data.sendSliderStartResult(time, x, y, color, true);
+ } else {
+ data.sendSliderTickResult(time, GameData.HIT_MISS, x, y, hitObject, currentRepeats);
+ data.sendSliderStartResult(trackPosition, x, y, color, false);
+ }
}
// "auto" mod: send a perfect hit result
@@ -589,8 +639,8 @@ public class Slider extends GameObject {
if (Math.abs(trackPosition - time) < hitResultOffset[GameData.HIT_300]) {
ticksHit++;
sliderClickedInitial = true;
- data.sliderTickResult(time, GameData.HIT_SLIDER30, x, y, hitObject, currentRepeats);
- data.sendInitialSliderResult(time, x, y, color, mirrorColor);
+ data.sendSliderTickResult(time, GameData.HIT_SLIDER30, x, y, hitObject, currentRepeats);
+ data.sendSliderStartResult(time, x, y, color, true);
}
}
@@ -644,23 +694,6 @@ public class Slider extends GameObject {
tickIndex = 0;
isNewRepeat = true;
tickExpandTime = TICK_EXPAND_TIME;
-
- if (Options.isReverseArrowAnimationEnabled()) {
- // send hit result, to fade out reversearrow
- HitObjectType type;
- float posX, posY;
- if (currentRepeats % 2 == 1) {
- type = HitObjectType.SLIDER_LAST;
- Vec2f endPos = curve.pointAt(1);
- posX = endPos.x;
- posY = endPos.y;
- } else {
- type = HitObjectType.SLIDER_FIRST;
- posX = this.x;
- posY = this.y;
- }
- data.sendRepeatSliderResult(trackPosition, posX, posY, Color.white, curve, type);
- }
}
}
@@ -688,19 +721,34 @@ public class Slider extends GameObject {
// held during new repeat
if (isNewRepeat) {
ticksHit++;
- if (currentRepeats % 2 > 0) { // last circle
- int lastIndex = hitObject.getSliderX().length;
- data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30,
- curve.getX(lastIndex), curve.getY(lastIndex), hitObject, currentRepeats);
- } else // first circle
- data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30,
- c.x, c.y, hitObject, currentRepeats);
+
+ HitObjectType type;
+ float posX, posY;
+ if (currentRepeats % 2 > 0) {
+ // last circle
+ type = HitObjectType.SLIDER_LAST;
+ Vec2f endPos = curve.pointAt(1f);
+ posX = endPos.x;
+ posY = endPos.y;
+ } else {
+ // first circle
+ type = HitObjectType.SLIDER_FIRST;
+ posX = this.x;
+ posY = this.y;
+ }
+ data.sendSliderTickResult(trackPosition, GameData.HIT_SLIDER30,
+ posX, posY, hitObject, currentRepeats);
+
+ // fade out reverse arrow
+ float colorLuminance = Utils.getLuminance(color);
+ Color arrowColor = colorLuminance < 0.8f ? Color.white : Color.black;
+ data.sendSliderRepeatResult(trackPosition, posX, posY, arrowColor, curve, type);
}
// held during new tick
if (isNewTick) {
ticksHit++;
- data.sliderTickResult(trackPosition, GameData.HIT_SLIDER10,
+ data.sendSliderTickResult(trackPosition, GameData.HIT_SLIDER10,
c.x, c.y, hitObject, currentRepeats);
}
@@ -711,9 +759,9 @@ public class Slider extends GameObject {
followCircleActive = false;
if (isNewRepeat)
- data.sliderTickResult(trackPosition, GameData.HIT_MISS, 0, 0, hitObject, currentRepeats);
+ data.sendSliderTickResult(trackPosition, GameData.HIT_MISS, 0, 0, hitObject, currentRepeats);
if (isNewTick)
- data.sliderTickResult(trackPosition, GameData.HIT_MISS, 0, 0, hitObject, currentRepeats);
+ data.sendSliderTickResult(trackPosition, GameData.HIT_MISS, 0, 0, hitObject, currentRepeats);
}
return false;
diff --git a/src/itdelatrisu/opsu/objects/Spinner.java b/src/itdelatrisu/opsu/objects/Spinner.java
index 251f4e0d..551c80f8 100644
--- a/src/itdelatrisu/opsu/objects/Spinner.java
+++ b/src/itdelatrisu/opsu/objects/Spinner.java
@@ -255,7 +255,7 @@ public class Spinner extends GameObject {
else
result = GameData.HIT_MISS;
- data.hitResult(hitObject.getEndTime(), result, width / 2, height / 2,
+ data.sendHitResult(hitObject.getEndTime(), result, width / 2, height / 2,
Color.transparent, true, hitObject, HitObjectType.SPINNER, true, 0, null, false);
return result;
}
diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java
index 94b6c91e..abb2519a 100644
--- a/src/itdelatrisu/opsu/objects/curves/Curve.java
+++ b/src/itdelatrisu/opsu/objects/curves/Curve.java
@@ -101,12 +101,12 @@ public abstract class Curve {
Curve.borderColor = borderColor;
ContextCapabilities capabilities = GLContext.getCapabilities();
- mmsliderSupported = capabilities.OpenGL20;
+ mmsliderSupported = capabilities.OpenGL30;
if (mmsliderSupported)
CurveRenderState.init(width, height, circleDiameter);
else {
if (Options.getSkin().getSliderStyle() != Skin.STYLE_PEPPYSLIDER)
- Log.warn("New slider style requires OpenGL 2.0.");
+ Log.warn("New slider style requires OpenGL 3.0.");
}
}
diff --git a/src/itdelatrisu/opsu/states/ButtonMenu.java b/src/itdelatrisu/opsu/states/ButtonMenu.java
index da13dfbc..969e3e3c 100644
--- a/src/itdelatrisu/opsu/states/ButtonMenu.java
+++ b/src/itdelatrisu/opsu/states/ButtonMenu.java
@@ -46,8 +46,8 @@ import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame;
-import org.newdawn.slick.state.transition.FadeInTransition;
import org.newdawn.slick.state.transition.EmptyTransition;
+import org.newdawn.slick.state.transition.FadeInTransition;
/**
* Generic button menu state.
@@ -69,8 +69,8 @@ public class ButtonMenu extends BasicGameState {
Button.NO.click(container, game);
}
},
- /** The initial beatmap management screen. */
- BEATMAP (new Button[] { Button.CLEAR_SCORES, Button.DELETE, Button.CANCEL }) {
+ /** The initial beatmap management screen (for a non-"favorite" beatmap). */
+ BEATMAP (new Button[] { Button.CLEAR_SCORES, Button.FAVORITE_ADD, Button.DELETE, Button.CANCEL }) {
@Override
public String[] getTitle(GameContainer container, StateBasedGame game) {
BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
@@ -90,6 +90,23 @@ public class ButtonMenu extends BasicGameState {
super.scroll(container, game, newValue);
}
},
+ /** The initial beatmap management screen (for a "favorite" beatmap). */
+ BEATMAP_FAVORITE (new Button[] { Button.CLEAR_SCORES, Button.FAVORITE_REMOVE, Button.DELETE, Button.CANCEL }) {
+ @Override
+ public String[] getTitle(GameContainer container, StateBasedGame game) {
+ return BEATMAP.getTitle(container, game);
+ }
+
+ @Override
+ public void leave(GameContainer container, StateBasedGame game) {
+ BEATMAP.leave(container, game);
+ }
+
+ @Override
+ public void scroll(GameContainer container, StateBasedGame game, int newValue) {
+ BEATMAP.scroll(container, game, newValue);
+ }
+ },
/** The beatmap deletion screen for a beatmap set with multiple beatmaps. */
BEATMAP_DELETE_SELECT (new Button[] { Button.DELETE_GROUP, Button.DELETE_SONG, Button.CANCEL_DELETE }) {
@Override
@@ -468,6 +485,25 @@ public class ButtonMenu extends BasicGameState {
game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition());
}
},
+ FAVORITE_ADD ("Add to Favorites", Color.blue) {
+ @Override
+ public void click(GameContainer container, StateBasedGame game) {
+ SoundController.playSound(SoundEffect.MENUHIT);
+ BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
+ node.getBeatmapSet().setFavorite(true);
+ game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition());
+ }
+ },
+ FAVORITE_REMOVE ("Remove from Favorites", Color.blue) {
+ @Override
+ public void click(GameContainer container, StateBasedGame game) {
+ SoundController.playSound(SoundEffect.MENUHIT);
+ BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
+ node.getBeatmapSet().setFavorite(false);
+ ((SongMenu) game.getState(Opsu.STATE_SONGMENU)).doStateActionOnLoad(MenuState.BEATMAP_FAVORITE);
+ game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition());
+ }
+ },
DELETE ("Delete...", Color.red) {
@Override
public void click(GameContainer container, StateBasedGame game) {
diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java
index 7c249153..65ed00f0 100644
--- a/src/itdelatrisu/opsu/states/Game.java
+++ b/src/itdelatrisu/opsu/states/Game.java
@@ -49,6 +49,7 @@ import itdelatrisu.opsu.replay.PlaybackSpeed;
import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.replay.ReplayFrame;
import itdelatrisu.opsu.ui.*;
+import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.io.File;
@@ -98,6 +99,9 @@ public class Game extends BasicGameState {
/** Screen fade-out time, in milliseconds, when health hits zero. */
private static final int LOSE_FADEOUT_TIME = 500;
+ /** Game element fade-out time, in milliseconds, when the game ends. */
+ private static final int FINISHED_FADEOUT_TIME = 400;
+
/** Maximum rotation, in degrees, over fade out upon death. */
private static final float MAX_ROTATION = 90f;
@@ -287,6 +291,15 @@ public class Game extends BasicGameState {
/** The current alpha of the scoreboard. */
private float currentScoreboardAlpha;
+ /** The star stream shown when passing another score. */
+ private StarStream scoreboardStarStream;
+
+ /** Whether the game is finished (last hit object passed). */
+ private boolean gameFinished = false;
+
+ /** Timer after game has finished, before changing states. */
+ private AnimatedValue gameFinishedTimer = new AnimatedValue(2500, 0, 1, AnimationEquation.LINEAR);
+
/** Music position bar background colors. */
private static final Color
MUSICBAR_NORMAL = new Color(12, 9, 10, 0.25f),
@@ -369,6 +382,12 @@ public class Game extends BasicGameState {
musicBarWidth = Math.max(width * 0.005f, 7);
musicBarHeight = height * 0.9f;
+ // initialize scoreboard star stream
+ scoreboardStarStream = new StarStream(0, height * 2f / 3f, width / 4, 0, 0);
+ scoreboardStarStream.setPositionSpread(height / 20f);
+ scoreboardStarStream.setDirectionSpread(10f);
+ scoreboardStarStream.setDurationSpread(700, 100);
+
// create the associated GameData object
data = new GameData(width, height);
}
@@ -513,7 +532,7 @@ public class Game extends BasicGameState {
}
if (!Options.isHideUI() || !GameMod.AUTO.isActive()) {
- data.drawGameElements(g, true, objectIndex == 0);
+ data.drawGameElements(g, true, objectIndex == 0, 1f);
}
if (breakLength >= 8000 &&
@@ -552,7 +571,13 @@ public class Game extends BasicGameState {
else {
if (!GameMod.AUTO.isActive() || !Options.isHideUI()) {
// game elements
- data.drawGameElements(g, false, objectIndex == 0);
+ float gameElementAlpha = 1f;
+ if (gameFinished) {
+ // game finished: fade everything out
+ float t = 1f - Math.min(gameFinishedTimer.getTime() / (float) FINISHED_FADEOUT_TIME, 1f);
+ gameElementAlpha = AnimationEquation.OUT_CUBIC.calc(t);
+ }
+ data.drawGameElements(g, false, objectIndex == 0, gameElementAlpha);
// skip beginning
if (objectIndex == 0 &&
@@ -639,6 +664,7 @@ public class Game extends BasicGameState {
ScoreData currentScore = data.getCurrentScoreData(beatmap, true);
while (currentRank > 0 && previousScores[currentRank - 1].score < currentScore.score) {
currentRank--;
+ scoreboardStarStream.burst(20);
lastRankUpdateTime = trackPosition;
}
@@ -647,6 +673,9 @@ public class Game extends BasicGameState {
);
int scoreboardPosition = 2 * container.getHeight() / 3;
+ // draw star stream behind the scores
+ scoreboardStarStream.draw();
+
if (currentRank < 4) {
// draw the (new) top 5 ranks
for (int i = 0; i < 4; i++) {
@@ -749,6 +778,7 @@ public class Game extends BasicGameState {
playbackSpeed.getButton().hoverUpdate(delta, mouseX, mouseY);
int trackPosition = MusicController.getPosition();
int firstObjectTime = beatmap.objects[0].getTime();
+ scoreboardStarStream.update(delta);
// returning from pause screen: must click previous mouse position
if (pauseTime > -1) {
@@ -803,11 +833,11 @@ public class Game extends BasicGameState {
}
// normal game update
- if (!isReplay)
+ if (!isReplay && !gameFinished)
addReplayFrameAndRun(mouseX, mouseY, lastKeysPressed, trackPosition);
// watching replay
- else {
+ else if (!gameFinished) {
// out of frames, use previous data
if (replayIndex >= replay.frames.length)
updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed);
@@ -857,7 +887,8 @@ public class Game extends BasicGameState {
// update in-game scoreboard
if (!Options.isHideUI() && previousScores != null && trackPosition > firstObjectTime) {
// show scoreboard if selected, and always in break
- if (scoreboardVisible || breakTime > 0) {
+ // hide when game ends
+ if ((scoreboardVisible || breakTime > 0) && !gameFinished) {
currentScoreboardAlpha += 1f / SCOREBOARD_FADE_IN_TIME * delta;
if (currentScoreboardAlpha > 1f)
currentScoreboardAlpha = 1f;
@@ -869,6 +900,14 @@ public class Game extends BasicGameState {
}
data.updateDisplays(delta);
+
+ // game finished: change state after timer expires
+ if (gameFinished && !gameFinishedTimer.update(delta)) {
+ if (checkpointLoaded) // if checkpoint used, skip ranking screen
+ game.closeRequested();
+ else // go to ranking screen
+ game.enterState(Opsu.STATE_GAMERANKING, new EasedFadeOutTransition(), new FadeInTransition());
+ }
}
/**
@@ -892,12 +931,8 @@ public class Game extends BasicGameState {
if (MusicController.trackEnded() && objectIndex < gameObjects.length)
gameObjects[objectIndex].update(true, delta, mouseX, mouseY, false, trackPosition);
- // if checkpoint used, skip ranking screen
- if (checkpointLoaded)
- game.closeRequested();
-
- // go to ranking screen
- else {
+ // save score and replay
+ if (!checkpointLoaded) {
boolean unranked = (GameMod.AUTO.isActive() || GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive());
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
if (isReplay)
@@ -918,9 +953,12 @@ public class Game extends BasicGameState {
// add score to database
if (!unranked && !isReplay)
ScoreDB.addScore(score);
-
- game.enterState(Opsu.STATE_GAMERANKING, new EasedFadeOutTransition(), new FadeInTransition());
}
+
+ // start timer
+ gameFinished = true;
+ gameFinishedTimer.setTime(0);
+
return;
}
@@ -1050,6 +1088,8 @@ public class Game extends BasicGameState {
@Override
public void keyPressed(int key, char c) {
+ if (gameFinished)
+ return;
if (sbOverlay.keyPressed(key, c)) {
return;
@@ -1199,9 +1239,13 @@ public class Game extends BasicGameState {
@Override
public void mousePressed(int button, int x, int y) {
+ if (gameFinished)
+ return;
+
if (sbOverlay.mousePressed(button, x, y)) {
return;
}
+
// watching replay
if (isReplay || GameMod.AUTO.isActive()) {
if (button == Input.MOUSE_MIDDLE_BUTTON)
@@ -1294,6 +1338,9 @@ public class Game extends BasicGameState {
@Override
public void mouseReleased(int button, int x, int y) {
+ if (gameFinished)
+ return;
+
if (sbOverlay.mouseReleased(button, x, y)) {
return;
}
@@ -1315,6 +1362,9 @@ public class Game extends BasicGameState {
@Override
public void keyReleased(int key, char c) {
+ if (gameFinished)
+ return;
+
int keys = ReplayFrame.KEY_NONE;
if (key == Options.getGameKeyLeft())
keys = ReplayFrame.KEY_K1;
@@ -1343,7 +1393,7 @@ public class Game extends BasicGameState {
if (sbOverlay.mouseWheelMoved(newValue)) {
return;
}
- if (Options.isMouseWheelDisabled() || Options.isMouseDisabled())
+ if (Options.isMouseWheelDisabled())
return;
UI.changeVolume((newValue < 0) ? -1 : 1);
@@ -1375,6 +1425,11 @@ public class Game extends BasicGameState {
// restart the game
if (restart != Restart.FALSE) {
+ // update play stats
+ if (restart == Restart.NEW) {
+ beatmap.incrementPlayCounter();
+ BeatmapDB.updatePlayStatistics(beatmap);
+ }
// load epilepsy warning img
epiImgTime = Options.getEpilepsyWarningLength();
@@ -1400,11 +1455,13 @@ public class Game extends BasicGameState {
loadImages();
setMapModifiers();
retries = 0;
- } else if (restart == Restart.MANUAL) {
+ } else if (restart == Restart.MANUAL && !GameMod.AUTO.isActive()) {
// retry
retries++;
- } else if (restart == Restart.REPLAY)
+ } else if (restart == Restart.REPLAY || GameMod.AUTO.isActive()) {
+ // replay
retries = 0;
+ }
gameObjects = new GameObject[beatmap.objects.length];
playbackSpeed = PlaybackSpeed.NORMAL;
@@ -1613,6 +1670,11 @@ public class Game extends BasicGameState {
* @param trackPosition the track position
*/
private void drawHitObjects(Graphics g, int trackPosition) {
+ // draw result objects
+ if (!Options.isHideObjects()) {
+ data.drawHitResults(trackPosition);
+ }
+
if (Options.isMergingSliders() && knorkesliders != null) {
knorkesliders.draw(Color.white, this.slidercurveFrom, this.slidercurveTo);
if (Options.isMirror()) {
@@ -1622,6 +1684,7 @@ public class Game extends BasicGameState {
g.popTransform();
}
}
+
// include previous object in follow points
int lastObjectIndex = -1;
if (objectIndex > 0 && objectIndex < beatmap.objects.length &&
@@ -1736,18 +1799,13 @@ public class Game extends BasicGameState {
// translate and rotate the object
g.translate(0, dt * dt * container.getHeight());
- Vec2f rotationCenter = gameObj.getPointAt(beatmap.objects[idx].getTime());
+ Vec2f rotationCenter = gameObj.getPointAt((beatmap.objects[idx].getTime() + beatmap.objects[idx].getEndTime()) / 2);
g.rotate(rotationCenter.x, rotationCenter.y, rotSpeed * dt);
gameObj.draw(g, trackPosition, false);
g.popTransform();
}
}
-
- // draw result objects
- if (!Options.isHideObjects()) {
- data.drawHitResults(trackPosition);
- }
}
/**
@@ -1760,7 +1818,7 @@ public class Game extends BasicGameState {
}
this.beatmap = beatmap;
Display.setTitle(String.format("%s - %s", game.getTitle(), beatmap.toString()));
- if (beatmap.timingPoints == null)
+ if (beatmap.breaks == null)
BeatmapDB.load(beatmap, BeatmapDB.LOAD_ARRAY);
BeatmapParser.parseHitObjects(beatmap);
HitSound.setDefaultSampleSet(beatmap.sampleSet);
@@ -1792,6 +1850,9 @@ public class Game extends BasicGameState {
autoMousePosition = new Vec2f();
autoMousePressed = false;
flashlightRadius = container.getHeight() * 2 / 3;
+ scoreboardStarStream.clear();
+ gameFinished = false;
+ gameFinishedTimer.setTime(0);
System.gc();
}
diff --git a/src/itdelatrisu/opsu/states/GamePauseMenu.java b/src/itdelatrisu/opsu/states/GamePauseMenu.java
index 25447f47..c3c9dec1 100644
--- a/src/itdelatrisu/opsu/states/GamePauseMenu.java
+++ b/src/itdelatrisu/opsu/states/GamePauseMenu.java
@@ -184,7 +184,7 @@ public class GamePauseMenu extends BasicGameState {
@Override
public void mouseWheelMoved(int newValue) {
- if (Options.isMouseWheelDisabled() || Options.isMouseDisabled())
+ if (Options.isMouseWheelDisabled())
return;
UI.changeVolume((newValue < 0) ? -1 : 1);
diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java
index 6d2f8eca..29153854 100644
--- a/src/itdelatrisu/opsu/states/MainMenu.java
+++ b/src/itdelatrisu/opsu/states/MainMenu.java
@@ -32,8 +32,12 @@ import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.beatmap.TimingPoint;
import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
-import itdelatrisu.opsu.ui.*;
+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.StarFountain;
+import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
@@ -51,8 +55,8 @@ import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame;
-import org.newdawn.slick.state.transition.FadeInTransition;
import org.newdawn.slick.state.transition.EasedFadeOutTransition;
+import org.newdawn.slick.state.transition.FadeInTransition;
/**
* "Main Menu" state.
@@ -117,15 +121,18 @@ public class MainMenu extends BasicGameState {
/** Music position bar coordinates and dimensions. */
private float musicBarX, musicBarY, musicBarWidth, musicBarHeight;
+ /** Last measure progress value. */
+ private float lastMeasureProgress = 0f;
+
+ /** The star fountain. */
+ private StarFountain starFountain;
+
// game-related variables
private GameContainer container;
private StateBasedGame game;
private Input input;
private final int state;
- private float hue = 0;
- private boolean huedone = false;
-
public MainMenu(int state) {
this.state = state;
}
@@ -203,9 +210,6 @@ public class MainMenu extends BasicGameState {
repoButton.setHoverAnimationDuration(350);
repoButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
repoButton.setHoverExpand();
- }
-
- if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { // only if a webpage can be opened
Image repoImg = GameImage.REPOSITORY.getImage();
danceRepoButton = new MenuButton(repoImg,
startX - repoImg.getWidth(), startY - repoImg.getHeight()
@@ -228,6 +232,9 @@ public class MainMenu extends BasicGameState {
restartButton.setHoverAnimationEquation(AnimationEquation.LINEAR);
restartButton.setHoverRotate(360);
+ // initialize star fountain
+ starFountain = new StarFountain(width, height);
+
// logo animations
float centerOffsetX = width / 6.5f;
logoOpen = new AnimatedValue(100, 0, centerOffsetX, AnimationEquation.OUT_QUAD);
@@ -262,6 +269,9 @@ public class MainMenu extends BasicGameState {
g.fillRect(0, height * 8 / 9f, width, height / 9f);
Colors.BLACK_ALPHA.a = oldAlpha;
+ // draw star fountain
+ starFountain.draw();
+
// draw downloads button
downloadsButton.draw();
@@ -271,26 +281,15 @@ public class MainMenu extends BasicGameState {
exitButton.draw();
}
- // logo
- Double position = MusicController.getBeatProgress();
- Color color = Options.isColorMainMenuLogo() ? Cursor.lastCursorColor : Color.white;
- boolean renderPiece = position != null;
- if (position == null) {
- position = System.currentTimeMillis() % 1000 / 1000d;
- }
- double scale = 1 - (0 - position) * 0.05;
- logo.draw(color, (float) scale);
- if (renderPiece) {
- Image piece = GameImage.MENU_LOGO_PIECE.getImage().getScaledCopy(logo.getCurrentScale());
- float scaleposmodx = piece.getWidth() / 2;
- float scaleposmody = piece.getHeight() / 2;
- piece.rotate((float) (position * 360));
- piece.draw(logo.getX() - scaleposmodx, logo.getY() - scaleposmody, color);
- }
- Image logoCopy = GameImage.MENU_LOGO.getImage().getScaledCopy(logo.getCurrentScale() / (float) scale * 1.05f);
- float scaleposmodx = logoCopy.getWidth() / 2;
- float scaleposmody = logoCopy.getHeight() / 2;
- logoCopy.draw(logo.getX() - scaleposmodx, logo.getY() - scaleposmody, Colors.GHOST_LOGO);
+ // draw logo (pulsing)
+ Float position = MusicController.getBeatProgress();
+ if (position == null) // default to 60bpm
+ position = System.currentTimeMillis() % 1000 / 1000f;
+ float scale = 1f + position * 0.05f;
+ logo.draw(Color.white, scale);
+ float ghostScale = logo.getLastScale() / scale * 1.05f;
+ Image ghostLogo = GameImage.MENU_LOGO.getImage().getScaledCopy(ghostScale);
+ ghostLogo.drawCentered(logo.getX(), logo.getY(), Colors.GHOST_LOGO);
// draw music buttons
if (MusicController.isPlaying())
@@ -310,16 +309,13 @@ public class MainMenu extends BasicGameState {
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth * musicBarPosition, musicBarHeight, 4);
}
- // draw repository button
+ // draw repository buttons
if (repoButton != null) {
repoButton.draw();
String text = "opsu!";
int fheight = Fonts.SMALL.getLineHeight();
int fwidth = Fonts.SMALL.getWidth(text);
Fonts.SMALL.drawString(repoButton.getX() - fwidth / 2, repoButton.getY() - repoButton.getImage().getHeight() / 2 - fheight, text, Color.white);
- }
-
- if (danceRepoButton != null) {
danceRepoButton.draw();
String text = "opsu!dance";
int fheight = Fonts.SMALL.getLineHeight();
@@ -367,15 +363,15 @@ public class MainMenu extends BasicGameState {
throws SlickException {
UI.update(delta);
if (MusicController.trackEnded())
- nextTrack(); // end of track: go to next track
+ nextTrack(false); // end of track: go to next track
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
logo.hoverUpdate(delta, mouseX, mouseY, 0.25f);
playButton.hoverUpdate(delta, mouseX, mouseY, 0.25f);
exitButton.hoverUpdate(delta, mouseX, mouseY, 0.25f);
- if (repoButton != null)
+ if (repoButton != null) {
repoButton.hoverUpdate(delta, mouseX, mouseY);
- if (danceRepoButton != null)
danceRepoButton.hoverUpdate(delta, mouseX, mouseY);
+ }
if (Updater.get().showButton()) {
updateButton.autoHoverUpdate(delta, true);
restartButton.autoHoverUpdate(delta, false);
@@ -389,6 +385,7 @@ public class MainMenu extends BasicGameState {
noHoverUpdate |= contains;
musicNext.hoverUpdate(delta, !noHoverUpdate && musicNext.contains(mouseX, mouseY));
musicPrevious.hoverUpdate(delta, !noHoverUpdate && musicPrevious.contains(mouseX, mouseY));
+ starFountain.update(delta);
// window focus change: increase/decrease theme song volume
if (MusicController.isThemePlaying() &&
@@ -400,6 +397,14 @@ public class MainMenu extends BasicGameState {
if (!(Options.isDynamicBackgroundEnabled() && beatmap != null && beatmap.isBackgroundLoading()))
bgAlpha.update(delta);
+ // check measure progress
+ Float measureProgress = MusicController.getMeasureProgress(2);
+ if (measureProgress != null) {
+ if (measureProgress < lastMeasureProgress)
+ starFountain.burst(true);
+ lastMeasureProgress = measureProgress;
+ }
+
// buttons
int centerX = container.getWidth() / 2;
float currentLogoButtonAlpha;
@@ -472,6 +477,10 @@ public class MainMenu extends BasicGameState {
}
}
+ // reset measure info
+ lastMeasureProgress = 0f;
+ starFountain.clear();
+
// reset button hover states if mouse is not currently hovering over the button
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
if (!logo.contains(mouseX, mouseY, 0.25f))
@@ -514,6 +523,7 @@ public class MainMenu extends BasicGameState {
// music position bar
if (MusicController.isPlaying()) {
if (musicPositionBarContains(x, y)) {
+ lastMeasureProgress = 0f;
float pos = (x - musicBarX) / musicBarWidth;
MusicController.setPosition((int) (pos * MusicController.getDuration()));
return;
@@ -531,10 +541,11 @@ public class MainMenu extends BasicGameState {
}
return;
} else if (musicNext.contains(x, y)) {
- nextTrack();
+ nextTrack(true);
UI.sendBarNotification(">> Next");
return;
} else if (musicPrevious.contains(x, y)) {
+ lastMeasureProgress = 0f;
if (!previous.isEmpty()) {
SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
menu.setFocus(BeatmapSetList.get().getBaseNode(previous.pop()), -1, true, false);
@@ -657,7 +668,7 @@ public class MainMenu extends BasicGameState {
game.enterState(Opsu.STATE_DOWNLOADSMENU, new EasedFadeOutTransition(), new FadeInTransition());
break;
case Input.KEY_R:
- nextTrack();
+ nextTrack(true);
break;
case Input.KEY_UP:
UI.changeVolume(1);
@@ -717,9 +728,17 @@ public class MainMenu extends BasicGameState {
/**
* Plays the next track, and adds the previous one to the stack.
+ * @param user {@code true} if this was user-initiated, false otherwise (track end)
*/
- private void nextTrack() {
+ private void nextTrack(boolean user) {
+ lastMeasureProgress = 0f;
boolean isTheme = MusicController.isThemePlaying();
+ if (isTheme && !user) {
+ // theme was playing, restart
+ // NOTE: not looping due to inaccurate track positions after loop
+ MusicController.playAt(0, false);
+ return;
+ }
SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
BeatmapSetNode node = menu.setFocus(BeatmapSetList.get().getRandomNode(), -1, true, false);
boolean sameAudio = false;
diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java
index 9d9e8648..a0328350 100644
--- a/src/itdelatrisu/opsu/states/SongMenu.java
+++ b/src/itdelatrisu/opsu/states/SongMenu.java
@@ -32,6 +32,7 @@ import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.BeatmapDifficultyCalculator;
+import itdelatrisu.opsu.beatmap.BeatmapGroup;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.BeatmapSet;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
@@ -45,6 +46,7 @@ import itdelatrisu.opsu.db.BeatmapDB;
import itdelatrisu.opsu.db.ScoreDB;
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
import itdelatrisu.opsu.ui.Colors;
+import itdelatrisu.opsu.ui.DropdownMenu;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.KineticScrolling;
import itdelatrisu.opsu.ui.MenuButton;
@@ -259,8 +261,11 @@ public class SongMenu extends BasicGameState {
/** Header and footer end and start y coordinates, respectively. */
private float headerY, footerY;
- /** Height of the footer */
- private float footerHeight;
+ /** Footer pulsing logo button. */
+ private MenuButton footerLogoButton;
+
+ /** Size of the pulsing logo in the footer. */
+ private float footerLogoSize;
/** Time, in milliseconds, for fading the search bar. */
private int searchTransitionTimer = SEARCH_TRANSITION_TIME;
@@ -309,9 +314,15 @@ public class SongMenu extends BasicGameState {
/** The star stream. */
private StarStream starStream;
+ /** The maximum number of stars in the star stream. */
+ private static final int MAX_STREAM_STARS = 20;
+
/** Whether the menu is currently scrolling to the focus node (blocks other actions). */
private boolean isScrollingToFocusNode = false;
+ /** Sort order dropdown menu. */
+ private DropdownMenu sortMenu;
+
// game-related variables
private GameContainer container;
private StateBasedGame game;
@@ -337,11 +348,48 @@ public class SongMenu extends BasicGameState {
Fonts.BOLD.getLineHeight() + Fonts.DEFAULT.getLineHeight() +
Fonts.SMALL.getLineHeight();
footerY = height - GameImage.SELECTION_MODS.getImage().getHeight();
- footerHeight = height - footerY;
+
+ // footer logo coordinates
+ float footerHeight = height - footerY;
+ footerLogoSize = footerHeight * 3.25f;
+ Image logo = GameImage.MENU_LOGO.getImage();
+ logo = logo.getScaledCopy(footerLogoSize / logo.getWidth());
+ footerLogoButton = new MenuButton(logo, width - footerHeight * 0.8f, height - footerHeight * 0.65f);
+ footerLogoButton.setHoverAnimationDuration(1);
+ footerLogoButton.setHoverExpand(1.2f);
// initialize sorts
- for (BeatmapSortOrder sort : BeatmapSortOrder.values())
- sort.init(width, headerY - SongMenu.DIVIDER_LINE_WIDTH / 2);
+ int sortWidth = (int) (width * 0.12f);
+ sortMenu = new DropdownMenu(container, BeatmapSortOrder.values(),
+ width * 0.87f, headerY - GameImage.MENU_TAB.getImage().getHeight() * 2.25f, sortWidth) {
+ @Override
+ public void itemSelected(int index, BeatmapSortOrder item) {
+ BeatmapSortOrder.set(item);
+ if (focusNode == null)
+ return;
+ BeatmapSetNode oldFocusBase = BeatmapSetList.get().getBaseNode(focusNode.index);
+ int oldFocusFileIndex = focusNode.beatmapIndex;
+ focusNode = null;
+ BeatmapSetList.get().init();
+ SongMenu.this.setFocus(oldFocusBase, oldFocusFileIndex, true, true);
+ }
+
+ @Override
+ public boolean menuClicked(int index) {
+ if (isInputBlocked())
+ return false;
+
+ SoundController.playSound(SoundEffect.MENUCLICK);
+ return true;
+ }
+ };
+ sortMenu.setBackgroundColor(Colors.BLACK_BG_HOVER);
+ sortMenu.setBorderColor(Colors.BLUE_DIVIDER);
+ sortMenu.setChevronRightColor(Color.white);
+
+ // initialize group tabs
+ for (BeatmapGroup group : BeatmapGroup.values())
+ group.init(width, headerY - DIVIDER_LINE_WIDTH / 2);
// initialize score data buttons
ScoreData.init(width, headerY + height * 0.01f);
@@ -414,7 +462,9 @@ public class SongMenu extends BasicGameState {
});
// star stream
- starStream = new StarStream(width, height);
+ starStream = new StarStream(width, (height - GameImage.STAR.getImage().getHeight()) / 2, -width, 0, MAX_STREAM_STARS);
+ starStream.setPositionSpread(height / 20f);
+ starStream.setDirectionSpread(10f);
}
@Override
@@ -425,6 +475,7 @@ public class SongMenu extends BasicGameState {
int width = container.getWidth();
int height = container.getHeight();
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
+ boolean inDropdownMenu = sortMenu.contains(mouseX, mouseY);
// background
if (focusNode != null) {
@@ -496,7 +547,7 @@ public class SongMenu extends BasicGameState {
g.clearClip();
// scroll bar
- if (focusScores.length > MAX_SCORE_BUTTONS && ScoreData.areaContains(mouseX, mouseY))
+ if (focusScores.length > MAX_SCORE_BUTTONS && ScoreData.areaContains(mouseX, mouseY) && !inDropdownMenu)
ScoreData.drawScrollbar(g, startScorePos.getPosition(), focusScores.length * ScoreData.getButtonOffset());
}
@@ -510,25 +561,22 @@ public class SongMenu extends BasicGameState {
g.drawLine(0, footerY, width, footerY);
g.resetLineWidth();
- // opsu logo in bottom bar
- Image logo = GameImage.MENU_LOGO.getImage();
- float logoSize = footerHeight * 2f;
- logo = logo.getScaledCopy(logoSize / logo.getWidth());
- Double position = MusicController.getBeatProgress();
- float x = width - footerHeight * 0.61f;
- float y = height - footerHeight * 0.40f;
- if (position != null) {
- Image ghostLogo = logo.getScaledCopy((float) (1 - (0 - position) * 0.15));
- logo = logo.getScaledCopy((float) (1 - (position) * 0.15));
- logoSize = logo.getWidth();
- logo.draw(x - logoSize / 2, y - logoSize / 2);
- logoSize = ghostLogo.getWidth();
- float a = Colors.GHOST_LOGO.a;
- Colors.GHOST_LOGO.a *= (1d - position);
- ghostLogo.draw(x - logoSize / 2, y - logoSize / 2, Colors.GHOST_LOGO);
- Colors.GHOST_LOGO.a = a;
+ // footer logo (pulsing)
+ Float position = MusicController.getBeatProgress();
+ if (position == null) // default to 60bpm
+ position = System.currentTimeMillis() % 1000 / 1000f;
+ if (footerLogoButton.contains(mouseX, mouseY, 0.25f) && !inDropdownMenu) {
+ // hovering over logo: stop pulsing
+ footerLogoButton.draw();
} else {
- logo.draw(x - logoSize / 2, y - logoSize / 2);
+ float expand = position * 0.15f;
+ footerLogoButton.draw(Color.white, 1f - expand);
+ Image ghostLogo = GameImage.MENU_LOGO.getImage();
+ ghostLogo = ghostLogo.getScaledCopy((1f + expand) * footerLogoSize / ghostLogo.getWidth());
+ float oldGhostAlpha = Colors.GHOST_LOGO.a;
+ Colors.GHOST_LOGO.a *= (1f - position);
+ ghostLogo.drawCentered(footerLogoButton.getX(), footerLogoButton.getY(), Colors.GHOST_LOGO);
+ Colors.GHOST_LOGO.a = oldGhostAlpha;
}
// header
@@ -607,20 +655,22 @@ public class SongMenu extends BasicGameState {
GameImage.SELECTION_OTHER_OPTIONS.getImage().drawCentered(selectOptionsButton.getX(), selectOptionsButton.getY());
selectOptionsButton.draw();
- // sorting tabs
- BeatmapSortOrder currentSort = BeatmapSortOrder.getSort();
- BeatmapSortOrder hoverSort = null;
- for (BeatmapSortOrder sort : BeatmapSortOrder.values()) {
- if (sort.contains(mouseX, mouseY)) {
- hoverSort = sort;
- break;
+ // group tabs
+ BeatmapGroup currentGroup = BeatmapGroup.current();
+ BeatmapGroup hoverGroup = null;
+ if (!inDropdownMenu) {
+ for (BeatmapGroup group : BeatmapGroup.values()) {
+ if (group.contains(mouseX, mouseY)) {
+ hoverGroup = group;
+ break;
+ }
}
}
- for (BeatmapSortOrder sort : BeatmapSortOrder.VALUES_REVERSED) {
- if (sort != currentSort)
- sort.draw(false, sort == hoverSort);
+ for (BeatmapGroup group : BeatmapGroup.VALUES_REVERSED) {
+ if (group != currentGroup)
+ group.draw(false, group == hoverGroup);
}
- currentSort.draw(true, false);
+ currentGroup.draw(true, false);
// search
boolean searchEmpty = search.getText().isEmpty();
@@ -655,6 +705,9 @@ public class SongMenu extends BasicGameState {
(searchResultString == null) ? "Searching..." : searchResultString, Color.white);
}
+ // sorting options
+ sortMenu.render(container, g);
+
// reloading beatmaps
if (reloadThread != null) {
// darken the screen
@@ -678,20 +731,25 @@ public class SongMenu extends BasicGameState {
if (reloadThread == null)
MusicController.loopTrackIfEnded(true);
else if (reloadThread.isFinished()) {
+ BeatmapGroup.set(BeatmapGroup.ALL);
+ BeatmapSortOrder.set(BeatmapSortOrder.TITLE);
+ BeatmapSetList.get().reset();
+ BeatmapSetList.get().init();
if (BeatmapSetList.get().size() > 0) {
// initialize song list
- BeatmapSetList.get().init();
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
} else
MusicController.playThemeSong();
reloadThread = null;
}
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
+ boolean inDropdownMenu = sortMenu.contains(mouseX, mouseY);
UI.getBackButton().hoverUpdate(delta, mouseX, mouseY);
selectModsButton.hoverUpdate(delta, mouseX, mouseY);
selectRandomButton.hoverUpdate(delta, mouseX, mouseY);
selectMapOptionsButton.hoverUpdate(delta, mouseX, mouseY);
selectOptionsButton.hoverUpdate(delta, mouseX, mouseY);
+ footerLogoButton.hoverUpdate(delta, mouseX, mouseY, 0.25f);
// beatmap menu timer
if (beatmapMenuTimer > -1) {
@@ -699,7 +757,9 @@ public class SongMenu extends BasicGameState {
if (beatmapMenuTimer >= BEATMAP_MENU_DELAY) {
beatmapMenuTimer = -1;
if (focusNode != null) {
- ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.BEATMAP, focusNode);
+ MenuState state = focusNode.getBeatmapSet().isFavorite() ?
+ MenuState.BEATMAP_FAVORITE : MenuState.BEATMAP;
+ ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(state, focusNode);
game.enterState(Opsu.STATE_BUTTONMENU);
}
return;
@@ -788,7 +848,7 @@ public class SongMenu extends BasicGameState {
// mouse hover
BeatmapSetNode node = getNodeAtPosition(mouseX, mouseY);
- if (node != null) {
+ if (node != null && !inDropdownMenu) {
if (node == hoverIndex)
hoverOffset.update(delta);
else {
@@ -802,7 +862,9 @@ public class SongMenu extends BasicGameState {
}
// tooltips
- if (focusScores != null && ScoreData.areaContains(mouseX, mouseY)) {
+ if (sortMenu.baseContains(mouseX, mouseY))
+ UI.updateTooltip(delta, "Sort by...", false);
+ else if (focusScores != null && ScoreData.areaContains(mouseX, mouseY)) {
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);
@@ -880,25 +942,42 @@ public class SongMenu extends BasicGameState {
return;
}
- if (focusNode == null)
- return;
-
- // sorting buttons
- for (BeatmapSortOrder sort : BeatmapSortOrder.values()) {
- if (sort.contains(x, y)) {
- if (sort != BeatmapSortOrder.getSort()) {
- BeatmapSortOrder.setSort(sort);
+ // group tabs
+ for (BeatmapGroup group : BeatmapGroup.values()) {
+ if (group.contains(x, y)) {
+ if (group != BeatmapGroup.current()) {
+ BeatmapGroup.set(group);
SoundController.playSound(SoundEffect.MENUCLICK);
- BeatmapSetNode oldFocusBase = BeatmapSetList.get().getBaseNode(focusNode.index);
- int oldFocusFileIndex = focusNode.beatmapIndex;
- focusNode = null;
+ startNode = focusNode = null;
+ oldFocusNode = null;
+ randomStack = new Stack();
+ songInfo = null;
+ scoreMap = null;
+ focusScores = null;
+ search.setText("");
+ searchTimer = SEARCH_DELAY;
+ searchTransitionTimer = SEARCH_TRANSITION_TIME;
+ searchResultString = null;
+ BeatmapSetList.get().reset();
BeatmapSetList.get().init();
- setFocus(oldFocusBase, oldFocusFileIndex, true, true);
+ setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
+
+ if (BeatmapSetList.get().size() < 1 && group.getEmptyMessage() != null)
+ UI.sendBarNotification(group.getEmptyMessage());
}
return;
}
}
+ if (focusNode == null)
+ return;
+
+ // logo: start game
+ if (footerLogoButton.contains(x, y, 0.25f)) {
+ startGame();
+ return;
+ }
+
// song buttons
BeatmapSetNode node = getNodeAtPosition(x, y);
if (node != null) {
@@ -978,6 +1057,7 @@ public class SongMenu extends BasicGameState {
search.setText("");
searchTimer = SEARCH_DELAY;
searchTransitionTimer = 0;
+ searchResultString = null;
} else {
// return to main menu
SoundController.playSound(SoundEffect.MENUBACK);
@@ -999,7 +1079,11 @@ public class SongMenu extends BasicGameState {
SongNode prev;
if (randomStack.isEmpty() || (prev = randomStack.pop()) == null)
break;
- setFocus(prev.getNode(), prev.getIndex(), true, true);
+ BeatmapSetNode node = prev.getNode();
+ int expandedIndex = BeatmapSetList.get().getExpandedIndex();
+ if (node.index == expandedIndex)
+ node = node.next; // move past base node
+ setFocus(node, prev.getIndex(), true, true);
} else {
// random track, add previous to stack
randomStack.push(new SongNode(BeatmapSetList.get().getBaseNode(focusNode.index), focusNode.beatmapIndex));
@@ -1010,7 +1094,9 @@ public class SongMenu extends BasicGameState {
if (focusNode == null)
break;
SoundController.playSound(SoundEffect.MENUHIT);
- ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.BEATMAP, focusNode);
+ MenuState state = focusNode.getBeatmapSet().isFavorite() ?
+ MenuState.BEATMAP_FAVORITE : MenuState.BEATMAP;
+ ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(state, focusNode);
game.enterState(Opsu.STATE_BUTTONMENU);
break;
case Input.KEY_F5:
@@ -1045,11 +1131,6 @@ public class SongMenu extends BasicGameState {
case Input.KEY_ENTER:
if (focusNode == null)
break;
- if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) {
- // turn on "auto" mod
- if (!GameMod.AUTO.isActive())
- GameMod.AUTO.toggle(true);
- }
startGame();
break;
case Input.KEY_DOWN:
@@ -1194,6 +1275,8 @@ public class SongMenu extends BasicGameState {
songChangeTimer.setTime(songChangeTimer.getDuration());
musicIconBounceTimer.setTime(musicIconBounceTimer.getDuration());
starStream.clear();
+ sortMenu.activate();
+ sortMenu.reset();
// reset song stack
randomStack = new Stack();
@@ -1241,6 +1324,15 @@ public class SongMenu extends BasicGameState {
focusScores = getScoreDataForNode(focusNode, true);
}
+ // re-sort (in case play count updated)
+ if (BeatmapSortOrder.current() == BeatmapSortOrder.PLAYS) {
+ BeatmapSetNode oldFocusBase = BeatmapSetList.get().getBaseNode(focusNode.index);
+ int oldFocusFileIndex = focusNode.beatmapIndex;
+ focusNode = null;
+ BeatmapSetList.get().init();
+ setFocus(oldFocusBase, oldFocusFileIndex, true, true);
+ }
+
resetGame = false;
}
@@ -1325,6 +1417,19 @@ public class SongMenu extends BasicGameState {
case RELOAD: // reload beatmaps
reloadBeatmaps(true);
break;
+ case BEATMAP_FAVORITE: // removed favorite, reset beatmap list
+ if (BeatmapGroup.current() == BeatmapGroup.FAVORITE) {
+ startNode = focusNode = null;
+ oldFocusNode = null;
+ randomStack = new Stack();
+ songInfo = null;
+ scoreMap = null;
+ focusScores = null;
+ BeatmapSetList.get().reset();
+ BeatmapSetList.get().init();
+ setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
+ }
+ break;
default:
break;
}
@@ -1338,6 +1443,7 @@ public class SongMenu extends BasicGameState {
public void leave(GameContainer container, StateBasedGame game)
throws SlickException {
search.setFocus(false);
+ sortMenu.deactivate();
}
/**
@@ -1430,7 +1536,8 @@ public class SongMenu extends BasicGameState {
focusNode = BeatmapSetList.get().getNode(node, beatmapIndex);
Beatmap beatmap = focusNode.getSelectedBeatmap();
if (beatmap.timingPoints == null) {
- BeatmapParser.parseOnlyTimingPoints(beatmap);
+ // parse timing points so we can pulse the logo
+ BeatmapParser.parseTimingPoints(beatmap);
}
MusicController.play(beatmap, false, preview);
@@ -1670,12 +1777,19 @@ public class SongMenu extends BasicGameState {
if (MusicController.isTrackLoading())
return;
- SoundController.playSound(SoundEffect.MENUHIT);
Beatmap beatmap = MusicController.getBeatmap();
if (focusNode == null || beatmap != focusNode.getSelectedBeatmap()) {
UI.sendBarNotification("Unable to load the beatmap audio.");
return;
}
+
+ // turn on "auto" mod if holding "ctrl" key
+ if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) {
+ if (!GameMod.AUTO.isActive())
+ GameMod.AUTO.toggle(true);
+ }
+
+ SoundController.playSound(SoundEffect.MENUHIT);
MultiClip.destroyExtraClips();
Game gameState = (Game) game.getState(Opsu.STATE_GAME);
gameState.loadBeatmap(beatmap);
diff --git a/src/itdelatrisu/opsu/ui/Colors.java b/src/itdelatrisu/opsu/ui/Colors.java
index 70a836d7..3b133d9c 100644
--- a/src/itdelatrisu/opsu/ui/Colors.java
+++ b/src/itdelatrisu/opsu/ui/Colors.java
@@ -33,6 +33,7 @@ public class Colors {
BLUE_BACKGROUND = new Color(74, 130, 255),
BLUE_BUTTON = new Color(40, 129, 237),
ORANGE_BUTTON = new Color(200, 90, 3),
+ PINK_BUTTON = new Color(223, 71, 147),
YELLOW_ALPHA = new Color(255, 255, 0, 0.4f),
WHITE_FADE = new Color(255, 255, 255, 1f),
RED_HOVER = new Color(255, 112, 112),
@@ -48,8 +49,7 @@ public class Colors {
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),
- GHOST_LOGO = new Color(1.0f, 1.0f, 1.0f, 0.25f);
-
+ GHOST_LOGO = new Color(1.0f, 1.0f, 1.0f, 0.25f);
// 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 98c01e53..7ad8171d 100644
--- a/src/itdelatrisu/opsu/ui/Cursor.java
+++ b/src/itdelatrisu/opsu/ui/Cursor.java
@@ -320,8 +320,6 @@ public class Cursor {
// reset angles
cursorAngle = 0f;
- GameImage.CURSOR.getImage().setRotation(0f);
- GameImage.CURSOR_TRAIL.getImage().setRotation(0f);
}
/**
diff --git a/src/itdelatrisu/opsu/ui/DropdownMenu.java b/src/itdelatrisu/opsu/ui/DropdownMenu.java
index 059751a0..5204c257 100644
--- a/src/itdelatrisu/opsu/ui/DropdownMenu.java
+++ b/src/itdelatrisu/opsu/ui/DropdownMenu.java
@@ -95,6 +95,9 @@ public class DropdownMenu extends AbstractComponent {
/** The chevron images. */
private Image chevronDown, chevronRight;
+ /** Should the next click be blocked? */
+ private boolean blockClick = false;
+
/**
* Creates a new dropdown menu.
* @param container the container rendering this menu
@@ -327,6 +330,7 @@ public class DropdownMenu extends AbstractComponent {
this.expanded = false;
this.lastUpdateTime = 0;
expandProgress.setTime(0);
+ blockClick = false;
}
@Override
@@ -349,9 +353,21 @@ public class DropdownMenu extends AbstractComponent {
this.itemIndex = idx;
itemSelected(idx, items[idx]);
}
+ blockClick = true;
consumeEvent();
}
+ @Override
+ public void mouseClicked(int button, int x, int y, int clickCount) {
+ if (!active)
+ return;
+
+ if (blockClick) {
+ blockClick = false;
+ consumeEvent();
+ }
+ }
+
/**
* Notification that a new item was selected (via override).
* @param index the index of the item selected
diff --git a/src/itdelatrisu/opsu/ui/MenuButton.java b/src/itdelatrisu/opsu/ui/MenuButton.java
index 1a71c536..a155d270 100644
--- a/src/itdelatrisu/opsu/ui/MenuButton.java
+++ b/src/itdelatrisu/opsu/ui/MenuButton.java
@@ -98,11 +98,8 @@ public class MenuButton {
/** The default max rotation angle of the button. */
private static final float DEFAULT_ANGLE_MAX = 30f;
- private float currentScale = 1f;
-
- public float getCurrentScale() {
- return currentScale;
- }
+ /** The last scale at which the button was drawn. */
+ private float lastScale = 1f;
/**
* Creates a new button from an Image.
@@ -172,6 +169,11 @@ public class MenuButton {
*/
public float getY() { return y; }
+ /**
+ * Returns the last scale at which the button was drawn.
+ */
+ public float getLastScale() { return lastScale; }
+
/**
* Sets text to draw in the middle of the button.
* @param text the text to draw
@@ -197,21 +199,21 @@ public class MenuButton {
/**
* Draws the button.
*/
- public void draw() { draw(Color.white, 1.0f); }
+ public void draw() { draw(Color.white, 1f); }
/**
* Draws the button with a color filter.
* @param filter the color to filter with when drawing
*/
- public void draw(Color filter) { draw(filter, 1.0f); }
+ public void draw(Color filter) { draw(filter, 1f); }
/**
- * Draw the button with a color filter and scale.
- * @param filter the color to filter with when drawing
- * @param scaleoverride the scale to use when drawing
+ * Draw the button with a color filter at the given scale.
+ * @param filter the color to filter with when drawing
+ * @param scaleOverride the scale to use when drawing (only works for normal images)
*/
@SuppressWarnings("deprecation")
- public void draw(Color filter, float scaleoverride) {
+ public void draw(Color filter, float scaleOverride) {
// animations: get current frame
Image image = this.img;
if (image == null) {
@@ -219,20 +221,17 @@ public class MenuButton {
image = anim.getCurrentFrame();
}
- currentScale = 1f;
-
// normal images
if (imgL == null) {
- float scaleposmodx = 0;
- float scaleposmody = 0;
- if (scaleoverride != 1f) {
- image = image.getScaledCopy(scaleoverride);
- scaleposmodx = image.getWidth() / 2 - xRadius;
- scaleposmody = image.getHeight() / 2 - yRadius;
- currentScale = scaleoverride;
+ float xScaleOffset = 0f, yScaleOffset = 0f;
+ if (scaleOverride != 1f) {
+ image = image.getScaledCopy(scaleOverride);
+ xScaleOffset = image.getWidth() / 2f - xRadius;
+ yScaleOffset = image.getHeight() / 2f - yRadius;
}
+ lastScale = scaleOverride;
if (hoverEffect == 0)
- image.draw(x - xRadius - scaleposmodx, y - yRadius - scaleposmody, filter);
+ image.draw(x - xRadius, y - yRadius, filter);
else {
float oldAlpha = image.getAlpha();
float oldAngle = image.getRotation();
@@ -240,16 +239,18 @@ public class MenuButton {
if (scale.getValue() != 1f) {
image = image.getScaledCopy(scale.getValue());
image.setAlpha(oldAlpha);
- scaleposmodx = image.getWidth() / 2 - xRadius;
- scaleposmody = image.getHeight() / 2 - yRadius;
- currentScale *= scale.getValue();
+ if (scaleOverride != 1f) {
+ xScaleOffset = image.getWidth() / 2f - xRadius;
+ yScaleOffset = image.getHeight() / 2f - yRadius;
+ }
+ lastScale *= scale.getValue();
}
}
if ((hoverEffect & EFFECT_FADE) > 0)
image.setAlpha(alpha.getValue());
if ((hoverEffect & EFFECT_ROTATE) > 0)
image.setRotation(angle.getValue());
- image.draw(x - xRadius - scaleposmodx, y - yRadius - scaleposmody, filter);
+ image.draw(x - xRadius - xScaleOffset, y - yRadius - yScaleOffset, filter);
if (image == this.img) {
image.setAlpha(oldAlpha);
image.setRotation(oldAngle);
diff --git a/src/itdelatrisu/opsu/ui/StarFountain.java b/src/itdelatrisu/opsu/ui/StarFountain.java
new file mode 100644
index 00000000..3686bf6e
--- /dev/null
+++ b/src/itdelatrisu/opsu/ui/StarFountain.java
@@ -0,0 +1,143 @@
+package itdelatrisu.opsu.ui;
+
+import itdelatrisu.opsu.GameImage;
+import itdelatrisu.opsu.ui.animations.AnimatedValue;
+import itdelatrisu.opsu.ui.animations.AnimationEquation;
+
+import org.newdawn.slick.Image;
+
+/**
+ * Star fountain consisting of two star streams.
+ */
+public class StarFountain {
+ /** The (approximate) number of stars in each burst. */
+ private static final int BURST_SIZE = 125;
+
+ /** Star streams. */
+ private final StarStream left, right;
+
+ /** Burst progress. */
+ private final AnimatedValue burstProgress = new AnimatedValue(1000, 0, 1, AnimationEquation.LINEAR);
+
+ /** The maximum direction offsets. */
+ private final float xDirection, yDirection;
+
+ /** Motion types. */
+ private enum Motion {
+ NONE {
+ @Override
+ public void init(StarFountain fountain) {
+ fountain.left.setDirection(0, fountain.yDirection);
+ fountain.right.setDirection(0, fountain.yDirection);
+ fountain.left.setDirectionSpread(20f);
+ fountain.right.setDirectionSpread(20f);
+ fountain.left.setDurationSpread(1000, 200);
+ fountain.right.setDurationSpread(1000, 200);
+ }
+ },
+ OUTWARD_SWEEP {
+ @Override
+ public void init(StarFountain fountain) {
+ fountain.left.setDirectionSpread(0f);
+ fountain.right.setDirectionSpread(0f);
+ fountain.left.setDurationSpread(850, 0);
+ fountain.right.setDurationSpread(850, 0);
+ }
+
+ @Override
+ public void update(StarFountain fountain) {
+ float t = fountain.burstProgress.getValue();
+ fountain.left.setDirection(fountain.xDirection - fountain.xDirection * 2f * t, fountain.yDirection);
+ fountain.right.setDirection(-fountain.xDirection + fountain.xDirection * 2f * t, fountain.yDirection);
+ }
+ },
+ INWARD_SWEEP {
+ @Override
+ public void init(StarFountain fountain) { OUTWARD_SWEEP.init(fountain); }
+
+ @Override
+ public void update(StarFountain fountain) {
+ float t = fountain.burstProgress.getValue();
+ fountain.left.setDirection(-fountain.xDirection + fountain.xDirection * 2f * t, fountain.yDirection);
+ fountain.right.setDirection(fountain.xDirection - fountain.xDirection * 2f * t, fountain.yDirection);
+ }
+ };
+
+ /** Initializes the streams in the fountain. */
+ public void init(StarFountain fountain) {}
+
+ /** Updates the streams in the fountain. */
+ public void update(StarFountain fountain) {}
+ }
+
+ /** The current motion. */
+ private Motion motion = Motion.NONE;
+
+ /**
+ * Initializes the star fountain.
+ * @param containerWidth the container width
+ * @param containerHeight the container height
+ */
+ public StarFountain(int containerWidth, int containerHeight) {
+ Image img = GameImage.STAR2.getImage();
+ float xOffset = containerWidth * 0.125f;
+ this.xDirection = containerWidth / 2f - xOffset;
+ this.yDirection = -containerHeight * 0.85f;
+ this.left = new StarStream(xOffset - img.getWidth() / 2f, containerHeight, 0, yDirection, 0);
+ this.right = new StarStream(containerWidth - xOffset - img.getWidth() / 2f, containerHeight, 0, yDirection, 0);
+ left.setScaleSpread(1.1f, 0.2f);
+ right.setScaleSpread(1.1f, 0.2f);
+ }
+
+ /**
+ * Draws the star fountain.
+ */
+ public void draw() {
+ left.draw();
+ right.draw();
+ }
+
+ /**
+ * Updates the stars in the fountain by a delta interval.
+ * @param delta the delta interval since the last call
+ */
+ public void update(int delta) {
+ left.update(delta);
+ right.update(delta);
+
+ if (burstProgress.update(delta)) {
+ motion.update(this);
+ int size = Math.round((float) delta / burstProgress.getDuration() * BURST_SIZE);
+ left.burst(size);
+ right.burst(size);
+ }
+ }
+
+ /**
+ * Creates a burst of stars to be processed during the next {@link #update(int)} call.
+ * @param wait if {@code true}, will not burst if a previous burst is in progress
+ */
+ public void burst(boolean wait) {
+ if (wait && (burstProgress.getTime() < burstProgress.getDuration() || !left.isEmpty() || !right.isEmpty()))
+ return; // previous burst in progress
+
+ burstProgress.setTime(0);
+
+ Motion lastMotion = motion;
+ motion = Motion.values()[(int) (Math.random() * Motion.values().length)];
+ if (motion == lastMotion) // don't do the same sweep twice
+ motion = Motion.NONE;
+ motion.init(this);
+ }
+
+ /**
+ * Clears the stars currently in the fountain.
+ */
+ public void clear() {
+ left.clear();
+ right.clear();
+ burstProgress.setTime(burstProgress.getDuration());
+ motion = Motion.NONE;
+ motion.init(this);
+ }
+}
diff --git a/src/itdelatrisu/opsu/ui/StarStream.java b/src/itdelatrisu/opsu/ui/StarStream.java
index 35bb6acd..0bd11480 100644
--- a/src/itdelatrisu/opsu/ui/StarStream.java
+++ b/src/itdelatrisu/opsu/ui/StarStream.java
@@ -20,6 +20,7 @@ package itdelatrisu.opsu.ui;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
@@ -31,11 +32,35 @@ import java.util.Random;
import org.newdawn.slick.Image;
/**
- * Horizontal star stream.
+ * Star stream.
*/
public class StarStream {
- /** The container dimensions. */
- private final int containerWidth, containerHeight;
+ /** The origin of the star stream. */
+ private final Vec2f position;
+
+ /** The direction of the star stream. */
+ private final Vec2f direction;
+
+ /** The maximum number of stars to draw at once. */
+ private final int maxStars;
+
+ /** The spread of the stars' starting position. */
+ private float positionSpread = 0f;
+
+ /** The spread of the stars' direction. */
+ private float directionSpread = 0f;
+
+ /** The base (mean) duration for which stars are shown, in ms. */
+ private int durationBase = 1300;
+
+ /** The spread of the stars' duration, in ms. */
+ private int durationSpread = 300;
+
+ /** The base (mean) scale at which stars are drawn. */
+ private float scaleBase = 1f;
+
+ /** The spread of the stars' scale.*/
+ private float scaleSpread = 0f;
/** The star image. */
private final Image starImg;
@@ -43,33 +68,41 @@ public class StarStream {
/** 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 position offset. */
+ private final Vec2f offset;
+
+ /** The star direction vector. */
+ private final Vec2f dir;
+
+ /** The star image rotation angle. */
+ private final int angle;
+
+ /** The star image scale. */
+ private final float scale;
+
/** 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 offset the position offset vector
+ * @param direction the direction vector
+ * @param angle the image rotation angle
+ * @param scale the image scale
* @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;
+ public Star(Vec2f offset, Vec2f direction, int angle, float scale, int duration, AnimationEquation eqn) {
+ this.offset = offset;
+ this.dir = direction;
this.angle = angle;
+ this.scale = scale;
+ this.animatedValue = new AnimatedValue(duration, 0f, 1f, eqn);
}
/**
@@ -79,8 +112,9 @@ public class StarStream {
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);
+ offset.x + t * dir.x, offset.y + t * dir.y,
+ starImg.getWidth() * scale, starImg.getHeight() * scale, angle
+ );
}
/**
@@ -93,17 +127,60 @@ public class StarStream {
/**
* Initializes the star stream.
- * @param width the container width
- * @param height the container height
+ * @param x the x position
+ * @param y the y position
+ * @param dirX the x-axis direction
+ * @param dirY the y-axis direction
+ * @param k the maximum number of stars to draw at once (excluding bursts)
*/
- public StarStream(int width, int height) {
- this.containerWidth = width;
- this.containerHeight = height;
+ public StarStream(float x, float y, float dirX, float dirY, int k) {
+ this.position = new Vec2f(x, y);
+ this.direction = new Vec2f(dirX, dirY);
+ this.maxStars = k;
this.starImg = GameImage.STAR2.getImage().copy();
- this.stars = new ArrayList();
+ this.stars = new ArrayList(k);
this.random = new Random();
}
+ /**
+ * Set the direction spread of this star stream.
+ * @param spread the spread of the stars' starting position
+ */
+ public void setPositionSpread(float spread) { this.positionSpread = spread; }
+
+ /**
+ * Sets the direction of this star stream.
+ * @param dirX the new x-axis direction
+ * @param dirY the new y-axis direction
+ */
+ public void setDirection(float dirX, float dirY) { direction.set(dirX, dirY); }
+
+ /**
+ * Set the direction spread of this star stream.
+ * @param spread the spread of the stars' direction
+ */
+ public void setDirectionSpread(float spread) { this.directionSpread = spread; }
+
+ /**
+ * Sets the duration base and spread of this star stream.
+ * @param base the base (mean) duration for which stars are shown, in ms
+ * @param spread the spread of the stars' duration, in ms
+ */
+ public void setDurationSpread(int base, int spread) {
+ this.durationBase = base;
+ this.durationSpread = spread;
+ }
+
+ /**
+ * Sets the scale base and spread of this star stream.
+ * @param base the base (mean) scale at which stars are drawn
+ * @param spread the spread of the stars' scale
+ */
+ public void setScaleSpread(float base, float spread) {
+ this.scaleBase = base;
+ this.scaleSpread = spread;
+ }
+
/**
* Draws the star stream.
*/
@@ -131,27 +208,48 @@ public class StarStream {
}
// create new stars
- for (int i = stars.size(); i < MAX_STARS; i++) {
- if (Math.random() < ((i < 5) ? 0.25 : 0.66))
- break;
+ for (int i = stars.size(); i < maxStars; i++) {
+ if (Math.random() < ((i < maxStars / 4) ? 0.25 : 0.66))
+ break; // stagger spawning new stars
- // 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, yOffset, angle, eqn));
+ stars.add(createStar());
}
}
+ /**
+ * Creates a new star with randomized properties.
+ */
+ private Star createStar() {
+ float distanceRatio = Utils.clamp((float) getGaussian(0.65, 0.25), 0.2f, 0.925f);
+ Vec2f offset = position.cpy().add(direction.cpy().nor().normalize().scale((float) getGaussian(0, positionSpread)));
+ Vec2f dir = direction.cpy().scale(distanceRatio).add((float) getGaussian(0, directionSpread), (float) getGaussian(0, directionSpread));
+ int angle = (int) getGaussian(0, 22.5);
+ float scale = (float) getGaussian(scaleBase, scaleSpread);
+ int duration = Math.max(0, (int) (distanceRatio * getGaussian(durationBase, durationSpread)));
+ AnimationEquation eqn = random.nextBoolean() ? AnimationEquation.IN_OUT_QUAD : AnimationEquation.OUT_QUAD;
+
+ return new Star(offset, dir, angle, scale, duration, eqn);
+ }
+
+ /**
+ * Creates a burst of stars instantly.
+ * @param count the number of stars to create
+ */
+ public void burst(int count) {
+ for (int i = 0; i < count; i++)
+ stars.add(createStar());
+ }
+
/**
* Clears the stars currently in the stream.
*/
public void clear() { stars.clear(); }
+ /**
+ * Returns whether there are any stars currently in this stream.
+ */
+ public boolean isEmpty() { return stars.isEmpty(); }
+
/**
* Returns the next pseudorandom, Gaussian ("normally") distributed {@code double} value
* with the given mean and standard deviation.