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

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

View File

@ -31,5 +31,6 @@ The following projects were referenced in creating opsu!:
Theme Song
----------
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

Binary file not shown.

Binary file not shown.

View File

@ -21,7 +21,9 @@ package itdelatrisu.opsu;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.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();

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -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}.

View File

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

View File

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

View File

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

View File

@ -0,0 +1,179 @@
package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.PriorityQueue;
import org.newdawn.slick.Image;
/**
* Beatmap groups.
*/
public enum BeatmapGroup {
/** All beatmaps (no filter). */
ALL (0, "All Songs", null),
/** Most recently played beatmaps. */
RECENT (1, "Last Played", "Your recently played beatmaps will appear in this list!") {
/** Number of elements to show. */
private static final int K = 20;
/** Returns the latest "last played" time in a beatmap set. */
private long lastPlayed(BeatmapSet set) {
long max = 0;
for (Beatmap beatmap : set) {
if (beatmap.lastPlayed > max)
max = beatmap.lastPlayed;
}
return max;
}
@Override
public ArrayList<BeatmapSetNode> filter(ArrayList<BeatmapSetNode> list) {
// find top K elements
PriorityQueue<BeatmapSetNode> pq = new PriorityQueue<BeatmapSetNode>(K, new Comparator<BeatmapSetNode>() {
@Override
public int compare(BeatmapSetNode v, BeatmapSetNode w) {
return Long.compare(lastPlayed(v.getBeatmapSet()), lastPlayed(w.getBeatmapSet()));
}
});
for (BeatmapSetNode node : list) {
long timestamp = lastPlayed(node.getBeatmapSet());
if (timestamp == 0)
continue; // skip unplayed beatmaps
if (pq.size() < K || timestamp > lastPlayed(pq.peek().getBeatmapSet())) {
if (pq.size() == K)
pq.poll();
pq.add(node);
}
}
// return as list
ArrayList<BeatmapSetNode> filteredList = new ArrayList<BeatmapSetNode>();
for (BeatmapSetNode node : pq)
filteredList.add(node);
return filteredList;
}
},
/** "Favorite" beatmaps. */
FAVORITE (2, "Favorites", "Right-click a beatmap to add it to your Favorites!") {
@Override
public ArrayList<BeatmapSetNode> filter(ArrayList<BeatmapSetNode> list) {
// find "favorite" beatmaps
ArrayList<BeatmapSetNode> filteredList = new ArrayList<BeatmapSetNode>();
for (BeatmapSetNode node : list) {
if (node.getBeatmapSet().isFavorite())
filteredList.add(node);
}
return filteredList;
}
};
/** The ID of the group (used for tab positioning). */
private final int id;
/** The name of the group. */
private final String name;
/** The message to display if this list is empty. */
private final String emptyMessage;
/** The tab associated with the group (displayed in Song Menu screen). */
private MenuButton tab;
/** Total number of groups. */
private static final int SIZE = values().length;
/** Array of BeatmapGroup objects in reverse order. */
public static final BeatmapGroup[] VALUES_REVERSED;
static {
VALUES_REVERSED = values();
Collections.reverse(Arrays.asList(VALUES_REVERSED));
}
/** Current group. */
private static BeatmapGroup currentGroup = ALL;
/**
* Returns the current group.
* @return the current group
*/
public static BeatmapGroup current() { return currentGroup; }
/**
* Sets a new group.
* @param group the new group
*/
public static void set(BeatmapGroup group) { currentGroup = group; }
/**
* Constructor.
* @param id the ID of the group (for tab positioning)
* @param name the group name
* @param emptyMessage the message to display if this list is empty
*/
BeatmapGroup(int id, String name, String emptyMessage) {
this.id = id;
this.name = name;
this.emptyMessage = emptyMessage;
}
/**
* Returns the message to display if this list is empty.
* @return the message, or null if none
*/
public String getEmptyMessage() { return emptyMessage; }
/**
* Returns a filtered list of beatmap set nodes.
* @param list the unfiltered list
* @return the filtered list
*/
public ArrayList<BeatmapSetNode> filter(ArrayList<BeatmapSetNode> list) {
return list;
}
/**
* Initializes the tab.
* @param containerWidth the container width
* @param bottomY the bottom y coordinate
*/
public void init(int containerWidth, float bottomY) {
Image tab = GameImage.MENU_TAB.getImage();
int tabWidth = tab.getWidth();
float buttonX = containerWidth / 2f;
float tabOffset = (containerWidth - buttonX - tabWidth) / (SIZE - 1);
if (tabOffset > tabWidth) { // prevent tabs from being spaced out
tabOffset = tabWidth;
buttonX = (containerWidth * 0.99f) - (tabWidth * SIZE);
}
this.tab = new MenuButton(tab,
(buttonX + (tabWidth / 2f)) + (id * tabOffset),
bottomY - (tab.getHeight() / 2f)
);
}
/**
* Checks if the coordinates are within the image bounds.
* @param x the x coordinate
* @param y the y coordinate
* @return true if within bounds
*/
public boolean contains(float x, float y) { return tab.contains(x, y); }
/**
* Draws the tab.
* @param selected whether the tab is selected (white) or not (red)
* @param isHover whether to include a hover effect (unselected only)
*/
public void draw(boolean selected, boolean isHover) {
UI.drawTab(tab.getX(), tab.getY(), name, selected, isHover);
}
}

