Merge remote-tracking branch 'remotes/original/master' into upstream

# Conflicts:
#	src/itdelatrisu/opsu/Container.java
#	src/itdelatrisu/opsu/GameData.java
#	src/itdelatrisu/opsu/Options.java
#	src/itdelatrisu/opsu/audio/MusicController.java
#	src/itdelatrisu/opsu/objects/Circle.java
#	src/itdelatrisu/opsu/objects/Slider.java
#	src/itdelatrisu/opsu/render/CurveRenderState.java
#	src/itdelatrisu/opsu/states/Game.java
#	src/itdelatrisu/opsu/states/MainMenu.java
#	src/itdelatrisu/opsu/states/SongMenu.java
#	src/itdelatrisu/opsu/ui/Colors.java
#	src/itdelatrisu/opsu/ui/MenuButton.java
This commit is contained in:
yugecin 2016-12-24 14:35:20 +01:00
commit a6540044b6
36 changed files with 1667 additions and 569 deletions

View File

@ -31,5 +31,6 @@ The following projects were referenced in creating opsu!:
Theme Song Theme Song
---------- ----------
The theme song is "On the Bach" by Jingle Punks, from the [YouTube Audio Library] Rainbows - Kevin MacLeod (incompetech.com)
(https://www.youtube.com/audiolibrary/music). Licensed under Creative Commons: By Attribution 3.0 License
http://creativecommons.org/licenses/by/3.0/

BIN
res/theme.mp3 Normal file

Binary file not shown.

Binary file not shown.

View File

@ -21,7 +21,9 @@ package itdelatrisu.opsu;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.beatmap.Beatmap; import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.BeatmapGroup;
import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSortOrder;
import itdelatrisu.opsu.beatmap.BeatmapWatchService; import itdelatrisu.opsu.beatmap.BeatmapWatchService;
import itdelatrisu.opsu.downloads.DownloadList; import itdelatrisu.opsu.downloads.DownloadList;
import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.downloads.Updater;
@ -38,12 +40,8 @@ import org.newdawn.slick.opengl.InternalTextureLoader;
* AppGameContainer extension that sends critical errors to ErrorHandler. * AppGameContainer extension that sends critical errors to ErrorHandler.
*/ */
public class Container extends AppGameContainer { public class Container extends AppGameContainer {
/** SlickException causing game failure. */ /** Exception causing game failure. */
protected SlickException e = null; protected Exception e = null;
private Exception anyException = null;
public static Container instance;
/** /**
* Create a new container wrapping a game * Create a new container wrapping a game
@ -53,9 +51,19 @@ public class Container extends AppGameContainer {
*/ */
public Container(Game game) throws SlickException { public Container(Game game) throws SlickException {
super(game); 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 @Override
@ -67,19 +75,23 @@ public class Container extends AppGameContainer {
while (running()) while (running())
gameLoop(); gameLoop();
} catch (Exception e) { } catch (Exception e) {
anyException = e; this.e = e;
} finally { }
// destroy the game container // destroy the game container
try {
close_sub(); close_sub();
} catch (Exception e) {
if (this.e == null) // suppress if caused by a previous exception
this.e = e;
}
destroy(); destroy();
if (anyException != null) { // report any critical errors
ErrorHandler.error("Something bad happend while playing", anyException, true); if (e != null) {
anyException = null;
} else if (e != null) {
ErrorHandler.error(null, e, true); ErrorHandler.error(null, e, true);
e = null; e = null;
} forceExit = true;
} }
if (forceExit) { if (forceExit) {
@ -118,9 +130,7 @@ public class Container extends AppGameContainer {
Options.saveOptions(); Options.saveOptions();
// reset cursor // reset cursor
if (UI.getCursor() != null) {
UI.getCursor().reset(); UI.getCursor().reset();
}
// destroy images // destroy images
InternalTextureLoader.get().clear(); InternalTextureLoader.get().clear();
@ -137,6 +147,8 @@ public class Container extends AppGameContainer {
SoundController.stopTrack(); SoundController.stopTrack();
// reset BeatmapSetList data // reset BeatmapSetList data
BeatmapGroup.set(BeatmapGroup.ALL);
BeatmapSortOrder.set(BeatmapSortOrder.TITLE);
if (BeatmapSetList.get() != null) if (BeatmapSetList.get() != null)
BeatmapSetList.get().reset(); BeatmapSetList.get().reset();

View File

@ -151,13 +151,13 @@ public class GameData {
HIT_SLIDER10 = 7, HIT_SLIDER10 = 7,
HIT_SLIDER30 = 8, HIT_SLIDER30 = 8,
HIT_MAX = 9, // not a hit result HIT_MAX = 9, // not a hit result
HIT_SLIDER_INITIAL = 10, // not a hit result HIT_SLIDER_REPEAT = 10, // not a hit result
HIT_SLIDER_REPEAT = 11; // 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; 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; private int[] hitResultCount;
/** Total objects including slider hits/ticks (for determining Full Combo status). */ /** 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). */ /** Current x coordinate of the combo burst image (for sliding animation). */
private float comboBurstX; 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; private int[] hitResultOffset;
/** List of hit result objects associated with hit objects. */ /** List of hit result objects associated with hit objects. */
@ -557,10 +557,11 @@ public class GameData {
* @param x the starting x coordinate * @param x the starting x coordinate
* @param y the y coordinate * @param y the y coordinate
* @param scale the scale to apply * @param scale the scale to apply
* @param alpha the alpha level
* @param fixedsize the width to use for all symbols * @param fixedsize the width to use for all symbols
* @param rightAlign align right (true) or left (false) * @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(); char[] c = str.toCharArray();
float cx = x; float cx = x;
if (rightAlign) { if (rightAlign) {
@ -569,14 +570,18 @@ public class GameData {
if (scale != 1.0f) if (scale != 1.0f)
digit = digit.getScaledCopy(scale); digit = digit.getScaledCopy(scale);
cx -= fixedsize; cx -= fixedsize;
digit.setAlpha(alpha);
digit.draw(cx + (fixedsize - digit.getWidth()) / 2, y); digit.draw(cx + (fixedsize - digit.getWidth()) / 2, y);
digit.setAlpha(1f);
} }
} else { } else {
for (int i = 0; i < c.length; i++) { for (int i = 0; i < c.length; i++) {
Image digit = getScoreSymbolImage(c[i]); Image digit = getScoreSymbolImage(c[i]);
if (scale != 1.0f) if (scale != 1.0f)
digit = digit.getScaledCopy(scale); digit = digit.getScaledCopy(scale);
digit.setAlpha(alpha);
digit.draw(cx + (fixedsize - digit.getWidth()) / 2, y); digit.draw(cx + (fixedsize - digit.getWidth()) / 2, y);
digit.setAlpha(1f);
cx += fixedsize; cx += fixedsize;
} }
} }
@ -589,9 +594,10 @@ public class GameData {
* @param g the graphics context * @param g the graphics context
* @param breakPeriod if true, will not draw scorebar and combo elements, and will draw grade * @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 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") @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()); boolean relaxAutoPilot = (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive());
int margin = (int) (width * 0.008f); int margin = (int) (width * 0.008f);
float uiScale = GameImage.getUIscale(); float uiScale = GameImage.getUIscale();
@ -599,14 +605,14 @@ public class GameData {
// score // score
if (!relaxAutoPilot) if (!relaxAutoPilot)
drawFixedSizeSymbolString((scoreDisplay < 100000000) ? String.format("%08d", scoreDisplay) : Long.toString(scoreDisplay), 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 // score percentage
int symbolHeight = getScoreSymbolImage('0').getHeight(); int symbolHeight = getScoreSymbolImage('0').getHeight();
if (!relaxAutoPilot) if (!relaxAutoPilot)
drawSymbolString( drawSymbolString(
String.format((scorePercentDisplay < 10f) ? "0%.2f%%" : "%.2f%%", scorePercentDisplay), 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 // map progress circle
Beatmap beatmap = MusicController.getBeatmap(); Beatmap beatmap = MusicController.getBeatmap();
@ -620,23 +626,27 @@ public class GameData {
getScoreSymbolImage('%').getWidth() getScoreSymbolImage('%').getWidth()
) * 0.60f - circleDiameter); ) * 0.60f - circleDiameter);
if (!relaxAutoPilot) { if (!relaxAutoPilot) {
float oldWhiteAlpha = Colors.WHITE_ALPHA.a;
Colors.WHITE_ALPHA.a = alpha;
g.setAntiAlias(true); g.setAntiAlias(true);
g.setLineWidth(2f); g.setLineWidth(2f);
g.setColor(Color.white); g.setColor(Colors.WHITE_ALPHA);
g.drawOval(circleX, symbolHeight, circleDiameter, circleDiameter); g.drawOval(circleX, symbolHeight, circleDiameter, circleDiameter);
if (trackPosition > firstObjectTime) { if (trackPosition > firstObjectTime) {
// map progress (white) // map progress (white)
g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter, float progress = Math.min((float) (trackPosition - firstObjectTime) / (beatmap.endTime - firstObjectTime), 1f);
-90, -90 + (int) (360f * (trackPosition - firstObjectTime) / (beatmap.endTime - firstObjectTime)) g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter, -90, -90 + (int) (360f * progress));
);
} else { } else {
// lead-in time (yellow) // 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.setColor(Colors.YELLOW_ALPHA);
g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter, g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter, -90 + (int) (360f * progress), -90);
-90 + (int) (360f * trackPosition / firstObjectTime), -90 Colors.YELLOW_ALPHA.a = oldYellowAlpha;
);
} }
g.setAntiAlias(false); g.setAntiAlias(false);
Colors.WHITE_ALPHA.a = oldWhiteAlpha;
} }
// mod icons // mod icons
@ -646,10 +656,12 @@ public class GameData {
int modCount = 0; int modCount = 0;
for (GameMod mod : GameMod.VALUES_REVERSED) { for (GameMod mod : GameMod.VALUES_REVERSED) {
if (mod.isActive()) { if (mod.isActive()) {
mod.getImage().setAlpha(alpha);
mod.getImage().draw( mod.getImage().draw(
modX - (modCount * (modWidth / 2f)), modX - (modCount * (modWidth / 2f)),
symbolHeight + circleDiameter + 10 symbolHeight + circleDiameter + 10
); );
mod.getImage().setAlpha(1f);
modCount++; modCount++;
} }
} }
@ -697,8 +709,8 @@ public class GameData {
float tickWidth = 2 * uiScale; float tickWidth = 2 * uiScale;
for (HitErrorInfo info : hitErrorList) { for (HitErrorInfo info : hitErrorList) {
int time = info.time; int time = info.time;
float alpha = 1 - ((float) (trackPosition - time) / HIT_ERROR_FADE_TIME); float tickAlpha = 1 - ((float) (trackPosition - time) / HIT_ERROR_FADE_TIME);
white.a = alpha * hitErrorAlpha; white.a = tickAlpha * hitErrorAlpha;
g.setColor(white); g.setColor(white);
g.fillRect((hitErrorX + info.timeDiff - 1) * uiScale, tickY, tickWidth, tickHeight); g.fillRect((hitErrorX + info.timeDiff - 1) * uiScale, tickY, tickWidth, tickHeight);
} }
@ -721,9 +733,12 @@ public class GameData {
float colourX = 4 * uiScale, colourY = 15 * uiScale; float colourX = 4 * uiScale, colourY = 15 * uiScale;
Image colourCropped = colour.getSubImage(0, 0, (int) (645 * uiScale * healthRatio), colour.getHeight()); Image colourCropped = colour.getSubImage(0, 0, (int) (645 * uiScale * healthRatio), colour.getHeight());
scorebar.setAlpha(1f); scorebar.setAlpha(alpha);
scorebar.draw(0, 0); scorebar.draw(0, 0);
scorebar.setAlpha(1f);
colourCropped.setAlpha(alpha);
colourCropped.draw(colourX, colourY); colourCropped.draw(colourX, colourY);
colourCropped.setAlpha(1f);
Image ki = null; Image ki = null;
if (health >= 50f) if (health >= 50f)
@ -734,7 +749,9 @@ public class GameData {
ki = GameImage.SCOREBAR_KI_DANGER2.getImage(); ki = GameImage.SCOREBAR_KI_DANGER2.getImage();
if (comboPopTime < COMBO_POP_TIME) if (comboPopTime < COMBO_POP_TIME)
ki = ki.getScaledCopy(1f + (0.45f * (1f - (float) 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.drawCentered(colourX + colourCropped.getWidth(), colourY);
ki.setAlpha(1f);
// combo burst // combo burst
if (comboBurstIndex != -1 && comboBurstAlpha > 0f) { if (comboBurstIndex != -1 && comboBurstAlpha > 0f) {
@ -750,8 +767,8 @@ public class GameData {
float comboPopFront = 1 + comboPop * 0.08f; float comboPopFront = 1 + comboPop * 0.08f;
String comboString = String.format("%dx", combo); String comboString = String.format("%dx", combo);
if (comboPopTime != COMBO_POP_TIME) if (comboPopTime != COMBO_POP_TIME)
drawSymbolString(comboString, margin, height - margin - (symbolHeight * comboPopBack), comboPopBack, 0.5f, false); drawSymbolString(comboString, margin, height - margin - (symbolHeight * comboPopBack), comboPopBack, 0.5f * alpha, false);
drawSymbolString(comboString, margin, height - margin - (symbolHeight * comboPopFront), comboPopFront, 1f, false); drawSymbolString(comboString, margin, height - margin - (symbolHeight * comboPopFront), comboPopFront, alpha, false);
} }
} else if (!relaxAutoPilot) { } else if (!relaxAutoPilot) {
// grade // grade
@ -759,9 +776,9 @@ public class GameData {
if (grade != Grade.NULL) { if (grade != Grade.NULL) {
Image gradeImage = grade.getSmallImage(); Image gradeImage = grade.getSmallImage();
float gradeScale = symbolHeight * 0.75f / gradeImage.getHeight(); float gradeScale = symbolHeight * 0.75f / gradeImage.getHeight();
gradeImage.getScaledCopy(gradeScale).draw( gradeImage = gradeImage.getScaledCopy(gradeScale);
circleX - gradeImage.getWidth(), symbolHeight gradeImage.setAlpha(alpha);
); gradeImage.draw(circleX - gradeImage.getWidth(), symbolHeight);
} }
} }
} }
@ -786,7 +803,7 @@ public class GameData {
drawFixedSizeSymbolString( drawFixedSizeSymbolString(
(score < 100000000) ? String.format("%08d", score) : Long.toString(score), (score < 100000000) ? String.format("%08d", score) : Long.toString(score),
210 * uiScale, (rankingHeight + 50) * uiScale, 210 * uiScale, (rankingHeight + 50) * uiScale,
scoreTextScale, getScoreSymbolImage('0').getWidth() * scoreTextScale - 2, false scoreTextScale, 1f, getScoreSymbolImage('0').getWidth() * scoreTextScale - 2, false
); );
// result counts // result counts
@ -897,63 +914,17 @@ public class GameData {
lighting.drawCentered(hitResult.x, hitResult.y, hitResult.color); lighting.drawCentered(hitResult.x, hitResult.y, hitResult.color);
} }
// hit animation // hit animations (only draw when the "Hidden" mod is not enabled)
if (Options.isHitAnimationEnabled() && if (!GameMod.HIDDEN.isActive()) {
hitResult.result != HIT_MISS && ( drawHitAnimations(hitResult, trackPosition);
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 result // hit result
if (!hitResult.hideResult && ( if (!hitResult.hideResult && (
hitResult.hitResultType == HitObjectType.CIRCLE || hitResult.hitResultType == HitObjectType.CIRCLE ||
hitResult.hitResultType == HitObjectType.SPINNER || hitResult.hitResultType == HitObjectType.SLIDER_FIRST ||
hitResult.curve != null)) { hitResult.hitResultType == HitObjectType.SLIDER_LAST ||
hitResult.hitResultType == HitObjectType.SPINNER)) {
float scaleProgress = AnimationEquation.IN_OUT_BOUNCE.calc( float scaleProgress = AnimationEquation.IN_OUT_BOUNCE.calc(
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_TEXT_BOUNCE_TIME) / HITCIRCLE_TEXT_BOUNCE_TIME); (float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_TEXT_BOUNCE_TIME) / HITCIRCLE_TEXT_BOUNCE_TIME);
float scale = 1f + (HITCIRCLE_TEXT_ANIM_SCALE - 1f) * scaleProgress; 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. * Changes health by a given percentage, modified by drainRate.
* @param percent the health percentage * @param percent the health percentage
@ -1202,16 +1231,16 @@ public class GameData {
health = 0f; 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)); * Handles a slider repeat result (animation only: arrow).
if (!Options.isMirror() || !GameMod.AUTO.isActive()) { * @param time the repeat time
return; * @param x the x coordinate
} * @param y the y coordinate
float[] m = Utils.mirrorPoint(x, y); * @param color the arrow color
hitResultList.add(new HitObjectResult(time, HIT_SLIDER_INITIAL, m[0], m[1], mirrorcolor, null, null, true, false)); * @param curve the slider curve
} * @param type the hit object type
*/
public void sendRepeatSliderResult(int time, float x, float y, Color color, Curve curve, HitObjectType 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)); hitResultList.add(new HitObjectResult(time, HIT_SLIDER_REPEAT, x, y, color, type, curve, true, true));
if (!Options.isMirror()) { if (!Options.isMirror()) {
return; 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)); 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. * Handles a slider tick result.
* @param time the tick start time * @param time the tick start time
@ -1229,7 +1270,7 @@ public class GameData {
* @param hitObject the hit object * @param hitObject the hit object
* @param repeat the current repeat number * @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; int hitValue = 0;
switch (result) { switch (result) {
case HIT_SLIDER30: case HIT_SLIDER30:
@ -1415,6 +1456,23 @@ public class GameData {
boolean expand, int repeat, Curve curve, boolean sliderHeldToEnd) { boolean expand, int repeat, Curve curve, boolean sliderHeldToEnd) {
hitResult(time, result, x, y, color, end, hitObject, hitResultType, expand, repeat, curve, sliderHeldToEnd, true); 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, public void hitResult(int time, int result, float x, float y, Color color,
boolean end, HitObject hitObject, HitObjectType hitResultType, boolean end, HitObject hitObject, HitObjectType hitResultType,
boolean expand, int repeat, Curve curve, boolean sliderHeldToEnd, boolean handleResult) { 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(); 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)); 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));
}
*/
} }
/** /**

View File

@ -655,7 +655,7 @@ public enum GameImage {
* If the default image has already been loaded, this will do nothing. * If the default image has already been loaded, this will do nothing.
*/ */
public void setDefaultImage() { public void setDefaultImage() {
if (defaultImage != null || defaultImages != null) if (defaultImage != null || defaultImages != null || Options.getSkin() == null)
return; return;
// try to load multiple images // try to load multiple images

View File

@ -249,9 +249,12 @@ public class Opsu extends StateBasedGame {
} else } else
songMenu.resetTrackOnLoad(); songMenu.resetTrackOnLoad();
} }
// reset game data
if (UI.getCursor().isBeatmapSkinned()) if (UI.getCursor().isBeatmapSkinned())
UI.getCursor().reset(); UI.getCursor().reset();
songMenu.resetGameDataOnLoad(); songMenu.resetGameDataOnLoad();
this.enterState(Opsu.STATE_SONGMENU, new EasedFadeOutTransition(), new FadeInTransition()); this.enterState(Opsu.STATE_SONGMENU, new EasedFadeOutTransition(), new FadeInTransition());
return false; return false;
} }

View File

@ -143,6 +143,12 @@ public class Options {
private static boolean noSingleInstance; 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". * Returns whether the XDG flag in the manifest (if any) is set to "true".
* @return true if XDG directories are enabled, false otherwise * @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 * @return the XDG base directory, or the working directory if unavailable
*/ */
private static File getXDGBaseDir(String env, String fallback) { private static File getXDGBaseDir(String env, String fallback) {
File workingDir = Utils.isJarRunning() ?
Utils.getRunningDirectory().getParentFile() : Utils.getWorkingDirectory();
if (!USE_XDG) if (!USE_XDG)
return new File("./"); return workingDir;
String OS = System.getProperty("os.name").toLowerCase(); String OS = System.getProperty("os.name").toLowerCase();
if (OS.indexOf("nix") >= 0 || OS.indexOf("nux") >= 0 || OS.indexOf("aix") > 0) { 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); ErrorHandler.error(String.format("Failed to create configuration folder at '%s/opsu'.", rootPath), null, false);
return dir; return dir;
} else } else
return new File("./"); return workingDir;
} }
/** /**
@ -219,12 +228,6 @@ public class Options {
return (dir.isDirectory()) ? dir : null; 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. */ /** Game options. */
public enum GameOption { public enum GameOption {
// internal options (not displayed in-game) // internal options (not displayed in-game)
@ -275,7 +278,32 @@ public class Options {
public String write() { return themeString; } public String write() { return themeString; }
@Override @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") { PORT ("Port") {
@Override @Override
@ -1876,25 +1904,26 @@ public class Options {
/** /**
* Returns a dummy Beatmap containing the theme song. * 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() { public static Beatmap getThemeBeatmap() {
String[] tokens = themeString.split(","); String[] tokens = themeString.split(",");
if (tokens.length != 4) { if (tokens.length != 4)
ErrorHandler.error("Theme song string is malformed.", null, false);
return null; return null;
}
Beatmap beatmap = new Beatmap(null); Beatmap beatmap = new Beatmap(null);
beatmap.audioFilename = new File(tokens[0]); beatmap.audioFilename = new File(tokens[0]);
beatmap.title = tokens[1]; beatmap.title = tokens[1];
beatmap.artist = tokens[2]; beatmap.artist = tokens[2];
beatmap.timingPoints = new ArrayList<>(1);
beatmap.timingPoints.add(new TimingPoint("-44,631.578947368421,4,1,0,100,1,0"));
try { try {
beatmap.endTime = Integer.parseInt(tokens[3]); beatmap.endTime = Integer.parseInt(tokens[3]);
} catch (NumberFormatException e) { } 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; return null;
} }

View File

@ -41,6 +41,7 @@ import java.net.SocketTimeoutException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.file.Paths;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
@ -158,6 +159,14 @@ public class Utils {
anim.draw(x - (anim.getWidth() / 2f), y - (anim.getHeight() / 2f)); 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. * Clamps a value between a lower and upper bound.
* @param val the value to clamp * @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: * Parses the integer string argument as a boolean:
* {@code 1} is {@code true}, and all other values are {@code false}. * {@code 1} is {@code true}, and all other values are {@code false}.

View File

@ -78,6 +78,12 @@ public class MusicController {
/** The track dim level, if dimmed. */ /** The track dim level, if dimmed. */
private static float dimLevel = 1f; 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. // This class should not be instantiated.
private MusicController() {} private MusicController() {}
@ -94,7 +100,6 @@ public class MusicController {
final File audioFile = beatmap.audioFilename; final File audioFile = beatmap.audioFilename;
if (!audioFile.isFile() && !ResourceLoader.resourceExists(audioFile.getPath())) { if (!audioFile.isFile() && !ResourceLoader.resourceExists(audioFile.getPath())) {
UI.sendBarNotification(String.format("Could not find track '%s'.", audioFile.getName())); UI.sendBarNotification(String.format("Could not find track '%s'.", audioFile.getName()));
System.out.println(beatmap);
return; return;
} }
@ -136,8 +141,10 @@ public class MusicController {
player.addListener(new MusicListener() { player.addListener(new MusicListener() {
@Override @Override
public void musicEnded(Music music) { 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; trackEnded = true;
resetTimingPoint();
}
} }
@Override @Override
@ -159,6 +166,7 @@ public class MusicController {
setVolume(Options.getMusicVolume() * Options.getMasterVolume()); setVolume(Options.getMusicVolume() * Options.getMasterVolume());
trackEnded = false; trackEnded = false;
pauseTime = 0f; pauseTime = 0f;
resetTimingPoint();
if (loop) if (loop)
player.loop(); player.loop();
else else
@ -187,34 +195,81 @@ public class MusicController {
/** /**
* Gets the progress of the current beat. * 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() { public static Float getBeatProgress() {
if (!isPlaying() || getBeatmap() == null) { if (!updateTimingPoint())
return null; 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(); Beatmap map = getBeatmap();
if (map.timingPoints == null) { if (!isPlaying() || map == null || map.timingPoints == null || map.timingPoints.isEmpty())
return null; 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; // advance timing point index, record last non-inherited timing point
float beatlen = 0f; int trackPosition = getPosition();
int time = 0; for (int i = timingPointIndex + 1; i < map.timingPoints.size(); i++) {
for (TimingPoint pts : map.timingPoints) { TimingPoint timingPoint = map.timingPoints.get(i);
if (p == null || pts.getTime() < getPosition()) { if (trackPosition < timingPoint.getTime())
p = pts; break;
if (!p.isInherited() && p.getBeatLength() > 0) { timingPointIndex = i;
beatlen = p.getBeatLength(); if (!timingPoint.isInherited() && timingPoint.getBeatLength() > 0)
time = p.getTime(); lastTimingPoint = timingPoint;
} }
} if (lastTimingPoint == null)
} return false; // no timing info
if (p == null) {
return null; return true;
}
double beatLength = beatlen * 100;
return (((trackposition * 100 - time * 100) % beatLength) / beatLength);
} }
/** /**
@ -258,8 +313,10 @@ public class MusicController {
public static void stop() { public static void stop() {
if (isPlaying()) if (isPlaying())
player.stop(); player.stop();
if (trackExists()) if (trackExists()) {
pauseTime = 0f; pauseTime = 0f;
resetTimingPoint();
}
} }
/** /**
@ -298,7 +355,11 @@ public class MusicController {
* @param position the new track position (in ms) * @param position the new track position (in ms)
*/ */
public static boolean setPosition(int position) { 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) { public static void play(boolean loop) {
if (trackExists()) { if (trackExists()) {
trackEnded = false; trackEnded = false;
resetTimingPoint();
if (loop) if (loop)
player.loop(); player.loop();
else else
@ -409,6 +471,14 @@ public class MusicController {
setVolume(volume); setVolume(volume);
} }
/**
* Resets timing point information.
*/
private static void resetTimingPoint() {
timingPointIndex = 0;
lastTimingPoint = null;
}
/** /**
* Completely resets MusicController state. * Completely resets MusicController state.
* <p> * <p>
@ -427,7 +497,7 @@ public class MusicController {
try { try {
trackLoader.join(); trackLoader.join();
} catch (InterruptedException e) { } catch (InterruptedException e) {
e.printStackTrace(); ErrorHandler.error(null, e, true);
} }
} }
trackLoader = null; trackLoader = null;
@ -439,6 +509,7 @@ public class MusicController {
themePlaying = false; themePlaying = false;
pauseTime = 0f; pauseTime = 0f;
trackDimmed = false; trackDimmed = false;
resetTimingPoint();
// releases all sources from previous tracks // releases all sources from previous tracks
destroyOpenAL(); destroyOpenAL();

View File

@ -68,6 +68,18 @@ public class Beatmap implements Comparable<Beatmap> {
/** The star rating. */ /** The star rating. */
public double starRating = -1; 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] * [General]
*/ */
@ -501,4 +513,12 @@ public class Beatmap implements Comparable<Beatmap> {
String[] rgb = s.split(","); String[] rgb = s.split(",");
this.sliderBorder = new Color(new Color(Integer.parseInt(rgb[0]), Integer.parseInt(rgb[1]), Integer.parseInt(rgb[2]))); 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();
}
} }

View File

@ -81,7 +81,7 @@ public class BeatmapDifficultyCalculator {
*/ */
public BeatmapDifficultyCalculator(Beatmap beatmap) { public BeatmapDifficultyCalculator(Beatmap beatmap) {
this.beatmap = beatmap; this.beatmap = beatmap;
if (beatmap.timingPoints == null) if (beatmap.breaks == null)
BeatmapDB.load(beatmap, BeatmapDB.LOAD_ARRAY); BeatmapDB.load(beatmap, BeatmapDB.LOAD_ARRAY);
BeatmapParser.parseHitObjects(beatmap); BeatmapParser.parseHitObjects(beatmap);
} }

View File

@ -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<BeatmapSetNode> filter(ArrayList<BeatmapSetNode> list) {
// find top K elements
PriorityQueue<BeatmapSetNode> pq = new PriorityQueue<BeatmapSetNode>(K, new Comparator<BeatmapSetNode>() {
@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<BeatmapSetNode> filteredList = new ArrayList<BeatmapSetNode>();
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<BeatmapSetNode> filter(ArrayList<BeatmapSetNode> list) {
// find "favorite" beatmaps
ArrayList<BeatmapSetNode> filteredList = new ArrayList<BeatmapSetNode>();
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<BeatmapSetNode> filter(ArrayList<BeatmapSetNode> 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);
}
}

View File

@ -112,6 +112,7 @@ public class BeatmapParser {
// parse directories // parse directories
BeatmapSetNode lastNode = null; BeatmapSetNode lastNode = null;
long timestamp = System.currentTimeMillis();
for (File dir : dirs) { for (File dir : dirs) {
currentDirectoryIndex++; currentDirectoryIndex++;
if (!dir.isDirectory()) if (!dir.isDirectory())
@ -160,6 +161,7 @@ public class BeatmapParser {
// add to parsed beatmap list // add to parsed beatmap list
if (beatmap != null) { if (beatmap != null) {
beatmap.dateAdded = timestamp;
beatmaps.add(beatmap); beatmaps.add(beatmap);
parsedBeatmaps.add(beatmap); parsedBeatmaps.add(beatmap);
} }
@ -547,21 +549,7 @@ public class BeatmapParser {
break; break;
try { try {
// parse timing point parseTimingPoint(beatmap, line);
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);
} catch (Exception e) { } catch (Exception e) {
Log.warn(String.format("Failed to read timing point '%s' for file '%s'.", Log.warn(String.format("Failed to read timing point '%s' for file '%s'.",
line, file.getAbsolutePath()), e); line, file.getAbsolutePath()), e);
@ -678,6 +666,71 @@ public class BeatmapParser {
return beatmap; 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<TimingPoint>();
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. * Parses all hit objects in a beatmap.
* @param beatmap the beatmap to parse * @param beatmap the beatmap to parse

View File

@ -19,6 +19,7 @@
package itdelatrisu.opsu.beatmap; package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.GameMod; import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.db.BeatmapDB;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.text.NumberFormat; import java.text.NumberFormat;
@ -185,4 +186,37 @@ public class BeatmapSet implements Iterable<Beatmap> {
return false; 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;
}
} }

View File

@ -54,6 +54,9 @@ public class BeatmapSetList {
/** Total number of beatmaps (i.e. Beatmap objects). */ /** Total number of beatmaps (i.e. Beatmap objects). */
private int mapCount = 0; private int mapCount = 0;
/** List containing all nodes in the current group. */
private ArrayList<BeatmapSetNode> groupNodes;
/** Current list of nodes (subset of parsedNodes, used for searches). */ /** Current list of nodes (subset of parsedNodes, used for searches). */
private ArrayList<BeatmapSetNode> nodes; private ArrayList<BeatmapSetNode> nodes;
@ -97,7 +100,7 @@ public class BeatmapSetList {
* This does not erase any parsed nodes. * This does not erase any parsed nodes.
*/ */
public void reset() { public void reset() {
nodes = parsedNodes; nodes = groupNodes = BeatmapGroup.current().filter(parsedNodes);
expandedIndex = -1; expandedIndex = -1;
expandedStartNode = expandedEndNode = null; expandedStartNode = expandedEndNode = null;
lastQuery = ""; lastQuery = "";
@ -168,6 +171,7 @@ public class BeatmapSetList {
Beatmap beatmap = beatmapSet.get(0); Beatmap beatmap = beatmapSet.get(0);
nodes.remove(index); nodes.remove(index);
parsedNodes.remove(eCur); parsedNodes.remove(eCur);
groupNodes.remove(eCur);
mapCount -= beatmapSet.size(); mapCount -= beatmapSet.size();
if (beatmap.beatmapSetID > 0) if (beatmap.beatmapSetID > 0)
MSIDdb.remove(beatmap.beatmapSetID); MSIDdb.remove(beatmap.beatmapSetID);
@ -407,7 +411,7 @@ public class BeatmapSetList {
return; return;
// sort the list // sort the list
Collections.sort(nodes, BeatmapSortOrder.getSort().getComparator()); Collections.sort(nodes, BeatmapSortOrder.current().getComparator());
expandedIndex = -1; expandedIndex = -1;
expandedStartNode = expandedEndNode = null; expandedStartNode = expandedEndNode = null;
@ -444,7 +448,7 @@ public class BeatmapSetList {
// if empty query, reset to original list // if empty query, reset to original list
if (query.isEmpty() || terms.isEmpty()) { if (query.isEmpty() || terms.isEmpty()) {
nodes = parsedNodes; nodes = groupNodes;
return true; return true;
} }
@ -472,14 +476,14 @@ public class BeatmapSetList {
String type = condType.remove(); String type = condType.remove();
String operator = condOperator.remove(); String operator = condOperator.remove();
float value = condValue.remove(); float value = condValue.remove();
for (BeatmapSetNode node : parsedNodes) { for (BeatmapSetNode node : groupNodes) {
if (node.getBeatmapSet().matches(type, operator, value)) if (node.getBeatmapSet().matches(type, operator, value))
nodes.add(node); nodes.add(node);
} }
} else { } else {
// normal term // normal term
String term = terms.remove(); String term = terms.remove();
for (BeatmapSetNode node : parsedNodes) { for (BeatmapSetNode node : groupNodes) {
if (node.getBeatmapSet().matches(term)) if (node.getBeatmapSet().matches(term))
nodes.add(node); nodes.add(node);
} }

View File

@ -75,7 +75,7 @@ public class BeatmapSetNode {
public void draw(float x, float y, Grade grade, boolean focus) { public void draw(float x, float y, Grade grade, boolean focus) {
Image bg = GameImage.MENU_BUTTON_BG.getImage(); Image bg = GameImage.MENU_BUTTON_BG.getImage();
boolean expanded = (beatmapIndex > -1); boolean expanded = (beatmapIndex > -1);
Beatmap beatmap; Beatmap beatmap = beatmapSet.get(expanded ? beatmapIndex : 0);
bg.setAlpha(0.9f); bg.setAlpha(0.9f);
Color bgColor; Color bgColor;
Color textColor = Options.getSkin().getSongSelectInactiveTextColor(); Color textColor = Options.getSkin().getSongSelectInactiveTextColor();
@ -88,11 +88,10 @@ public class BeatmapSetNode {
textColor = Options.getSkin().getSongSelectActiveTextColor(); textColor = Options.getSkin().getSongSelectActiveTextColor();
} else } else
bgColor = Colors.BLUE_BUTTON; bgColor = Colors.BLUE_BUTTON;
beatmap = beatmapSet.get(beatmapIndex); } else if (beatmapSet.isPlayed())
} else {
bgColor = Colors.ORANGE_BUTTON; bgColor = Colors.ORANGE_BUTTON;
beatmap = beatmapSet.get(0); else
} bgColor = Colors.PINK_BUTTON;
bg.draw(x, y, bgColor); bg.draw(x, y, bgColor);
float cx = x + (bg.getWidth() * 0.043f); float cx = x + (bg.getWidth() * 0.043f);

View File

@ -18,28 +18,19 @@
package itdelatrisu.opsu.beatmap; 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 java.util.Comparator;
import org.newdawn.slick.Image;
/** /**
* Beatmap sorting orders. * Beatmap sorting orders.
*/ */
public enum BeatmapSortOrder { public enum BeatmapSortOrder {
TITLE (0, "Title", new TitleOrder()), TITLE ("Title", new TitleOrder()),
ARTIST (1, "Artist", new ArtistOrder()), ARTIST ("Artist", new ArtistOrder()),
CREATOR (2, "Creator", new CreatorOrder()), CREATOR ("Creator", new CreatorOrder()),
BPM (3, "BPM", new BPMOrder()), BPM ("BPM", new BPMOrder()),
LENGTH (4, "Length", new LengthOrder()); LENGTH ("Length", new LengthOrder()),
DATE ("Date Added", new DateOrder()),
/** The ID of the sort (used for tab positioning). */ PLAYS ("Most Played", new PlayOrder());
private final int id;
/** The name of the sort. */ /** The name of the sort. */
private final String name; private final String name;
@ -47,19 +38,6 @@ public enum BeatmapSortOrder {
/** The comparator for the sort. */ /** The comparator for the sort. */
private final Comparator<BeatmapSetNode> comparator; private final Comparator<BeatmapSetNode> 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. */ /** Current sort. */
private static BeatmapSortOrder currentSort = TITLE; private static BeatmapSortOrder currentSort = TITLE;
@ -67,13 +45,13 @@ public enum BeatmapSortOrder {
* Returns the current sort. * Returns the current sort.
* @return the current sort * @return the current sort
*/ */
public static BeatmapSortOrder getSort() { return currentSort; } public static BeatmapSortOrder current() { return currentSort; }
/** /**
* Sets a new sort. * Sets a new sort.
* @param sort the 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. * 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<BeatmapSetNode> {
@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<BeatmapSetNode> {
@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. * Constructor.
* @param id the ID of the sort (for tab positioning)
* @param name the sort name * @param name the sort name
* @param comparator the comparator for the sort * @param comparator the comparator for the sort
*/ */
BeatmapSortOrder(int id, String name, Comparator<BeatmapSetNode> comparator) { BeatmapSortOrder(String name, Comparator<BeatmapSetNode> comparator) {
this.id = id;
this.name = name; this.name = name;
this.comparator = comparator; this.comparator = comparator;
} }
/** /**
* Initializes the sort tab. * Returns the sort name.
* @param containerWidth the container width * @return the name
* @param bottomY the bottom y coordinate
*/ */
public void init(int containerWidth, float bottomY) { public String getName() { return name; }
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)
);
}
/** /**
* Returns the comparator for the sort. * Returns the comparator for the sort.
@ -173,20 +171,6 @@ public enum BeatmapSortOrder {
*/ */
public Comparator<BeatmapSetNode> getComparator() { return comparator; } public Comparator<BeatmapSetNode> getComparator() { return comparator; }
/** @Override
* Checks if the coordinates are within the image bounds. public String toString() { return name; }
* @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);
}
} }

View File

@ -58,6 +58,14 @@ public class TimingPoint {
* @param line the line to be parsed * @param line the line to be parsed
*/ */
public TimingPoint(String line) { 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 // TODO: better support for old formats
String[] tokens = line.split(","); String[] tokens = line.split(",");
try { try {

View File

@ -30,6 +30,7 @@ import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -42,8 +43,30 @@ public class BeatmapDB {
/** /**
* Current database version. * Current database version.
* This value should be changed whenever the database format changes. * 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<String> getUpdateQueries(int version) {
List<String> list = new LinkedList<String>();
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. */ /** Minimum batch size ratio ({@code batchSize/cacheSize}) to invoke batch loading. */
private static final float LOAD_BATCH_MIN_RATIO = 0.2f; private static final float LOAD_BATCH_MIN_RATIO = 0.2f;
@ -58,7 +81,9 @@ public class BeatmapDB {
private static Connection connection; private static Connection connection;
/** Query statements. */ /** 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. */ /** Current size of beatmap cache table. */
private static int cacheSize = -1; private static int cacheSize = -1;
@ -75,6 +100,9 @@ public class BeatmapDB {
if (connection == null) if (connection == null)
return; return;
// run any database updates
updateDatabase();
// create the database // create the database
createDatabase(); createDatabase();
@ -88,20 +116,21 @@ public class BeatmapDB {
// retrieve the cache size // retrieve the cache size
getCacheSize(); getCacheSize();
// check the database version
checkVersion();
// prepare sql statements (not used here) // prepare sql statements (not used here)
try { try {
insertStmt = connection.prepareStatement( insertStmt = connection.prepareStatement(
"INSERT INTO beatmaps VALUES (" + "INSERT INTO beatmaps VALUES (" +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," + "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," +
"?, ?, ?, ?, ?, ?" +
")"
); );
selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?"); selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?");
deleteMapStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?"); deleteMapStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?");
deleteGroupStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ?"); deleteGroupStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ?");
setStarsStmt = connection.prepareStatement("UPDATE beatmaps SET stars = ? WHERE dir = ? AND file = ?"); 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) { } catch (SQLException e) {
ErrorHandler.error("Failed to prepare beatmap statements.", e, true); 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, " + "audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " +
"mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " + "mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " +
"bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT, " + "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 (" + "CREATE TABLE IF NOT EXISTS info (" +
"key TEXT NOT NULL UNIQUE, value TEXT" + "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 * Applies any database updates by comparing the current version to the
* from the current version, then updates the version field. * stored version. Does nothing if tables have not been created.
*/ */
private static void checkVersion() { private static void updateDatabase() {
try (Statement stmt = connection.createStatement()) { try (Statement stmt = connection.createStatement()) {
// get the stored version int version = 0;
String sql = "SELECT value FROM info WHERE key = 'version'";
ResultSet rs = stmt.executeQuery(sql);
String version = (rs.next()) ? rs.getString(1) : "";
rs.close();
// if different from current version, clear the database // if 'info' table does not exist, assume version 0 and apply all updates
if (!version.equals(DATABASE_VERSION)) { String sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'info'";
clearDatabase(); ResultSet rs = stmt.executeQuery(sql);
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) {}
}
// database versions match
if (version >= DATABASE_VERSION)
return;
// apply updates
for (String query : getUpdateQueries(version))
stmt.executeUpdate(query);
// update version // update version
if (infoExists) {
PreparedStatement ps = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('version', ?)"); 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.executeUpdate();
ps.close(); ps.close();
} }
} catch (SQLException e) { } 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(40, beatmap.comboToString());
stmt.setString(41, beatmap.md5Hash); stmt.setString(41, beatmap.md5Hash);
stmt.setDouble(42, beatmap.starRating); 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) { } catch (SQLException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
@ -487,6 +546,10 @@ public class BeatmapDB {
beatmap.sliderBorderFromString(rs.getString(37)); beatmap.sliderBorderFromString(rs.getString(37));
beatmap.md5Hash = rs.getString(41); beatmap.md5Hash = rs.getString(41);
beatmap.starRating = rs.getDouble(42); 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) { } catch (SQLException e) {
throw e; throw e;
} catch (Exception 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. * Closes the connection to the database.
*/ */

View File

@ -360,7 +360,7 @@ public class ScoreDB {
ResultSet rs = selectMapSetStmt.executeQuery(); ResultSet rs = selectMapSetStmt.executeQuery();
List<ScoreData> list = null; List<ScoreData> 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()) { while (rs.next()) {
ScoreData s = new ScoreData(rs); ScoreData s = new ScoreData(rs);
if (!s.version.equals(version)) { if (!s.version.equals(version)) {

View File

@ -179,7 +179,7 @@ public class Circle extends GameObject {
if (result > -1) { if (result > -1) {
data.addHitError(hitObject.getTime(), x, y, timeDiff); 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; return true;
} }
} }
@ -195,25 +195,25 @@ public class Circle extends GameObject {
if (trackPosition > time + hitResultOffset[GameData.HIT_50]) { if (trackPosition > time + hitResultOffset[GameData.HIT_50]) {
if (isAutoMod) {// "auto" mod: catch any missed notes due to lag 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()) { if (Options.isMirror() && GameMod.AUTO.isActive()) {
float[] m = Utils.mirrorPoint(x, y); 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 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; return true;
} }
// "auto" mod: send a perfect hit result // "auto" mod: send a perfect hit result
else if (isAutoMod) { else if (isAutoMod) {
if (Math.abs(trackPosition - time) < hitResultOffset[GameData.HIT_300]) { 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()) { if (Options.isMirror() && GameMod.AUTO.isActive()) {
float[] m = Utils.mirrorPoint(x, y); 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; return true;
} }

View File

@ -205,6 +205,7 @@ public class Slider extends GameObject {
} }
int timeDiff = hitObject.getTime() - trackPosition; int timeDiff = hitObject.getTime() - trackPosition;
final int repeatCount = hitObject.getRepeatCount();
final int approachTime = game.getApproachTime(); final int approachTime = game.getApproachTime();
final int fadeInTime = game.getFadeInTime(); final int fadeInTime = game.getFadeInTime();
float scale = timeDiff / (float) approachTime; float scale = timeDiff / (float) approachTime;
@ -219,9 +220,12 @@ public class Slider extends GameObject {
Image hitCircle = GameImage.HITCIRCLE.getImage(); Image hitCircle = GameImage.HITCIRCLE.getImage();
Vec2f endPos = curve.pointAt(1); Vec2f endPos = curve.pointAt(1);
float curveAlpha = 1f; float oldWhiteFadeAlpha = Colors.WHITE_FADE.a;
if (GameMod.HIDDEN.isActive() && trackPosition > getTime()) { float sliderAlpha = 1f;
curveAlpha = Math.max(0f, 1f - ((float) (trackPosition - getTime()) / (getEndTime() - getTime())) * 1.05f); 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; curveColor.a = curveAlpha;
@ -232,19 +236,37 @@ public class Slider extends GameObject {
if (mirror) { if (mirror) {
g.rotate(x, y, -180f); 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)) {
// end circle 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); Vec2f endCircPos = curve.pointAt(curveInterval);
hitCircle.drawCentered(endCircPos.x, endCircPos.y, color); hitCircle.drawCentered(endCircPos.x, endCircPos.y, circleColor);
hitCircleOverlay.drawCentered(endCircPos.x, endCircPos.y, Colors.WHITE_FADE); hitCircleOverlay.drawCentered(endCircPos.x, endCircPos.y, overlayColor);
*/ }
// start circle, don't draw if already clicked // set first circle colors to fade in after repeats
if (!sliderClickedInitial) { Color firstCircleColor = new Color(color);
hitCircle.drawCentered(x, y, color); Color startCircleOverlayColor = new Color(Colors.WHITE_FADE);
if (!overlayAboveNumber) if (sliderClickedInitial) {
hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE); // fade in first circle after repeats
firstCircleColor.a = startCircleOverlayColor.a = sliderAlpha * getCircleAlphaAfterRepeat(trackPosition, false);
}
// 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(); g.popTransform();
@ -252,7 +274,7 @@ public class Slider extends GameObject {
// ticks // ticks
if (ticksT != null) { if (ticksT != null) {
drawSliderTicks(g, trackPosition, alpha, decorationsAlpha, mirror); drawSliderTicks(g, trackPosition, alpha, decorationsAlpha, mirror);
Colors.WHITE_FADE.a = alpha; Colors.WHITE_FADE.a = oldWhiteFadeAlpha;
} }
g.pushTransform(); g.pushTransform();
@ -269,38 +291,40 @@ public class Slider extends GameObject {
} }
} }
// draw combo number and overlay if not initially clicked
if (!sliderClickedInitial) { if (!sliderClickedInitial) {
data.drawSymbolNumber(hitObject.getComboNumber(), x, y, data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
hitCircle.getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha); 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(); g.popTransform();
// repeats // repeats
if (isCurveCompletelyDrawn) { if (isCurveCompletelyDrawn) {
for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) { for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1 && tcurRepeat < repeatCount - 1; tcurRepeat++) {
if (hitObject.getRepeatCount() - 1 > tcurRepeat) {
Image arrow = GameImage.REVERSEARROW.getImage(); Image arrow = GameImage.REVERSEARROW.getImage();
arrow = arrow.getScaledCopy((float) (1 + 0.2d * ((trackPosition + sliderTime * tcurRepeat) % 292) / 292)); arrow = arrow.getScaledCopy((float) (1 + 0.2d * ((trackPosition + sliderTime * tcurRepeat) % 292) / 292));
Color arrowColor = Color.white; if (tcurRepeat == 0) {
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); arrow.setAlpha(Options.isSliderSnaking() ? decorationsAlpha : 1f);
} else {
if (!sliderClickedInitial) {
continue;
}
arrow.setAlpha(getCircleAlphaAfterRepeat(trackPosition, tcurRepeat % 2 == 0));
}
if (tcurRepeat % 2 == 0) { if (tcurRepeat % 2 == 0) {
// last circle // last circle
arrow.setRotation(curve.getEndAngle()); arrow.setRotation(curve.getEndAngle());
arrow.drawCentered(endPos.x, endPos.y, arrowColor); arrow.drawCentered(endPos.x, endPos.y);
} else { } else {
// first circle // first circle
arrow.setRotation(curve.getStartAngle()); arrow.setRotation(curve.getStartAngle());
arrow.drawCentered(x, y, arrowColor); arrow.drawCentered(x, y);
}
} }
} }
} }
@ -447,6 +471,24 @@ public class Slider extends GameObject {
return curveIntervalTo == 1d; 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. * Calculates the slider hit result.
* @return the hit result (GameData.HIT_* constants) * @return the hit result (GameData.HIT_* constants)
@ -513,17 +555,19 @@ public class Slider extends GameObject {
float cx, cy; float cx, cy;
HitObjectType type; HitObjectType type;
if (currentRepeats % 2 == 0) { // last circle if (currentRepeats % 2 == 0) {
// last circle
Vec2f lastPos = curve.pointAt(1); Vec2f lastPos = curve.pointAt(1);
cx = lastPos.x; cx = lastPos.x;
cy = lastPos.y; cy = lastPos.y;
type = HitObjectType.SLIDER_LAST; type = HitObjectType.SLIDER_LAST;
} else { // first circle } else {
// first circle
cx = x; cx = x;
cy = y; cy = y;
type = HitObjectType.SLIDER_FIRST; 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, cx, cy, color, comboEnd, hitObject, type, sliderHeldToEnd,
currentRepeats + 1, curve, sliderHeldToEnd); currentRepeats + 1, curve, sliderHeldToEnd);
if (Options.isMirror() && GameMod.AUTO.isActive()) { if (Options.isMirror() && GameMod.AUTO.isActive()) {
@ -550,15 +594,18 @@ public class Slider extends GameObject {
if (timeDiff < hitResultOffset[GameData.HIT_50]) { if (timeDiff < hitResultOffset[GameData.HIT_50]) {
result = GameData.HIT_SLIDER30; result = GameData.HIT_SLIDER30;
ticksHit++; 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; result = GameData.HIT_MISS;
data.sendSliderStartResult(trackPosition, this.x, this.y, color, false);
}
//else not a hit //else not a hit
if (result > -1) { if (result > -1) {
data.sendInitialSliderResult(trackPosition, this.x, this.y, color, mirrorColor); data.sendInitialSliderResult(trackPosition, this.x, this.y, color, mirrorColor);
data.addHitError(hitObject.getTime(), x,y,trackPosition - hitObject.getTime()); data.addHitError(hitObject.getTime(), x,y,trackPosition - hitObject.getTime());
sliderClickedInitial = true; 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; return true;
} }
} }
@ -579,9 +626,12 @@ public class Slider extends GameObject {
sliderClickedInitial = true; sliderClickedInitial = true;
if (isAutoMod) { // "auto" mod: catch any missed notes due to lag if (isAutoMod) { // "auto" mod: catch any missed notes due to lag
ticksHit++; ticksHit++;
data.sliderTickResult(time, GameData.HIT_SLIDER30, x, y, hitObject, currentRepeats); data.sendSliderTickResult(time, GameData.HIT_SLIDER30, x, y, hitObject, currentRepeats);
} else data.sendSliderStartResult(time, x, y, color, true);
data.sliderTickResult(time, GameData.HIT_MISS, x, y, hitObject, currentRepeats); } 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 // "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]) { if (Math.abs(trackPosition - time) < hitResultOffset[GameData.HIT_300]) {
ticksHit++; ticksHit++;
sliderClickedInitial = true; sliderClickedInitial = true;
data.sliderTickResult(time, GameData.HIT_SLIDER30, x, y, hitObject, currentRepeats); data.sendSliderTickResult(time, GameData.HIT_SLIDER30, x, y, hitObject, currentRepeats);
data.sendInitialSliderResult(time, x, y, color, mirrorColor); data.sendSliderStartResult(time, x, y, color, true);
} }
} }
@ -644,23 +694,6 @@ public class Slider extends GameObject {
tickIndex = 0; tickIndex = 0;
isNewRepeat = true; isNewRepeat = true;
tickExpandTime = TICK_EXPAND_TIME; 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 // held during new repeat
if (isNewRepeat) { if (isNewRepeat) {
ticksHit++; ticksHit++;
if (currentRepeats % 2 > 0) { // last circle
int lastIndex = hitObject.getSliderX().length; HitObjectType type;
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30, float posX, posY;
curve.getX(lastIndex), curve.getY(lastIndex), hitObject, currentRepeats); if (currentRepeats % 2 > 0) {
} else // first circle // last circle
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30, type = HitObjectType.SLIDER_LAST;
c.x, c.y, hitObject, currentRepeats); 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 // held during new tick
if (isNewTick) { if (isNewTick) {
ticksHit++; ticksHit++;
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER10, data.sendSliderTickResult(trackPosition, GameData.HIT_SLIDER10,
c.x, c.y, hitObject, currentRepeats); c.x, c.y, hitObject, currentRepeats);
} }
@ -711,9 +759,9 @@ public class Slider extends GameObject {
followCircleActive = false; followCircleActive = false;
if (isNewRepeat) if (isNewRepeat)
data.sliderTickResult(trackPosition, GameData.HIT_MISS, 0, 0, hitObject, currentRepeats); data.sendSliderTickResult(trackPosition, GameData.HIT_MISS, 0, 0, hitObject, currentRepeats);
if (isNewTick) if (isNewTick)
data.sliderTickResult(trackPosition, GameData.HIT_MISS, 0, 0, hitObject, currentRepeats); data.sendSliderTickResult(trackPosition, GameData.HIT_MISS, 0, 0, hitObject, currentRepeats);
} }
return false; return false;

View File

@ -255,7 +255,7 @@ public class Spinner extends GameObject {
else else
result = GameData.HIT_MISS; 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); Color.transparent, true, hitObject, HitObjectType.SPINNER, true, 0, null, false);
return result; return result;
} }

View File

@ -101,12 +101,12 @@ public abstract class Curve {
Curve.borderColor = borderColor; Curve.borderColor = borderColor;
ContextCapabilities capabilities = GLContext.getCapabilities(); ContextCapabilities capabilities = GLContext.getCapabilities();
mmsliderSupported = capabilities.OpenGL20; mmsliderSupported = capabilities.OpenGL30;
if (mmsliderSupported) if (mmsliderSupported)
CurveRenderState.init(width, height, circleDiameter); CurveRenderState.init(width, height, circleDiameter);
else { else {
if (Options.getSkin().getSliderStyle() != Skin.STYLE_PEPPYSLIDER) 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.");
} }
} }

View File

@ -46,8 +46,8 @@ import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException; import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState; import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame; 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.EmptyTransition;
import org.newdawn.slick.state.transition.FadeInTransition;
/** /**
* Generic button menu state. * Generic button menu state.
@ -69,8 +69,8 @@ public class ButtonMenu extends BasicGameState {
Button.NO.click(container, game); Button.NO.click(container, game);
} }
}, },
/** The initial beatmap management screen. */ /** The initial beatmap management screen (for a non-"favorite" beatmap). */
BEATMAP (new Button[] { Button.CLEAR_SCORES, Button.DELETE, Button.CANCEL }) { BEATMAP (new Button[] { Button.CLEAR_SCORES, Button.FAVORITE_ADD, Button.DELETE, Button.CANCEL }) {
@Override @Override
public String[] getTitle(GameContainer container, StateBasedGame game) { public String[] getTitle(GameContainer container, StateBasedGame game) {
BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode(); BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
@ -90,6 +90,23 @@ public class ButtonMenu extends BasicGameState {
super.scroll(container, game, newValue); 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. */ /** 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 }) { BEATMAP_DELETE_SELECT (new Button[] { Button.DELETE_GROUP, Button.DELETE_SONG, Button.CANCEL_DELETE }) {
@Override @Override
@ -468,6 +485,25 @@ public class ButtonMenu extends BasicGameState {
game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition()); 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) { DELETE ("Delete...", Color.red) {
@Override @Override
public void click(GameContainer container, StateBasedGame game) { public void click(GameContainer container, StateBasedGame game) {

View File

@ -49,6 +49,7 @@ import itdelatrisu.opsu.replay.PlaybackSpeed;
import itdelatrisu.opsu.replay.Replay; import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.replay.ReplayFrame; import itdelatrisu.opsu.replay.ReplayFrame;
import itdelatrisu.opsu.ui.*; import itdelatrisu.opsu.ui.*;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation; import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.io.File; import java.io.File;
@ -98,6 +99,9 @@ public class Game extends BasicGameState {
/** Screen fade-out time, in milliseconds, when health hits zero. */ /** Screen fade-out time, in milliseconds, when health hits zero. */
private static final int LOSE_FADEOUT_TIME = 500; 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. */ /** Maximum rotation, in degrees, over fade out upon death. */
private static final float MAX_ROTATION = 90f; private static final float MAX_ROTATION = 90f;
@ -287,6 +291,15 @@ public class Game extends BasicGameState {
/** The current alpha of the scoreboard. */ /** The current alpha of the scoreboard. */
private float currentScoreboardAlpha; 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. */ /** Music position bar background colors. */
private static final Color private static final Color
MUSICBAR_NORMAL = new Color(12, 9, 10, 0.25f), 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); musicBarWidth = Math.max(width * 0.005f, 7);
musicBarHeight = height * 0.9f; 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 // create the associated GameData object
data = new GameData(width, height); data = new GameData(width, height);
} }
@ -513,7 +532,7 @@ public class Game extends BasicGameState {
} }
if (!Options.isHideUI() || !GameMod.AUTO.isActive()) { if (!Options.isHideUI() || !GameMod.AUTO.isActive()) {
data.drawGameElements(g, true, objectIndex == 0); data.drawGameElements(g, true, objectIndex == 0, 1f);
} }
if (breakLength >= 8000 && if (breakLength >= 8000 &&
@ -552,7 +571,13 @@ public class Game extends BasicGameState {
else { else {
if (!GameMod.AUTO.isActive() || !Options.isHideUI()) { if (!GameMod.AUTO.isActive() || !Options.isHideUI()) {
// game elements // 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 // skip beginning
if (objectIndex == 0 && if (objectIndex == 0 &&
@ -639,6 +664,7 @@ public class Game extends BasicGameState {
ScoreData currentScore = data.getCurrentScoreData(beatmap, true); ScoreData currentScore = data.getCurrentScoreData(beatmap, true);
while (currentRank > 0 && previousScores[currentRank - 1].score < currentScore.score) { while (currentRank > 0 && previousScores[currentRank - 1].score < currentScore.score) {
currentRank--; currentRank--;
scoreboardStarStream.burst(20);
lastRankUpdateTime = trackPosition; lastRankUpdateTime = trackPosition;
} }
@ -647,6 +673,9 @@ public class Game extends BasicGameState {
); );
int scoreboardPosition = 2 * container.getHeight() / 3; int scoreboardPosition = 2 * container.getHeight() / 3;
// draw star stream behind the scores
scoreboardStarStream.draw();
if (currentRank < 4) { if (currentRank < 4) {
// draw the (new) top 5 ranks // draw the (new) top 5 ranks
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
@ -749,6 +778,7 @@ public class Game extends BasicGameState {
playbackSpeed.getButton().hoverUpdate(delta, mouseX, mouseY); playbackSpeed.getButton().hoverUpdate(delta, mouseX, mouseY);
int trackPosition = MusicController.getPosition(); int trackPosition = MusicController.getPosition();
int firstObjectTime = beatmap.objects[0].getTime(); int firstObjectTime = beatmap.objects[0].getTime();
scoreboardStarStream.update(delta);
// returning from pause screen: must click previous mouse position // returning from pause screen: must click previous mouse position
if (pauseTime > -1) { if (pauseTime > -1) {
@ -803,11 +833,11 @@ public class Game extends BasicGameState {
} }
// normal game update // normal game update
if (!isReplay) if (!isReplay && !gameFinished)
addReplayFrameAndRun(mouseX, mouseY, lastKeysPressed, trackPosition); addReplayFrameAndRun(mouseX, mouseY, lastKeysPressed, trackPosition);
// watching replay // watching replay
else { else if (!gameFinished) {
// out of frames, use previous data // out of frames, use previous data
if (replayIndex >= replay.frames.length) if (replayIndex >= replay.frames.length)
updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed); updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed);
@ -857,7 +887,8 @@ public class Game extends BasicGameState {
// update in-game scoreboard // update in-game scoreboard
if (!Options.isHideUI() && previousScores != null && trackPosition > firstObjectTime) { if (!Options.isHideUI() && previousScores != null && trackPosition > firstObjectTime) {
// show scoreboard if selected, and always in break // 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; currentScoreboardAlpha += 1f / SCOREBOARD_FADE_IN_TIME * delta;
if (currentScoreboardAlpha > 1f) if (currentScoreboardAlpha > 1f)
currentScoreboardAlpha = 1f; currentScoreboardAlpha = 1f;
@ -869,6 +900,14 @@ public class Game extends BasicGameState {
} }
data.updateDisplays(delta); 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) if (MusicController.trackEnded() && objectIndex < gameObjects.length)
gameObjects[objectIndex].update(true, delta, mouseX, mouseY, false, trackPosition); gameObjects[objectIndex].update(true, delta, mouseX, mouseY, false, trackPosition);
// if checkpoint used, skip ranking screen // save score and replay
if (checkpointLoaded) if (!checkpointLoaded) {
game.closeRequested();
// go to ranking screen
else {
boolean unranked = (GameMod.AUTO.isActive() || GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive()); boolean unranked = (GameMod.AUTO.isActive() || GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive());
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data); ((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
if (isReplay) if (isReplay)
@ -918,9 +953,12 @@ public class Game extends BasicGameState {
// add score to database // add score to database
if (!unranked && !isReplay) if (!unranked && !isReplay)
ScoreDB.addScore(score); ScoreDB.addScore(score);
game.enterState(Opsu.STATE_GAMERANKING, new EasedFadeOutTransition(), new FadeInTransition());
} }
// start timer
gameFinished = true;
gameFinishedTimer.setTime(0);
return; return;
} }
@ -1050,6 +1088,8 @@ public class Game extends BasicGameState {
@Override @Override
public void keyPressed(int key, char c) { public void keyPressed(int key, char c) {
if (gameFinished)
return;
if (sbOverlay.keyPressed(key, c)) { if (sbOverlay.keyPressed(key, c)) {
return; return;
@ -1199,9 +1239,13 @@ public class Game extends BasicGameState {
@Override @Override
public void mousePressed(int button, int x, int y) { public void mousePressed(int button, int x, int y) {
if (gameFinished)
return;
if (sbOverlay.mousePressed(button, x, y)) { if (sbOverlay.mousePressed(button, x, y)) {
return; return;
} }
// watching replay // watching replay
if (isReplay || GameMod.AUTO.isActive()) { if (isReplay || GameMod.AUTO.isActive()) {
if (button == Input.MOUSE_MIDDLE_BUTTON) if (button == Input.MOUSE_MIDDLE_BUTTON)
@ -1294,6 +1338,9 @@ public class Game extends BasicGameState {
@Override @Override
public void mouseReleased(int button, int x, int y) { public void mouseReleased(int button, int x, int y) {
if (gameFinished)
return;
if (sbOverlay.mouseReleased(button, x, y)) { if (sbOverlay.mouseReleased(button, x, y)) {
return; return;
} }
@ -1315,6 +1362,9 @@ public class Game extends BasicGameState {
@Override @Override
public void keyReleased(int key, char c) { public void keyReleased(int key, char c) {
if (gameFinished)
return;
int keys = ReplayFrame.KEY_NONE; int keys = ReplayFrame.KEY_NONE;
if (key == Options.getGameKeyLeft()) if (key == Options.getGameKeyLeft())
keys = ReplayFrame.KEY_K1; keys = ReplayFrame.KEY_K1;
@ -1343,7 +1393,7 @@ public class Game extends BasicGameState {
if (sbOverlay.mouseWheelMoved(newValue)) { if (sbOverlay.mouseWheelMoved(newValue)) {
return; return;
} }
if (Options.isMouseWheelDisabled() || Options.isMouseDisabled()) if (Options.isMouseWheelDisabled())
return; return;
UI.changeVolume((newValue < 0) ? -1 : 1); UI.changeVolume((newValue < 0) ? -1 : 1);
@ -1375,6 +1425,11 @@ public class Game extends BasicGameState {
// restart the game // restart the game
if (restart != Restart.FALSE) { if (restart != Restart.FALSE) {
// update play stats
if (restart == Restart.NEW) {
beatmap.incrementPlayCounter();
BeatmapDB.updatePlayStatistics(beatmap);
}
// load epilepsy warning img // load epilepsy warning img
epiImgTime = Options.getEpilepsyWarningLength(); epiImgTime = Options.getEpilepsyWarningLength();
@ -1400,11 +1455,13 @@ public class Game extends BasicGameState {
loadImages(); loadImages();
setMapModifiers(); setMapModifiers();
retries = 0; retries = 0;
} else if (restart == Restart.MANUAL) { } else if (restart == Restart.MANUAL && !GameMod.AUTO.isActive()) {
// retry // retry
retries++; retries++;
} else if (restart == Restart.REPLAY) } else if (restart == Restart.REPLAY || GameMod.AUTO.isActive()) {
// replay
retries = 0; retries = 0;
}
gameObjects = new GameObject[beatmap.objects.length]; gameObjects = new GameObject[beatmap.objects.length];
playbackSpeed = PlaybackSpeed.NORMAL; playbackSpeed = PlaybackSpeed.NORMAL;
@ -1613,6 +1670,11 @@ public class Game extends BasicGameState {
* @param trackPosition the track position * @param trackPosition the track position
*/ */
private void drawHitObjects(Graphics g, int trackPosition) { private void drawHitObjects(Graphics g, int trackPosition) {
// draw result objects
if (!Options.isHideObjects()) {
data.drawHitResults(trackPosition);
}
if (Options.isMergingSliders() && knorkesliders != null) { if (Options.isMergingSliders() && knorkesliders != null) {
knorkesliders.draw(Color.white, this.slidercurveFrom, this.slidercurveTo); knorkesliders.draw(Color.white, this.slidercurveFrom, this.slidercurveTo);
if (Options.isMirror()) { if (Options.isMirror()) {
@ -1622,6 +1684,7 @@ public class Game extends BasicGameState {
g.popTransform(); g.popTransform();
} }
} }
// include previous object in follow points // include previous object in follow points
int lastObjectIndex = -1; int lastObjectIndex = -1;
if (objectIndex > 0 && objectIndex < beatmap.objects.length && if (objectIndex > 0 && objectIndex < beatmap.objects.length &&
@ -1736,18 +1799,13 @@ public class Game extends BasicGameState {
// translate and rotate the object // translate and rotate the object
g.translate(0, dt * dt * container.getHeight()); 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); g.rotate(rotationCenter.x, rotationCenter.y, rotSpeed * dt);
gameObj.draw(g, trackPosition, false); gameObj.draw(g, trackPosition, false);
g.popTransform(); g.popTransform();
} }
} }
// draw result objects
if (!Options.isHideObjects()) {
data.drawHitResults(trackPosition);
}
} }
/** /**
@ -1760,7 +1818,7 @@ public class Game extends BasicGameState {
} }
this.beatmap = beatmap; this.beatmap = beatmap;
Display.setTitle(String.format("%s - %s", game.getTitle(), beatmap.toString())); Display.setTitle(String.format("%s - %s", game.getTitle(), beatmap.toString()));
if (beatmap.timingPoints == null) if (beatmap.breaks == null)
BeatmapDB.load(beatmap, BeatmapDB.LOAD_ARRAY); BeatmapDB.load(beatmap, BeatmapDB.LOAD_ARRAY);
BeatmapParser.parseHitObjects(beatmap); BeatmapParser.parseHitObjects(beatmap);
HitSound.setDefaultSampleSet(beatmap.sampleSet); HitSound.setDefaultSampleSet(beatmap.sampleSet);
@ -1792,6 +1850,9 @@ public class Game extends BasicGameState {
autoMousePosition = new Vec2f(); autoMousePosition = new Vec2f();
autoMousePressed = false; autoMousePressed = false;
flashlightRadius = container.getHeight() * 2 / 3; flashlightRadius = container.getHeight() * 2 / 3;
scoreboardStarStream.clear();
gameFinished = false;
gameFinishedTimer.setTime(0);
System.gc(); System.gc();
} }

View File

@ -184,7 +184,7 @@ public class GamePauseMenu extends BasicGameState {
@Override @Override
public void mouseWheelMoved(int newValue) { public void mouseWheelMoved(int newValue) {
if (Options.isMouseWheelDisabled() || Options.isMouseDisabled()) if (Options.isMouseWheelDisabled())
return; return;
UI.changeVolume((newValue < 0) ? -1 : 1); UI.changeVolume((newValue < 0) ? -1 : 1);

View File

@ -32,8 +32,12 @@ import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.beatmap.TimingPoint; import itdelatrisu.opsu.beatmap.TimingPoint;
import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.states.ButtonMenu.MenuState; 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.MenuButton.Expand;
import itdelatrisu.opsu.ui.StarFountain;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimatedValue; import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation; import itdelatrisu.opsu.ui.animations.AnimationEquation;
@ -51,8 +55,8 @@ import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException; import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState; import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame; 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.EasedFadeOutTransition;
import org.newdawn.slick.state.transition.FadeInTransition;
/** /**
* "Main Menu" state. * "Main Menu" state.
@ -117,15 +121,18 @@ public class MainMenu extends BasicGameState {
/** Music position bar coordinates and dimensions. */ /** Music position bar coordinates and dimensions. */
private float musicBarX, musicBarY, musicBarWidth, musicBarHeight; private float musicBarX, musicBarY, musicBarWidth, musicBarHeight;
/** Last measure progress value. */
private float lastMeasureProgress = 0f;
/** The star fountain. */
private StarFountain starFountain;
// game-related variables // game-related variables
private GameContainer container; private GameContainer container;
private StateBasedGame game; private StateBasedGame game;
private Input input; private Input input;
private final int state; private final int state;
private float hue = 0;
private boolean huedone = false;
public MainMenu(int state) { public MainMenu(int state) {
this.state = state; this.state = state;
} }
@ -203,9 +210,6 @@ public class MainMenu extends BasicGameState {
repoButton.setHoverAnimationDuration(350); repoButton.setHoverAnimationDuration(350);
repoButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK); repoButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
repoButton.setHoverExpand(); repoButton.setHoverExpand();
}
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { // only if a webpage can be opened
Image repoImg = GameImage.REPOSITORY.getImage(); Image repoImg = GameImage.REPOSITORY.getImage();
danceRepoButton = new MenuButton(repoImg, danceRepoButton = new MenuButton(repoImg,
startX - repoImg.getWidth(), startY - repoImg.getHeight() startX - repoImg.getWidth(), startY - repoImg.getHeight()
@ -228,6 +232,9 @@ public class MainMenu extends BasicGameState {
restartButton.setHoverAnimationEquation(AnimationEquation.LINEAR); restartButton.setHoverAnimationEquation(AnimationEquation.LINEAR);
restartButton.setHoverRotate(360); restartButton.setHoverRotate(360);
// initialize star fountain
starFountain = new StarFountain(width, height);
// logo animations // logo animations
float centerOffsetX = width / 6.5f; float centerOffsetX = width / 6.5f;
logoOpen = new AnimatedValue(100, 0, centerOffsetX, AnimationEquation.OUT_QUAD); 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); g.fillRect(0, height * 8 / 9f, width, height / 9f);
Colors.BLACK_ALPHA.a = oldAlpha; Colors.BLACK_ALPHA.a = oldAlpha;
// draw star fountain
starFountain.draw();
// draw downloads button // draw downloads button
downloadsButton.draw(); downloadsButton.draw();
@ -271,26 +281,15 @@ public class MainMenu extends BasicGameState {
exitButton.draw(); exitButton.draw();
} }
// logo // draw logo (pulsing)
Double position = MusicController.getBeatProgress(); Float position = MusicController.getBeatProgress();
Color color = Options.isColorMainMenuLogo() ? Cursor.lastCursorColor : Color.white; if (position == null) // default to 60bpm
boolean renderPiece = position != null; position = System.currentTimeMillis() % 1000 / 1000f;
if (position == null) { float scale = 1f + position * 0.05f;
position = System.currentTimeMillis() % 1000 / 1000d; logo.draw(Color.white, scale);
} float ghostScale = logo.getLastScale() / scale * 1.05f;
double scale = 1 - (0 - position) * 0.05; Image ghostLogo = GameImage.MENU_LOGO.getImage().getScaledCopy(ghostScale);
logo.draw(color, (float) scale); ghostLogo.drawCentered(logo.getX(), logo.getY(), Colors.GHOST_LOGO);
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 music buttons // draw music buttons
if (MusicController.isPlaying()) if (MusicController.isPlaying())
@ -310,16 +309,13 @@ public class MainMenu extends BasicGameState {
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth * musicBarPosition, musicBarHeight, 4); g.fillRoundRect(musicBarX, musicBarY, musicBarWidth * musicBarPosition, musicBarHeight, 4);
} }
// draw repository button // draw repository buttons
if (repoButton != null) { if (repoButton != null) {
repoButton.draw(); repoButton.draw();
String text = "opsu!"; String text = "opsu!";
int fheight = Fonts.SMALL.getLineHeight(); int fheight = Fonts.SMALL.getLineHeight();
int fwidth = Fonts.SMALL.getWidth(text); int fwidth = Fonts.SMALL.getWidth(text);
Fonts.SMALL.drawString(repoButton.getX() - fwidth / 2, repoButton.getY() - repoButton.getImage().getHeight() / 2 - fheight, text, Color.white); Fonts.SMALL.drawString(repoButton.getX() - fwidth / 2, repoButton.getY() - repoButton.getImage().getHeight() / 2 - fheight, text, Color.white);
}
if (danceRepoButton != null) {
danceRepoButton.draw(); danceRepoButton.draw();
String text = "opsu!dance"; String text = "opsu!dance";
int fheight = Fonts.SMALL.getLineHeight(); int fheight = Fonts.SMALL.getLineHeight();
@ -367,15 +363,15 @@ public class MainMenu extends BasicGameState {
throws SlickException { throws SlickException {
UI.update(delta); UI.update(delta);
if (MusicController.trackEnded()) 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(); int mouseX = input.getMouseX(), mouseY = input.getMouseY();
logo.hoverUpdate(delta, mouseX, mouseY, 0.25f); logo.hoverUpdate(delta, mouseX, mouseY, 0.25f);
playButton.hoverUpdate(delta, mouseX, mouseY, 0.25f); playButton.hoverUpdate(delta, mouseX, mouseY, 0.25f);
exitButton.hoverUpdate(delta, mouseX, mouseY, 0.25f); exitButton.hoverUpdate(delta, mouseX, mouseY, 0.25f);
if (repoButton != null) if (repoButton != null) {
repoButton.hoverUpdate(delta, mouseX, mouseY); repoButton.hoverUpdate(delta, mouseX, mouseY);
if (danceRepoButton != null)
danceRepoButton.hoverUpdate(delta, mouseX, mouseY); danceRepoButton.hoverUpdate(delta, mouseX, mouseY);
}
if (Updater.get().showButton()) { if (Updater.get().showButton()) {
updateButton.autoHoverUpdate(delta, true); updateButton.autoHoverUpdate(delta, true);
restartButton.autoHoverUpdate(delta, false); restartButton.autoHoverUpdate(delta, false);
@ -389,6 +385,7 @@ public class MainMenu extends BasicGameState {
noHoverUpdate |= contains; noHoverUpdate |= contains;
musicNext.hoverUpdate(delta, !noHoverUpdate && musicNext.contains(mouseX, mouseY)); musicNext.hoverUpdate(delta, !noHoverUpdate && musicNext.contains(mouseX, mouseY));
musicPrevious.hoverUpdate(delta, !noHoverUpdate && musicPrevious.contains(mouseX, mouseY)); musicPrevious.hoverUpdate(delta, !noHoverUpdate && musicPrevious.contains(mouseX, mouseY));
starFountain.update(delta);
// window focus change: increase/decrease theme song volume // window focus change: increase/decrease theme song volume
if (MusicController.isThemePlaying() && if (MusicController.isThemePlaying() &&
@ -400,6 +397,14 @@ public class MainMenu extends BasicGameState {
if (!(Options.isDynamicBackgroundEnabled() && beatmap != null && beatmap.isBackgroundLoading())) if (!(Options.isDynamicBackgroundEnabled() && beatmap != null && beatmap.isBackgroundLoading()))
bgAlpha.update(delta); bgAlpha.update(delta);
// check measure progress
Float measureProgress = MusicController.getMeasureProgress(2);
if (measureProgress != null) {
if (measureProgress < lastMeasureProgress)
starFountain.burst(true);
lastMeasureProgress = measureProgress;
}
// buttons // buttons
int centerX = container.getWidth() / 2; int centerX = container.getWidth() / 2;
float currentLogoButtonAlpha; 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 // reset button hover states if mouse is not currently hovering over the button
int mouseX = input.getMouseX(), mouseY = input.getMouseY(); int mouseX = input.getMouseX(), mouseY = input.getMouseY();
if (!logo.contains(mouseX, mouseY, 0.25f)) if (!logo.contains(mouseX, mouseY, 0.25f))
@ -514,6 +523,7 @@ public class MainMenu extends BasicGameState {
// music position bar // music position bar
if (MusicController.isPlaying()) { if (MusicController.isPlaying()) {
if (musicPositionBarContains(x, y)) { if (musicPositionBarContains(x, y)) {
lastMeasureProgress = 0f;
float pos = (x - musicBarX) / musicBarWidth; float pos = (x - musicBarX) / musicBarWidth;
MusicController.setPosition((int) (pos * MusicController.getDuration())); MusicController.setPosition((int) (pos * MusicController.getDuration()));
return; return;
@ -531,10 +541,11 @@ public class MainMenu extends BasicGameState {
} }
return; return;
} else if (musicNext.contains(x, y)) { } else if (musicNext.contains(x, y)) {
nextTrack(); nextTrack(true);
UI.sendBarNotification(">> Next"); UI.sendBarNotification(">> Next");
return; return;
} else if (musicPrevious.contains(x, y)) { } else if (musicPrevious.contains(x, y)) {
lastMeasureProgress = 0f;
if (!previous.isEmpty()) { if (!previous.isEmpty()) {
SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU); SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
menu.setFocus(BeatmapSetList.get().getBaseNode(previous.pop()), -1, true, false); 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()); game.enterState(Opsu.STATE_DOWNLOADSMENU, new EasedFadeOutTransition(), new FadeInTransition());
break; break;
case Input.KEY_R: case Input.KEY_R:
nextTrack(); nextTrack(true);
break; break;
case Input.KEY_UP: case Input.KEY_UP:
UI.changeVolume(1); UI.changeVolume(1);
@ -717,9 +728,17 @@ public class MainMenu extends BasicGameState {
/** /**
* Plays the next track, and adds the previous one to the stack. * 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(); 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); SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
BeatmapSetNode node = menu.setFocus(BeatmapSetList.get().getRandomNode(), -1, true, false); BeatmapSetNode node = menu.setFocus(BeatmapSetList.get().getRandomNode(), -1, true, false);
boolean sameAudio = false; boolean sameAudio = false;

View File

@ -32,6 +32,7 @@ import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.Beatmap; import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.BeatmapDifficultyCalculator; import itdelatrisu.opsu.beatmap.BeatmapDifficultyCalculator;
import itdelatrisu.opsu.beatmap.BeatmapGroup;
import itdelatrisu.opsu.beatmap.BeatmapParser; import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.BeatmapSet; import itdelatrisu.opsu.beatmap.BeatmapSet;
import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.beatmap.BeatmapSetList;
@ -45,6 +46,7 @@ import itdelatrisu.opsu.db.BeatmapDB;
import itdelatrisu.opsu.db.ScoreDB; import itdelatrisu.opsu.db.ScoreDB;
import itdelatrisu.opsu.states.ButtonMenu.MenuState; import itdelatrisu.opsu.states.ButtonMenu.MenuState;
import itdelatrisu.opsu.ui.Colors; import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.DropdownMenu;
import itdelatrisu.opsu.ui.Fonts; import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.KineticScrolling; import itdelatrisu.opsu.ui.KineticScrolling;
import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.MenuButton;
@ -259,8 +261,11 @@ public class SongMenu extends BasicGameState {
/** Header and footer end and start y coordinates, respectively. */ /** Header and footer end and start y coordinates, respectively. */
private float headerY, footerY; private float headerY, footerY;
/** Height of the footer */ /** Footer pulsing logo button. */
private float footerHeight; private MenuButton footerLogoButton;
/** Size of the pulsing logo in the footer. */
private float footerLogoSize;
/** Time, in milliseconds, for fading the search bar. */ /** Time, in milliseconds, for fading the search bar. */
private int searchTransitionTimer = SEARCH_TRANSITION_TIME; private int searchTransitionTimer = SEARCH_TRANSITION_TIME;
@ -309,9 +314,15 @@ public class SongMenu extends BasicGameState {
/** The star stream. */ /** The star stream. */
private StarStream starStream; 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). */ /** Whether the menu is currently scrolling to the focus node (blocks other actions). */
private boolean isScrollingToFocusNode = false; private boolean isScrollingToFocusNode = false;
/** Sort order dropdown menu. */
private DropdownMenu<BeatmapSortOrder> sortMenu;
// game-related variables // game-related variables
private GameContainer container; private GameContainer container;
private StateBasedGame game; private StateBasedGame game;
@ -337,11 +348,48 @@ public class SongMenu extends BasicGameState {
Fonts.BOLD.getLineHeight() + Fonts.DEFAULT.getLineHeight() + Fonts.BOLD.getLineHeight() + Fonts.DEFAULT.getLineHeight() +
Fonts.SMALL.getLineHeight(); Fonts.SMALL.getLineHeight();
footerY = height - GameImage.SELECTION_MODS.getImage().getHeight(); 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 // initialize sorts
for (BeatmapSortOrder sort : BeatmapSortOrder.values()) int sortWidth = (int) (width * 0.12f);
sort.init(width, headerY - SongMenu.DIVIDER_LINE_WIDTH / 2); sortMenu = new DropdownMenu<BeatmapSortOrder>(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 // initialize score data buttons
ScoreData.init(width, headerY + height * 0.01f); ScoreData.init(width, headerY + height * 0.01f);
@ -414,7 +462,9 @@ public class SongMenu extends BasicGameState {
}); });
// star stream // 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 @Override
@ -425,6 +475,7 @@ public class SongMenu extends BasicGameState {
int width = container.getWidth(); int width = container.getWidth();
int height = container.getHeight(); int height = container.getHeight();
int mouseX = input.getMouseX(), mouseY = input.getMouseY(); int mouseX = input.getMouseX(), mouseY = input.getMouseY();
boolean inDropdownMenu = sortMenu.contains(mouseX, mouseY);
// background // background
if (focusNode != null) { if (focusNode != null) {
@ -496,7 +547,7 @@ public class SongMenu extends BasicGameState {
g.clearClip(); g.clearClip();
// scroll bar // 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()); 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.drawLine(0, footerY, width, footerY);
g.resetLineWidth(); g.resetLineWidth();
// opsu logo in bottom bar // footer logo (pulsing)
Image logo = GameImage.MENU_LOGO.getImage(); Float position = MusicController.getBeatProgress();
float logoSize = footerHeight * 2f; if (position == null) // default to 60bpm
logo = logo.getScaledCopy(logoSize / logo.getWidth()); position = System.currentTimeMillis() % 1000 / 1000f;
Double position = MusicController.getBeatProgress(); if (footerLogoButton.contains(mouseX, mouseY, 0.25f) && !inDropdownMenu) {
float x = width - footerHeight * 0.61f; // hovering over logo: stop pulsing
float y = height - footerHeight * 0.40f; footerLogoButton.draw();
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;
} else { } 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 // header
@ -607,20 +655,22 @@ public class SongMenu extends BasicGameState {
GameImage.SELECTION_OTHER_OPTIONS.getImage().drawCentered(selectOptionsButton.getX(), selectOptionsButton.getY()); GameImage.SELECTION_OTHER_OPTIONS.getImage().drawCentered(selectOptionsButton.getX(), selectOptionsButton.getY());
selectOptionsButton.draw(); selectOptionsButton.draw();
// sorting tabs // group tabs
BeatmapSortOrder currentSort = BeatmapSortOrder.getSort(); BeatmapGroup currentGroup = BeatmapGroup.current();
BeatmapSortOrder hoverSort = null; BeatmapGroup hoverGroup = null;
for (BeatmapSortOrder sort : BeatmapSortOrder.values()) { if (!inDropdownMenu) {
if (sort.contains(mouseX, mouseY)) { for (BeatmapGroup group : BeatmapGroup.values()) {
hoverSort = sort; if (group.contains(mouseX, mouseY)) {
hoverGroup = group;
break; break;
} }
} }
for (BeatmapSortOrder sort : BeatmapSortOrder.VALUES_REVERSED) {
if (sort != currentSort)
sort.draw(false, sort == hoverSort);
} }
currentSort.draw(true, false); for (BeatmapGroup group : BeatmapGroup.VALUES_REVERSED) {
if (group != currentGroup)
group.draw(false, group == hoverGroup);
}
currentGroup.draw(true, false);
// search // search
boolean searchEmpty = search.getText().isEmpty(); boolean searchEmpty = search.getText().isEmpty();
@ -655,6 +705,9 @@ public class SongMenu extends BasicGameState {
(searchResultString == null) ? "Searching..." : searchResultString, Color.white); (searchResultString == null) ? "Searching..." : searchResultString, Color.white);
} }
// sorting options
sortMenu.render(container, g);
// reloading beatmaps // reloading beatmaps
if (reloadThread != null) { if (reloadThread != null) {
// darken the screen // darken the screen
@ -678,20 +731,25 @@ public class SongMenu extends BasicGameState {
if (reloadThread == null) if (reloadThread == null)
MusicController.loopTrackIfEnded(true); MusicController.loopTrackIfEnded(true);
else if (reloadThread.isFinished()) { else if (reloadThread.isFinished()) {
BeatmapGroup.set(BeatmapGroup.ALL);
BeatmapSortOrder.set(BeatmapSortOrder.TITLE);
BeatmapSetList.get().reset();
BeatmapSetList.get().init();
if (BeatmapSetList.get().size() > 0) { if (BeatmapSetList.get().size() > 0) {
// initialize song list // initialize song list
BeatmapSetList.get().init();
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true); setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
} else } else
MusicController.playThemeSong(); MusicController.playThemeSong();
reloadThread = null; reloadThread = null;
} }
int mouseX = input.getMouseX(), mouseY = input.getMouseY(); int mouseX = input.getMouseX(), mouseY = input.getMouseY();
boolean inDropdownMenu = sortMenu.contains(mouseX, mouseY);
UI.getBackButton().hoverUpdate(delta, mouseX, mouseY); UI.getBackButton().hoverUpdate(delta, mouseX, mouseY);
selectModsButton.hoverUpdate(delta, mouseX, mouseY); selectModsButton.hoverUpdate(delta, mouseX, mouseY);
selectRandomButton.hoverUpdate(delta, mouseX, mouseY); selectRandomButton.hoverUpdate(delta, mouseX, mouseY);
selectMapOptionsButton.hoverUpdate(delta, mouseX, mouseY); selectMapOptionsButton.hoverUpdate(delta, mouseX, mouseY);
selectOptionsButton.hoverUpdate(delta, mouseX, mouseY); selectOptionsButton.hoverUpdate(delta, mouseX, mouseY);
footerLogoButton.hoverUpdate(delta, mouseX, mouseY, 0.25f);
// beatmap menu timer // beatmap menu timer
if (beatmapMenuTimer > -1) { if (beatmapMenuTimer > -1) {
@ -699,7 +757,9 @@ public class SongMenu extends BasicGameState {
if (beatmapMenuTimer >= BEATMAP_MENU_DELAY) { if (beatmapMenuTimer >= BEATMAP_MENU_DELAY) {
beatmapMenuTimer = -1; beatmapMenuTimer = -1;
if (focusNode != null) { 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); game.enterState(Opsu.STATE_BUTTONMENU);
} }
return; return;
@ -788,7 +848,7 @@ public class SongMenu extends BasicGameState {
// mouse hover // mouse hover
BeatmapSetNode node = getNodeAtPosition(mouseX, mouseY); BeatmapSetNode node = getNodeAtPosition(mouseX, mouseY);
if (node != null) { if (node != null && !inDropdownMenu) {
if (node == hoverIndex) if (node == hoverIndex)
hoverOffset.update(delta); hoverOffset.update(delta);
else { else {
@ -802,7 +862,9 @@ public class SongMenu extends BasicGameState {
} }
// tooltips // 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 startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset()); int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS); int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS);
@ -880,24 +942,41 @@ public class SongMenu extends BasicGameState {
return; return;
} }
// group tabs
for (BeatmapGroup group : BeatmapGroup.values()) {
if (group.contains(x, y)) {
if (group != BeatmapGroup.current()) {
BeatmapGroup.set(group);
SoundController.playSound(SoundEffect.MENUCLICK);
startNode = focusNode = null;
oldFocusNode = null;
randomStack = new Stack<SongNode>();
songInfo = null;
scoreMap = null;
focusScores = null;
search.setText("");
searchTimer = SEARCH_DELAY;
searchTransitionTimer = SEARCH_TRANSITION_TIME;
searchResultString = null;
BeatmapSetList.get().reset();
BeatmapSetList.get().init();
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
if (BeatmapSetList.get().size() < 1 && group.getEmptyMessage() != null)
UI.sendBarNotification(group.getEmptyMessage());
}
return;
}
}
if (focusNode == null) if (focusNode == null)
return; return;
// sorting buttons // logo: start game
for (BeatmapSortOrder sort : BeatmapSortOrder.values()) { if (footerLogoButton.contains(x, y, 0.25f)) {
if (sort.contains(x, y)) { startGame();
if (sort != BeatmapSortOrder.getSort()) {
BeatmapSortOrder.setSort(sort);
SoundController.playSound(SoundEffect.MENUCLICK);
BeatmapSetNode oldFocusBase = BeatmapSetList.get().getBaseNode(focusNode.index);
int oldFocusFileIndex = focusNode.beatmapIndex;
focusNode = null;
BeatmapSetList.get().init();
setFocus(oldFocusBase, oldFocusFileIndex, true, true);
}
return; return;
} }
}
// song buttons // song buttons
BeatmapSetNode node = getNodeAtPosition(x, y); BeatmapSetNode node = getNodeAtPosition(x, y);
@ -978,6 +1057,7 @@ public class SongMenu extends BasicGameState {
search.setText(""); search.setText("");
searchTimer = SEARCH_DELAY; searchTimer = SEARCH_DELAY;
searchTransitionTimer = 0; searchTransitionTimer = 0;
searchResultString = null;
} else { } else {
// return to main menu // return to main menu
SoundController.playSound(SoundEffect.MENUBACK); SoundController.playSound(SoundEffect.MENUBACK);
@ -999,7 +1079,11 @@ public class SongMenu extends BasicGameState {
SongNode prev; SongNode prev;
if (randomStack.isEmpty() || (prev = randomStack.pop()) == null) if (randomStack.isEmpty() || (prev = randomStack.pop()) == null)
break; 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 { } else {
// random track, add previous to stack // random track, add previous to stack
randomStack.push(new SongNode(BeatmapSetList.get().getBaseNode(focusNode.index), focusNode.beatmapIndex)); randomStack.push(new SongNode(BeatmapSetList.get().getBaseNode(focusNode.index), focusNode.beatmapIndex));
@ -1010,7 +1094,9 @@ public class SongMenu extends BasicGameState {
if (focusNode == null) if (focusNode == null)
break; break;
SoundController.playSound(SoundEffect.MENUHIT); 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); game.enterState(Opsu.STATE_BUTTONMENU);
break; break;
case Input.KEY_F5: case Input.KEY_F5:
@ -1045,11 +1131,6 @@ public class SongMenu extends BasicGameState {
case Input.KEY_ENTER: case Input.KEY_ENTER:
if (focusNode == null) if (focusNode == null)
break; 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(); startGame();
break; break;
case Input.KEY_DOWN: case Input.KEY_DOWN:
@ -1194,6 +1275,8 @@ public class SongMenu extends BasicGameState {
songChangeTimer.setTime(songChangeTimer.getDuration()); songChangeTimer.setTime(songChangeTimer.getDuration());
musicIconBounceTimer.setTime(musicIconBounceTimer.getDuration()); musicIconBounceTimer.setTime(musicIconBounceTimer.getDuration());
starStream.clear(); starStream.clear();
sortMenu.activate();
sortMenu.reset();
// reset song stack // reset song stack
randomStack = new Stack<SongNode>(); randomStack = new Stack<SongNode>();
@ -1241,6 +1324,15 @@ public class SongMenu extends BasicGameState {
focusScores = getScoreDataForNode(focusNode, true); 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; resetGame = false;
} }
@ -1325,6 +1417,19 @@ public class SongMenu extends BasicGameState {
case RELOAD: // reload beatmaps case RELOAD: // reload beatmaps
reloadBeatmaps(true); reloadBeatmaps(true);
break; break;
case BEATMAP_FAVORITE: // removed favorite, reset beatmap list
if (BeatmapGroup.current() == BeatmapGroup.FAVORITE) {
startNode = focusNode = null;
oldFocusNode = null;
randomStack = new Stack<SongNode>();
songInfo = null;
scoreMap = null;
focusScores = null;
BeatmapSetList.get().reset();
BeatmapSetList.get().init();
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
}
break;
default: default:
break; break;
} }
@ -1338,6 +1443,7 @@ public class SongMenu extends BasicGameState {
public void leave(GameContainer container, StateBasedGame game) public void leave(GameContainer container, StateBasedGame game)
throws SlickException { throws SlickException {
search.setFocus(false); search.setFocus(false);
sortMenu.deactivate();
} }
/** /**
@ -1430,7 +1536,8 @@ public class SongMenu extends BasicGameState {
focusNode = BeatmapSetList.get().getNode(node, beatmapIndex); focusNode = BeatmapSetList.get().getNode(node, beatmapIndex);
Beatmap beatmap = focusNode.getSelectedBeatmap(); Beatmap beatmap = focusNode.getSelectedBeatmap();
if (beatmap.timingPoints == null) { if (beatmap.timingPoints == null) {
BeatmapParser.parseOnlyTimingPoints(beatmap); // parse timing points so we can pulse the logo
BeatmapParser.parseTimingPoints(beatmap);
} }
MusicController.play(beatmap, false, preview); MusicController.play(beatmap, false, preview);
@ -1670,12 +1777,19 @@ public class SongMenu extends BasicGameState {
if (MusicController.isTrackLoading()) if (MusicController.isTrackLoading())
return; return;
SoundController.playSound(SoundEffect.MENUHIT);
Beatmap beatmap = MusicController.getBeatmap(); Beatmap beatmap = MusicController.getBeatmap();
if (focusNode == null || beatmap != focusNode.getSelectedBeatmap()) { if (focusNode == null || beatmap != focusNode.getSelectedBeatmap()) {
UI.sendBarNotification("Unable to load the beatmap audio."); UI.sendBarNotification("Unable to load the beatmap audio.");
return; 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(); MultiClip.destroyExtraClips();
Game gameState = (Game) game.getState(Opsu.STATE_GAME); Game gameState = (Game) game.getState(Opsu.STATE_GAME);
gameState.loadBeatmap(beatmap); gameState.loadBeatmap(beatmap);

View File

@ -33,6 +33,7 @@ public class Colors {
BLUE_BACKGROUND = new Color(74, 130, 255), BLUE_BACKGROUND = new Color(74, 130, 255),
BLUE_BUTTON = new Color(40, 129, 237), BLUE_BUTTON = new Color(40, 129, 237),
ORANGE_BUTTON = new Color(200, 90, 3), ORANGE_BUTTON = new Color(200, 90, 3),
PINK_BUTTON = new Color(223, 71, 147),
YELLOW_ALPHA = new Color(255, 255, 0, 0.4f), YELLOW_ALPHA = new Color(255, 255, 0, 0.4f),
WHITE_FADE = new Color(255, 255, 255, 1f), WHITE_FADE = new Color(255, 255, 255, 1f),
RED_HOVER = new Color(255, 112, 112), RED_HOVER = new Color(255, 112, 112),
@ -50,7 +51,6 @@ public class Colors {
BLACK_BG_FOCUS = new Color(0, 0, 0, 0.75f), 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. // This class should not be instantiated.
private Colors() {} private Colors() {}
} }

View File

@ -320,8 +320,6 @@ public class Cursor {
// reset angles // reset angles
cursorAngle = 0f; cursorAngle = 0f;
GameImage.CURSOR.getImage().setRotation(0f);
GameImage.CURSOR_TRAIL.getImage().setRotation(0f);
} }
/** /**

View File

@ -95,6 +95,9 @@ public class DropdownMenu<E> extends AbstractComponent {
/** The chevron images. */ /** The chevron images. */
private Image chevronDown, chevronRight; private Image chevronDown, chevronRight;
/** Should the next click be blocked? */
private boolean blockClick = false;
/** /**
* Creates a new dropdown menu. * Creates a new dropdown menu.
* @param container the container rendering this menu * @param container the container rendering this menu
@ -327,6 +330,7 @@ public class DropdownMenu<E> extends AbstractComponent {
this.expanded = false; this.expanded = false;
this.lastUpdateTime = 0; this.lastUpdateTime = 0;
expandProgress.setTime(0); expandProgress.setTime(0);
blockClick = false;
} }
@Override @Override
@ -349,9 +353,21 @@ public class DropdownMenu<E> extends AbstractComponent {
this.itemIndex = idx; this.itemIndex = idx;
itemSelected(idx, items[idx]); itemSelected(idx, items[idx]);
} }
blockClick = true;
consumeEvent(); 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). * Notification that a new item was selected (via override).
* @param index the index of the item selected * @param index the index of the item selected

View File

@ -98,11 +98,8 @@ public class MenuButton {
/** The default max rotation angle of the button. */ /** The default max rotation angle of the button. */
private static final float DEFAULT_ANGLE_MAX = 30f; private static final float DEFAULT_ANGLE_MAX = 30f;
private float currentScale = 1f; /** The last scale at which the button was drawn. */
private float lastScale = 1f;
public float getCurrentScale() {
return currentScale;
}
/** /**
* Creates a new button from an Image. * Creates a new button from an Image.
@ -172,6 +169,11 @@ public class MenuButton {
*/ */
public float getY() { return y; } 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. * Sets text to draw in the middle of the button.
* @param text the text to draw * @param text the text to draw
@ -197,21 +199,21 @@ public class MenuButton {
/** /**
* Draws the button. * 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. * Draws the button with a color filter.
* @param filter the color to filter with when drawing * @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. * Draw the button with a color filter at the given scale.
* @param filter the color to filter with when drawing * @param filter the color to filter with when drawing
* @param scaleoverride the scale to use when drawing * @param scaleOverride the scale to use when drawing (only works for normal images)
*/ */
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public void draw(Color filter, float scaleoverride) { public void draw(Color filter, float scaleOverride) {
// animations: get current frame // animations: get current frame
Image image = this.img; Image image = this.img;
if (image == null) { if (image == null) {
@ -219,20 +221,17 @@ public class MenuButton {
image = anim.getCurrentFrame(); image = anim.getCurrentFrame();
} }
currentScale = 1f;
// normal images // normal images
if (imgL == null) { if (imgL == null) {
float scaleposmodx = 0; float xScaleOffset = 0f, yScaleOffset = 0f;
float scaleposmody = 0; if (scaleOverride != 1f) {
if (scaleoverride != 1f) { image = image.getScaledCopy(scaleOverride);
image = image.getScaledCopy(scaleoverride); xScaleOffset = image.getWidth() / 2f - xRadius;
scaleposmodx = image.getWidth() / 2 - xRadius; yScaleOffset = image.getHeight() / 2f - yRadius;
scaleposmody = image.getHeight() / 2 - yRadius;
currentScale = scaleoverride;
} }
lastScale = scaleOverride;
if (hoverEffect == 0) if (hoverEffect == 0)
image.draw(x - xRadius - scaleposmodx, y - yRadius - scaleposmody, filter); image.draw(x - xRadius, y - yRadius, filter);
else { else {
float oldAlpha = image.getAlpha(); float oldAlpha = image.getAlpha();
float oldAngle = image.getRotation(); float oldAngle = image.getRotation();
@ -240,16 +239,18 @@ public class MenuButton {
if (scale.getValue() != 1f) { if (scale.getValue() != 1f) {
image = image.getScaledCopy(scale.getValue()); image = image.getScaledCopy(scale.getValue());
image.setAlpha(oldAlpha); image.setAlpha(oldAlpha);
scaleposmodx = image.getWidth() / 2 - xRadius; if (scaleOverride != 1f) {
scaleposmody = image.getHeight() / 2 - yRadius; xScaleOffset = image.getWidth() / 2f - xRadius;
currentScale *= scale.getValue(); yScaleOffset = image.getHeight() / 2f - yRadius;
}
lastScale *= scale.getValue();
} }
} }
if ((hoverEffect & EFFECT_FADE) > 0) if ((hoverEffect & EFFECT_FADE) > 0)
image.setAlpha(alpha.getValue()); image.setAlpha(alpha.getValue());
if ((hoverEffect & EFFECT_ROTATE) > 0) if ((hoverEffect & EFFECT_ROTATE) > 0)
image.setRotation(angle.getValue()); 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) { if (image == this.img) {
image.setAlpha(oldAlpha); image.setAlpha(oldAlpha);
image.setRotation(oldAngle); image.setRotation(oldAngle);

View File

@ -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);
}
}

View File

@ -20,6 +20,7 @@ package itdelatrisu.opsu.ui;
import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.ui.animations.AnimatedValue; import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation; import itdelatrisu.opsu.ui.animations.AnimationEquation;
@ -31,11 +32,35 @@ import java.util.Random;
import org.newdawn.slick.Image; import org.newdawn.slick.Image;
/** /**
* Horizontal star stream. * Star stream.
*/ */
public class StarStream { public class StarStream {
/** The container dimensions. */ /** The origin of the star stream. */
private final int containerWidth, containerHeight; 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. */ /** The star image. */
private final Image starImg; private final Image starImg;
@ -43,33 +68,41 @@ public class StarStream {
/** The current list of stars. */ /** The current list of stars. */
private final List<Star> stars; private final List<Star> stars;
/** The maximum number of stars to draw at once. */
private static final int MAX_STARS = 20;
/** Random number generator instance. */ /** Random number generator instance. */
private final Random random; private final Random random;
/** Contains data for a single star. */ /** Contains data for a single star. */
private class 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. */ /** The star animation progress. */
private final AnimatedValue animatedValue; private final AnimatedValue animatedValue;
/** The star properties. */
private final int distance, yOffset, angle;
/** /**
* Creates a star with the given properties. * 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 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 * @param eqn the animation equation to use
*/ */
public Star(int duration, int distance, int yOffset, int angle, AnimationEquation eqn) { public Star(Vec2f offset, Vec2f direction, int angle, float scale, int duration, AnimationEquation eqn) {
this.animatedValue = new AnimatedValue(duration, 0f, 1f, eqn); this.offset = offset;
this.distance = distance; this.dir = direction;
this.yOffset = yOffset;
this.angle = angle; 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(); float t = animatedValue.getValue();
starImg.setImageColor(1f, 1f, 1f, Math.min((1 - t) * 5f, 1f)); starImg.setImageColor(1f, 1f, 1f, Math.min((1 - t) * 5f, 1f));
starImg.drawEmbedded( starImg.drawEmbedded(
containerWidth - (distance * t), ((containerHeight - starImg.getHeight()) / 2) + yOffset, offset.x + t * dir.x, offset.y + t * dir.y,
starImg.getWidth(), starImg.getHeight(), angle); starImg.getWidth() * scale, starImg.getHeight() * scale, angle
);
} }
/** /**
@ -93,17 +127,60 @@ public class StarStream {
/** /**
* Initializes the star stream. * Initializes the star stream.
* @param width the container width * @param x the x position
* @param height the container height * @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) { public StarStream(float x, float y, float dirX, float dirY, int k) {
this.containerWidth = width; this.position = new Vec2f(x, y);
this.containerHeight = height; this.direction = new Vec2f(dirX, dirY);
this.maxStars = k;
this.starImg = GameImage.STAR2.getImage().copy(); this.starImg = GameImage.STAR2.getImage().copy();
this.stars = new ArrayList<Star>(); this.stars = new ArrayList<Star>(k);
this.random = new Random(); 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. * Draws the star stream.
*/ */
@ -131,20 +208,36 @@ public class StarStream {
} }
// create new stars // create new stars
for (int i = stars.size(); i < MAX_STARS; i++) { for (int i = stars.size(); i < maxStars; i++) {
if (Math.random() < ((i < 5) ? 0.25 : 0.66)) if (Math.random() < ((i < maxStars / 4) ? 0.25 : 0.66))
break; break; // stagger spawning new stars
// generate star properties 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); float distanceRatio = Utils.clamp((float) getGaussian(0.65, 0.25), 0.2f, 0.925f);
int distance = (int) (containerWidth * distanceRatio); Vec2f offset = position.cpy().add(direction.cpy().nor().normalize().scale((float) getGaussian(0, positionSpread)));
int duration = (int) (distanceRatio * getGaussian(1300, 300)); Vec2f dir = direction.cpy().scale(distanceRatio).add((float) getGaussian(0, directionSpread), (float) getGaussian(0, directionSpread));
int yOffset = (int) getGaussian(0, containerHeight / 20);
int angle = (int) getGaussian(0, 22.5); 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; AnimationEquation eqn = random.nextBoolean() ? AnimationEquation.IN_OUT_QUAD : AnimationEquation.OUT_QUAD;
stars.add(new Star(duration, distance, yOffset, angle, eqn)); 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());
} }
/** /**
@ -152,6 +245,11 @@ public class StarStream {
*/ */
public void clear() { stars.clear(); } 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 * Returns the next pseudorandom, Gaussian ("normally") distributed {@code double} value
* with the given mean and standard deviation. * with the given mean and standard deviation.