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