View File

@ -112,6 +112,7 @@ public class BeatmapParser {
// parse directories
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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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.
*/

View File

@ -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)) {

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {}

View File

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

View File

@ -95,6 +95,9 @@ public class DropdownMenu<E> extends AbstractComponent {
/** The chevron images. */
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

View File

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

View File

@ -0,0 +1,143 @@
package itdelatrisu.opsu.ui;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import org.newdawn.slick.Image;
/**
* Star fountain consisting of two star streams.
*/
public class StarFountain {
/** The (approximate) number of stars in each burst. */
private static final int BURST_SIZE = 125;
/** Star streams. */
private final StarStream left, right;
/** Burst progress. */
private final AnimatedValue burstProgress = new AnimatedValue(1000, 0, 1, AnimationEquation.LINEAR);
/** The maximum direction offsets. */
private final float xDirection, yDirection;
/** Motion types. */
private enum Motion {
NONE {
@Override
public void init(StarFountain fountain) {
fountain.left.setDirection(0, fountain.yDirection);
fountain.right.setDirection(0, fountain.yDirection);
fountain.left.setDirectionSpread(20f);
fountain.right.setDirectionSpread(20f);
fountain.left.setDurationSpread(1000, 200);
fountain.right.setDurationSpread(1000, 200);
}
},
OUTWARD_SWEEP {
@Override
public void init(StarFountain fountain) {
fountain.left.setDirectionSpread(0f);
fountain.right.setDirectionSpread(0f);
fountain.left.setDurationSpread(850, 0);
fountain.right.setDurationSpread(850, 0);
}
@Override
public void update(StarFountain fountain) {
float t = fountain.burstProgress.getValue();
fountain.left.setDirection(fountain.xDirection - fountain.xDirection * 2f * t, fountain.yDirection);
fountain.right.setDirection(-fountain.xDirection + fountain.xDirection * 2f * t, fountain.yDirection);
}
},
INWARD_SWEEP {
@Override
public void init(StarFountain fountain) { OUTWARD_SWEEP.init(fountain); }
@Override
public void update(StarFountain fountain) {
float t = fountain.burstProgress.getValue();
fountain.left.setDirection(-fountain.xDirection + fountain.xDirection * 2f * t, fountain.yDirection);
fountain.right.setDirection(fountain.xDirection - fountain.xDirection * 2f * t, fountain.yDirection);
}
};
/** Initializes the streams in the fountain. */
public void init(StarFountain fountain) {}
/** Updates the streams in the fountain. */
public void update(StarFountain fountain) {}
}
/** The current motion. */
private Motion motion = Motion.NONE;
/**
* Initializes the star fountain.
* @param containerWidth the container width
* @param containerHeight the container height
*/
public StarFountain(int containerWidth, int containerHeight) {
Image img = GameImage.STAR2.getImage();
float xOffset = containerWidth * 0.125f;
this.xDirection = containerWidth / 2f - xOffset;
this.yDirection = -containerHeight * 0.85f;
this.left = new StarStream(xOffset - img.getWidth() / 2f, containerHeight, 0, yDirection, 0);
this.right = new StarStream(containerWidth - xOffset - img.getWidth() / 2f, containerHeight, 0, yDirection, 0);
left.setScaleSpread(1.1f, 0.2f);
right.setScaleSpread(1.1f, 0.2f);
}
/**
* Draws the star fountain.
*/
public void draw() {
left.draw();
right.draw();
}
/**
* Updates the stars in the fountain by a delta interval.
* @param delta the delta interval since the last call
*/
public void update(int delta) {
left.update(delta);
right.update(delta);
if (burstProgress.update(delta)) {
motion.update(this);
int size = Math.round((float) delta / burstProgress.getDuration() * BURST_SIZE);
left.burst(size);
right.burst(size);
}
}
/**
* Creates a burst of stars to be processed during the next {@link #update(int)} call.
* @param wait if {@code true}, will not burst if a previous burst is in progress
*/
public void burst(boolean wait) {
if (wait && (burstProgress.getTime() < burstProgress.getDuration() || !left.isEmpty() || !right.isEmpty()))
return; // previous burst in progress
burstProgress.setTime(0);
Motion lastMotion = motion;
motion = Motion.values()[(int) (Math.random() * Motion.values().length)];
if (motion == lastMotion) // don't do the same sweep twice
motion = Motion.NONE;
motion.init(this);
}
/**
* Clears the stars currently in the fountain.
*/
public void clear() {
left.clear();
right.clear();
burstProgress.setTime(burstProgress.getDuration());
motion = Motion.NONE;
motion.init(this);
}
}

View File

@ -20,6 +20,7 @@ package itdelatrisu.opsu.ui;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.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.