diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index 04cf78c1..80617239 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -39,6 +39,9 @@ public class GameData { /** Delta multiplier for steady HP drain. */ public static final float HP_DRAIN_MULTIPLIER = 1 / 200f; + /** Time, in milliseconds, for a hit error tick to fade. */ + private static final int HIT_ERROR_FADE_TIME = 5000; + /** Letter grades. */ public enum Grade { NULL (null, null), @@ -150,29 +153,44 @@ public class GameData { /** Current x coordinate of the combo burst image (for sliding animation). */ private int comboBurstX; + /** Time offsets for obtaining each hit result (indexed by HIT_* constants). */ + private int[] hitResultOffset; + /** List of hit result objects associated with hit objects. */ private LinkedList hitResultList; /** - * class to store Hit Error information. + * Class to store hit error information. * @author fluddokt */ - class HitErrorInfo { - int time, x, y, timeDiff; + private class HitErrorInfo { + /** The correct hit time. */ + private int time; + /** The coordinates of the hit. */ + @SuppressWarnings("unused") + private int x, y; + + /** The difference between the correct and actual hit times. */ + private int timeDiff; + + /** + * Constructor. + * @param time the correct hit time + * @param x the x coordinate of the hit + * @param y the y coordinate of the hit + * @param timeDiff the difference between the correct and actual hit times + */ public HitErrorInfo(int time, int x, int y, int timeDiff) { this.time = time; this.x = x; this.y = y; this.timeDiff = timeDiff; } - } - /** - * List of stored Hit Error Info - */ - private LinkedList hitErrorList = new LinkedList(); + /** List containing recent hit error information. */ + private LinkedList hitErrorList; /** * Hit result helper class. @@ -246,10 +264,6 @@ public class GameData { /** Container dimensions. */ private int width, height; - /** Time offsets for obtaining each hit result (indexed by HIT_* constants). */ - private int[] hitResultOffset; - - /** * Constructor for gameplay. * @param width container width @@ -302,13 +316,13 @@ public class GameData { healthDisplay = 100f; hitResultCount = new int[HIT_MAX]; hitResultList = new LinkedList(); + hitErrorList = new LinkedList(); fullObjectCount = 0; combo = 0; comboMax = 0; comboEnd = 0; comboBurstIndex = -1; scoreData = null; - hitErrorList.clear(); } /** @@ -390,10 +404,11 @@ public class GameData { public void setDifficulty(float difficulty) { this.difficulty = difficulty; } public float getDifficulty() { return difficulty; } - /** Sets the HitResultOffset */ - public void setHitResultOffset(int[] hitResultOffset) {this.hitResultOffset = hitResultOffset; } - - + /** + * Sets the array of hit result offsets. + */ + public void setHitResultOffset(int[] hitResultOffset) { this.hitResultOffset = hitResultOffset; } + /** * Draws a number with defaultSymbols. * @param n the number to draw @@ -446,7 +461,7 @@ public class GameData { /** * Draws game elements: * scorebar, score, score percentage, map progress circle, - * mod icons, combo count, combo burst, and grade. + * mod icons, combo count, combo burst, hit error bar, and grade. * @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 @@ -547,50 +562,46 @@ public class GameData { // combo count if (combo > 0) // 0 isn't a combo drawSymbolString(String.format("%dx", combo), 10, height - 10 - symbolHeight, 1.0f, false); - - //* - //Draw Hit Error bar - final int fadeDelay = 5000; - int hitErrorY = 30; - Iterator iter2 = hitErrorList.iterator(); - g.setColor(Color.black); - g.fillRect(width / 2f - 3 - hitResultOffset[HIT_50], - height - marginX - hitErrorY - 10, - hitResultOffset[HIT_50] * 2, - 20); - g.setColor(Utils.COLOR_LIGHT_ORANGE); - g.fillRect(width / 2f - 3 - hitResultOffset[HIT_50], - height - marginX - hitErrorY - 3, - hitResultOffset[HIT_50] * 2, - 6); - g.setColor(Utils.COLOR_LIGHT_GREEN); - g.fillRect(width / 2f - 3 - hitResultOffset[HIT_100], - height - marginX - hitErrorY - 3, - hitResultOffset[HIT_100] * 2, - 6); - g.setColor(Utils.COLOR_LIGHT_BLUE); - g.fillRect(width / 2f - 3 - hitResultOffset[HIT_300], - height- marginX - hitErrorY - 3, - hitResultOffset[HIT_300] * 2, - 6); - g.setColor(Color.white); - g.drawRect(width / 2f - 3, height - marginX - hitErrorY - 10, 6, 20); - while (iter2.hasNext()) { - HitErrorInfo info = iter2.next(); - int time = info.time; - if (Math.abs(info.timeDiff) < hitResultOffset[GameData.HIT_50] - && time + fadeDelay > trackPosition) { - float alpha = 1 - ((float) (trackPosition - time) / fadeDelay); - Color col = new Color(Color.white); - col.a = alpha; - g.setColor(col); - g.fillRect(width / 2 + info.timeDiff - 1, height - marginX - - hitErrorY - 10, 2, 20); - } else { - iter2.remove(); + + // hit error bar + if (Options.isHitErrorBarEnabled()) { + // draw bar + int hitErrorY = 30; + g.setColor(Color.black); + g.fillRect(width / 2f - 3 - hitResultOffset[HIT_50], + height - marginX - hitErrorY - 10, + hitResultOffset[HIT_50] * 2, 20); + g.setColor(Utils.COLOR_LIGHT_ORANGE); + g.fillRect(width / 2f - 3 - hitResultOffset[HIT_50], + height - marginX - hitErrorY - 3, + hitResultOffset[HIT_50] * 2, 6); + g.setColor(Utils.COLOR_LIGHT_GREEN); + g.fillRect(width / 2f - 3 - hitResultOffset[HIT_100], + height - marginX - hitErrorY - 3, + hitResultOffset[HIT_100] * 2, 6); + g.setColor(Utils.COLOR_LIGHT_BLUE); + g.fillRect(width / 2f - 3 - hitResultOffset[HIT_300], + height - marginX - hitErrorY - 3, + hitResultOffset[HIT_300] * 2, 6); + g.setColor(Color.white); + g.drawRect(width / 2f - 3, height - marginX - hitErrorY - 10, 6, 20); + + // draw ticks + Color white = new Color(Color.white); + Iterator iter = hitErrorList.iterator(); + while (iter.hasNext()) { + HitErrorInfo info = iter.next(); + int time = info.time; + if (Math.abs(info.timeDiff) < hitResultOffset[GameData.HIT_50] && + time + HIT_ERROR_FADE_TIME > trackPosition) { + float alpha = 1 - ((float) (trackPosition - time) / HIT_ERROR_FADE_TIME); + white.a = alpha; + g.setColor(white); + g.fillRect(width / 2 + info.timeDiff - 1, height - marginX - hitErrorY - 10, 2, 20); + } else + iter.remove(); } } - //*/ } else { // grade Grade grade = getGrade(); @@ -602,9 +613,6 @@ public class GameData { ); } } - - - } /** @@ -728,7 +736,7 @@ public class GameData { Image scaledLighting1 = GameImage.LIGHTING1.getImage().getScaledCopy(scale); scaledLighting.setAlpha(hitResult.alpha); scaledLighting1.setAlpha(hitResult.alpha); - + scaledLighting.draw(hitResult.x - (scaledLighting.getWidth() / 2f), hitResult.y - (scaledLighting.getHeight() / 2f), hitResult.color); scaledLighting1.draw(hitResult.x - (scaledLighting1.getWidth() / 2f), @@ -1088,15 +1096,15 @@ public class GameData { * @return true if gameplay, false if score viewing */ public boolean isGameplay() { return gameplay; } - + /** - * add a Hit Error Info to the list. - * @param time when it should have been hit - * @param x, y the location that it hit - * @param timeDiff the difference from the time from when it was actually hit + * Adds the hit into the list of hit error information. + * @param time the correct hit time + * @param x the x coordinate of the hit + * @param y the y coordinate of the hit + * @param timeDiff the difference between the correct and actual hit times */ public void addHitError(int time, int x, int y, int timeDiff) { hitErrorList.add(new HitErrorInfo(time, x, y, timeDiff)); } - } \ No newline at end of file diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index bbf666ad..cceb39d1 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -363,6 +363,13 @@ public class Options { @Override public void click(GameContainer container) { themeSongEnabled = !themeSongEnabled; } + }, + SHOW_HIT_ERROR_BAR ("Show Hit Error Bar", "Displays hit accuracy information at the bottom of the screen.") { + @Override + public String getValueString() { return showHitErrorBar ? "Yes" : "No"; } + + @Override + public void click(GameContainer container) { showHitErrorBar = !showHitErrorBar; } }; /** Option name. */ @@ -432,7 +439,6 @@ public class Options { RES_2560_1440 (2560, 1440), RES_2560_1600 (2560, 1600), RES_3840_2160 (3840, 2160); - /** Screen dimensions. */ private int width, height; @@ -535,6 +541,9 @@ public class Options { /** Whether or not to play the theme song. */ private static boolean themeSongEnabled = true; + /** Whether or not to show the hit error bar. */ + private static boolean showHitErrorBar = false; + /** Fixed difficulty overrides. */ private static float fixedCS = 0f, fixedHP = 0f, @@ -765,7 +774,7 @@ public class Options { * Returns whether or not to play the theme song. * @return true if enabled */ - public static boolean isThemSongEnabled() { return themeSongEnabled; } + public static boolean isThemeSongEnabled() { return themeSongEnabled; } /** * Sets the track checkpoint time, if within bounds. @@ -780,6 +789,12 @@ public class Options { return false; } + /** + * Returns whether or not to show the hit error bar. + * @return true if enabled + */ + public static boolean isHitErrorBarEnabled() { return showHitErrorBar; } + /** * Returns the left game key. * @return the left key code @@ -1042,6 +1057,9 @@ public class Options { case "PerfectHit": showPerfectHit = Boolean.parseBoolean(value); break; + case "HitErrorBar": + showHitErrorBar = Boolean.parseBoolean(value); + break; case "FixedCS": fixedCS = Float.parseFloat(value); break; @@ -1145,6 +1163,8 @@ public class Options { writer.newLine(); writer.write(String.format("PerfectHit = %b", showPerfectHit)); writer.newLine(); + writer.write(String.format("HitErrorBar = %b", showHitErrorBar)); + writer.newLine(); writer.write(String.format(Locale.US, "FixedCS = %.1f", fixedCS)); writer.newLine(); writer.write(String.format(Locale.US, "FixedHP = %.1f", fixedHP)); diff --git a/src/itdelatrisu/opsu/OsuFile.java b/src/itdelatrisu/opsu/OsuFile.java index 11a8c03d..d61c5156 100644 --- a/src/itdelatrisu/opsu/OsuFile.java +++ b/src/itdelatrisu/opsu/OsuFile.java @@ -277,7 +277,7 @@ public class OsuFile implements Comparable { else swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight()); } else { - //fill image to screen while keeping aspect ratio + // fill screen while maintaining aspect ratio if (bgImage.getWidth() / (float) bgImage.getHeight() > width / (float) height) // x > y swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight()); else diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 3d25faba..39270d73 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -321,6 +321,22 @@ public class Utils { return val; } + /** + * Clamps a value between a lower and upper bound. + * @param val the value to clamp + * @param low the lower bound + * @param high the upper bound + * @return the clamped value + * @author fluddokt + */ + public static float clamp(float val, int low, int high) { + if (val < low) + return low; + if (val > high) + return high; + return val; + } + /** * Draws the cursor. */ @@ -875,20 +891,4 @@ public class Utils { list.add(str); return list; } - - /** - * Clamps the value to be in between low and high - * @param val - * @param low - * @param high - * @return val clamped between low and high - * @author fluddokt (https://github.com/fluddokt) - */ - public static float clamp(float val, int low, int high) { - if(val < low) - return low; - if(val > high) - return high; - return val; - } } \ No newline at end of file diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java index d9bf1dcf..1922c47a 100644 --- a/src/itdelatrisu/opsu/objects/Circle.java +++ b/src/itdelatrisu/opsu/objects/Circle.java @@ -96,7 +96,7 @@ public class Circle implements HitObject { Utils.COLOR_WHITE_FADE.a = alpha; Utils.drawCentered(GameImage.HITCIRCLE.getImage(), x, y, color); Utils.drawCentered(GameImage.HITCIRCLE_OVERLAY.getImage(), x, y, Utils.COLOR_WHITE_FADE); - + color.a = oldAlpha; Utils.COLOR_WHITE_FADE.a = 1f; data.drawSymbolNumber(hitObject.getComboNumber(), x, y, @@ -135,7 +135,7 @@ public class Circle implements HitObject { int trackPosition = MusicController.getPosition(); int timeDiff = trackPosition - hitObject.getTime(); int result = hitResult(timeDiff); - + if (result > -1) { data.addHitError(hitObject.getTime(), x, y, timeDiff); data.hitResult( diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index f22822dd..2903b438 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -141,23 +141,21 @@ public class Slider implements HitObject { this.curve = new LinearBezier(hitObject, color); } + @SuppressWarnings("deprecation") @Override public void draw(int trackPosition, boolean currentObject, Graphics g) { int timeDiff = hitObject.getTime() - trackPosition; - + float scale = timeDiff / (float) game.getApproachTime(); + float approachScale = 1 + scale * 3; + float x = hitObject.getX(), y = hitObject.getY(); + float oldAlpha = color.a; float oldAlphaFade = Utils.COLOR_WHITE_FADE.a; - - float x = hitObject.getX(), y = hitObject.getY(); - float scale = timeDiff / (float)game.getApproachTime(); - - float approachScale = 1 + scale*3 ; - float alpha = (1 - scale);//(approachScale > 3.3f) ? 0f : 1f - (approachScale - 1f) / 2.7f; - color.a = Utils.clamp(alpha* 0.5f, 0, 1); - + float alpha = (1 - scale); + color.a = Utils.clamp(alpha * 0.5f, 0, 1); alpha = Utils.clamp(alpha, 0, 1); Utils.COLOR_WHITE_FADE.a = alpha; - + // curve curve.draw(); @@ -169,11 +167,10 @@ public class Slider implements HitObject { tick.drawCentered(c[0], c[1]); } } - - color.a = alpha; Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage(); Image hitCircle = GameImage.HITCIRCLE.getImage(); + color.a = alpha; // end circle float[] endPos = curve.pointAt(1); @@ -216,28 +213,27 @@ public class Slider implements HitObject { if (timeDiff >= 0) { // approach circle color.a = 1 - scale; - Utils.drawCentered(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale), x, y, color); } else { float[] c = curve.pointAt(getT(trackPosition, false)); float[] c2 = curve.pointAt(getT(trackPosition, false) + 0.01f); // slider ball - sliderBall.updateNoDraw();//TODO ..... I can't thing of anything else - //It might be better to make your own animation class or something - //also it might be better to update the animation based on the distance traveled + // TODO: deprecated method + // TODO 2: update the animation based on the distance traveled? + sliderBall.updateNoDraw(); Image sliderBallFrame = sliderBall.getCurrentFrame(); - float angle = (float) (Math.atan2(c2[1] - c[1], c2[0] - c[0]) * 180 / Math.PI) - + (currentRepeats % 2 == 1 ? 180 : 0); + float angle = (float) (Math.atan2(c2[1] - c[1], c2[0] - c[0]) * 180 / Math.PI); + if (currentRepeats % 2 == 1) + angle += 180; sliderBallFrame.setRotation(angle); sliderBallFrame.drawCentered(c[0], c[1]); + // follow circle if (followCircleActive) GameImage.SLIDER_FOLLOWCIRCLE.getImage().drawCentered(c[0], c[1]); } } - - /** * Calculates the slider hit result. diff --git a/src/itdelatrisu/opsu/states/OptionsMenu.java b/src/itdelatrisu/opsu/states/OptionsMenu.java index 40dfccc1..11fae815 100644 --- a/src/itdelatrisu/opsu/states/OptionsMenu.java +++ b/src/itdelatrisu/opsu/states/OptionsMenu.java @@ -83,7 +83,8 @@ public class OptionsMenu extends BasicGameState { GameOption.FIXED_HP, GameOption.FIXED_AR, GameOption.FIXED_OD, - GameOption.CHECKPOINT + GameOption.CHECKPOINT, + GameOption.SHOW_HIT_ERROR_BAR }); /** Total number of tabs. */ diff --git a/src/itdelatrisu/opsu/states/Splash.java b/src/itdelatrisu/opsu/states/Splash.java index 70c48897..e85cd627 100644 --- a/src/itdelatrisu/opsu/states/Splash.java +++ b/src/itdelatrisu/opsu/states/Splash.java @@ -126,7 +126,7 @@ public class Splash extends BasicGameState { // initialize song list if (OsuGroupList.get().size() > 0) { OsuGroupList.get().init(); - if (Options.isThemSongEnabled()) + if (Options.isThemeSongEnabled()) MusicController.playThemeSong(); else ((SongMenu) game.getState(Opsu.STATE_SONGMENU)).setFocus(OsuGroupList.get().getRandomNode(), -1, true);