diff --git a/.gitignore b/.gitignore index 172616ad..1773d9fe 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,10 @@ .classpath .project +# IntelliJ +.idea/ +*.iml +*.iws + Thumbs.db /target diff --git a/res/lighting.png b/res/lighting.png index 85e22fdc..01d9794c 100644 Binary files a/res/lighting.png and b/res/lighting.png differ diff --git a/res/lighting1.png b/res/lighting1.png deleted file mode 100644 index e83fdad6..00000000 Binary files a/res/lighting1.png and /dev/null differ diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index 254c25ac..210b7188 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -23,6 +23,8 @@ import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.downloads.Updater; +import itdelatrisu.opsu.objects.HitResultType; +import itdelatrisu.opsu.objects.curves.Curve; import itdelatrisu.opsu.replay.Replay; import itdelatrisu.opsu.replay.ReplayFrame; @@ -44,15 +46,33 @@ public class GameData { /** Delta multiplier for steady HP drain. */ public static final float HP_DRAIN_MULTIPLIER = 1 / 200f; + /** Time, in milliseconds, for a hit result to remain existent. */ + public static final int HITRESULT_TIME = 833; + /** Time, in milliseconds, for a hit result to fade. */ public static final int HITRESULT_FADE_TIME = 500; + /** Time, in milliseconds, for a hit circle to fade. */ + public static final int HITCIRCLE_FADE_TIME = 300; + /** Duration, in milliseconds, of a combo pop effect. */ private static final int COMBO_POP_TIME = 250; /** Time, in milliseconds, for a hit error tick to fade. */ private static final int HIT_ERROR_FADE_TIME = 5000; + /** Size of a hit circle at the end of the hit animation. */ + private static final float HITCIRCLE_ANIM_SCALE = 1.38f; + + /** Size of the hit result text at the end of its animation. */ + private static final float HITCIRCLE_TEXT_ANIM_SCALE = 1.28f; + + /** Time, in milliseconds, for the hit result text to bounce. */ + private static final int HITCIRCLE_TEXT_BOUNCE_TIME = 100; + + /** Time, in milliseconds, for the hit result text to fade. */ + private static final int HITCIRCLE_TEXT_FADE_TIME = 833; + /** Letter grades. */ public enum Grade { NULL (null, null), @@ -221,12 +241,15 @@ public class GameData { /** Combo color. */ public Color color; - /** Whether the hit object was a spinner. */ - public boolean isSpinner; + /** The type of the hit object. */ + public HitResultType hitResultType; /** Alpha level (for fading out). */ public float alpha = 1f; + /** Slider curve. */ + public Curve curve; + /** * Constructor. * @param time the result's starting track position @@ -234,15 +257,34 @@ public class GameData { * @param x the center x coordinate * @param y the center y coordinate * @param color the color of the hit object - * @param isSpinner whether the hit object was a spinner + * @param hitResultType the type of the hit object */ - public OsuHitObjectResult(int time, int result, float x, float y, Color color, boolean isSpinner) { + public OsuHitObjectResult(int time, int result, float x, float y, Color color, HitResultType hitResultType) { this.time = time; this.result = result; this.x = x; this.y = y; this.color = color; - this.isSpinner = isSpinner; + this.hitResultType = hitResultType; + } + + /** + * Constructor. + * @param time the result's starting track position + * @param result the hit result (HIT_* constants) + * @param x the center x coordinate + * @param y the center y coordinate + * @param color the color of the hit object + * @param curve the slider curve + */ + public OsuHitObjectResult(int time, int result, float x, float y, Color color, HitResultType hitResultType, Curve curve) { + this.time = time; + this.result = result; + this.x = x; + this.y = y; + this.color = color; + this.hitResultType = hitResultType; + this.curve = curve; } } @@ -822,14 +864,9 @@ public class GameData { Iterator iter = hitResultList.iterator(); while (iter.hasNext()) { OsuHitObjectResult hitResult = iter.next(); - if (hitResult.time + HITRESULT_FADE_TIME > trackPosition) { - // hit result - hitResults[hitResult.result].setAlpha(hitResult.alpha); - hitResults[hitResult.result].drawCentered(hitResult.x, hitResult.y); - hitResults[hitResult.result].setAlpha(1f); - + if (hitResult.time + HITRESULT_TIME > trackPosition) { // spinner - if (hitResult.isSpinner && hitResult.result != HIT_MISS) { + if (hitResult.hitResultType == HitResultType.SPINNER && hitResult.result != HIT_MISS) { Image spinnerOsu = GameImage.SPINNER_OSU.getImage(); spinnerOsu.setAlpha(hitResult.alpha); spinnerOsu.drawCentered(width / 2, height / 4); @@ -839,16 +876,71 @@ public class GameData { // hit lighting else if (Options.isHitLightingEnabled() && hitResult.result != HIT_MISS && hitResult.result != HIT_SLIDER30 && hitResult.result != HIT_SLIDER10) { - float scale = 1f + ((trackPosition - hitResult.time) / (float) HITRESULT_FADE_TIME); - Image scaledLighting = GameImage.LIGHTING.getImage().getScaledCopy(scale); - Image scaledLighting1 = GameImage.LIGHTING1.getImage().getScaledCopy(scale); - scaledLighting.setAlpha(hitResult.alpha); - scaledLighting1.setAlpha(hitResult.alpha); + // soon add particle system to reflect original game + Image lighting = GameImage.LIGHTING.getImage(); + lighting.setAlpha(hitResult.alpha); + lighting.drawCentered(hitResult.x, hitResult.y, hitResult.color); + } - scaledLighting.draw(hitResult.x - (scaledLighting.getWidth() / 2f), - hitResult.y - (scaledLighting.getHeight() / 2f), hitResult.color); - scaledLighting1.draw(hitResult.x - (scaledLighting1.getWidth() / 2f), - hitResult.y - (scaledLighting1.getHeight() / 2f), hitResult.color); + // hit animation + if (hitResult.hitResultType == HitResultType.CIRCLE + || hitResult.hitResultType == HitResultType.SLIDEREND + || hitResult.hitResultType == HitResultType.SLIDEREND_FIRSTOBJECT) { + float scale = Utils.easeOut( + Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME), + 1f, + HITCIRCLE_ANIM_SCALE-1f, + HITCIRCLE_FADE_TIME + ); + + float alpha = Utils.easeOut( + Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME), + 1f, + -1f, + HITCIRCLE_FADE_TIME + ); + + if (hitResult.curve != null) { + float oldAlpha = Utils.COLOR_WHITE_FADE.a; + + Curve curve = hitResult.curve; + Utils.COLOR_WHITE_FADE.a = alpha; + curve.color.a = alpha; + curve.draw(); + + Utils.COLOR_WHITE_FADE.a = oldAlpha; + } + + Image scaledHitCircle = GameImage.HITCIRCLE.getImage().getScaledCopy(scale); + scaledHitCircle.setAlpha(alpha); + Image scaledHitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(scale); + scaledHitCircleOverlay.setAlpha(alpha); + + scaledHitCircle.drawCentered(hitResult.x, hitResult.y, hitResult.color); + scaledHitCircleOverlay.drawCentered(hitResult.x, hitResult.y); + } + + // hit result + if (hitResult.hitResultType == HitResultType.CIRCLE + || hitResult.hitResultType == HitResultType.SLIDEREND + || hitResult.hitResultType == HitResultType.SPINNER) { + float scale = Utils.easeBounce( + Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_TEXT_BOUNCE_TIME), + 1f, + HITCIRCLE_TEXT_ANIM_SCALE - 1f, + HITCIRCLE_TEXT_BOUNCE_TIME + ); + + float alpha = Utils.easeOut( + Utils.clamp((trackPosition - hitResult.time) - HITCIRCLE_FADE_TIME, 0, HITCIRCLE_TEXT_FADE_TIME), + 1f, + -1f, + HITCIRCLE_TEXT_FADE_TIME + ); + + Image scaledHitResult = hitResults[hitResult.result].getScaledCopy(scale); + scaledHitResult.setAlpha(alpha); + scaledHitResult.drawCentered(hitResult.x, hitResult.y); } hitResult.alpha = 1 - ((float) (trackPosition - hitResult.time) / HITRESULT_FADE_TIME); @@ -1115,7 +1207,7 @@ public class GameData { if (!Options.isPerfectHitBurstEnabled()) ; // hide perfect hit results else - hitResultList.add(new OsuHitObjectResult(time, result, x, y, null, false)); + hitResultList.add(new OsuHitObjectResult(time, result, x, y, null, HitResultType.SLIDERTICK)); } } @@ -1129,14 +1221,14 @@ public class GameData { * @param end true if this is the last hit object in the combo * @param hitObject the hit object * @param repeat the current repeat number (for sliders, or 0 otherwise) + * @param hitResultType the type of hit object for the result + * @return the hit result (HIT_* constants) */ - public void hitResult(int time, int result, float x, float y, Color color, - boolean end, OsuHitObject hitObject, int repeat) { + private int hitRes(int time, int result, float x, float y, Color color, + boolean end, OsuHitObject hitObject, int repeat, HitResultType hitResultType) { int hitValue = 0; - boolean perfectHit = false; switch (result) { case HIT_300: - perfectHit = true; hitValue = 300; changeHealth(5f); break; @@ -1156,7 +1248,7 @@ public class GameData { resetComboStreak(); break; default: - return; + return HIT_MISS; } if (hitValue > 0) { SoundController.playHitSound( @@ -1197,12 +1289,58 @@ public class GameData { comboEnd = 0; } - if (perfectHit && !Options.isPerfectHitBurstEnabled()) + return result; + } + + /** + * 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 repeat the current repeat number (for sliders, or 0 otherwise) + * @param hitResultType the type of hit object for the result + */ + public void hitResult(int time, int result, float x, float y, Color color, + boolean end, OsuHitObject hitObject, int repeat, HitResultType hitResultType) { + result = hitRes(time, result, x, y, color, end, hitObject, repeat, hitResultType); + + if ((result == HIT_300 || result == HIT_300G || result == HIT_300K) + && !Options.isPerfectHitBurstEnabled()) ; // hide perfect hit results else if (result == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive())) ; // "relax" and "autopilot" mods: hide misses else - hitResultList.add(new OsuHitObjectResult(time, result, x, y, color, hitObject.isSpinner())); + hitResultList.add(new OsuHitObjectResult(time, result, x, y, color, hitResultType)); + } + + /** + * Handles a slider 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 repeat the current repeat number (for sliders, or 0 otherwise) + * @param hitResultType the type of hit object for the result + * @param curve the slider curve + */ + public void hitResult(int time, int result, float x, float y, Color color, + boolean end, OsuHitObject hitObject, int repeat, HitResultType hitResultType, Curve curve) { + result = hitRes(time, result, x, y, color, end, hitObject, repeat, hitResultType); + + if ((result == HIT_300 || result == HIT_300G || result == HIT_300K) + && !Options.isPerfectHitBurstEnabled()) + ; // hide perfect hit results + else if (result == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive())) + ; // "relax" and "autopilot" mods: hide misses + else + hitResultList.add(new OsuHitObjectResult(time, result, x, y, color, hitResultType, curve)); } /** diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index 5f7657a1..ae4cba70 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -202,7 +202,6 @@ public enum GameImage { SCORE_PERCENT ("score-percent", "png"), SCORE_X ("score-x", "png"), LIGHTING ("lighting", "png"), - LIGHTING1 ("lighting1", "png"), // Game Mods MOD_EASY ("selection-mod-easy", "png", false, false), diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 6c13aec9..db200ee4 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -607,4 +607,30 @@ public class Utils { else return String.format("%02d:%02d:%02d", seconds / 3600, (seconds / 60) % 60, seconds % 60); } + + /** + * Cubic ease out function + * @param t the current time + * @param a the starting position + * @param b the finishing position + * @param d the duration + * @return the eased float + */ + public static float easeOut(float t, float a, float b, float d) { + return b * ((t = t / d - 1f) * t * t + 1f) + a; + } + + /** + * Fake bounce ease function + * @param t the current time + * @param a the starting position + * @param b the finishing position + * @param d the duration + * @return the eased float + */ + public static float easeBounce(float t, float a, float b, float d) { + if (t < d / 2) + return easeOut(t, a, b, d); + return easeOut(d-t, a, b, d); + } } diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java index 6380078c..c8ee6c1b 100644 --- a/src/itdelatrisu/opsu/objects/Circle.java +++ b/src/itdelatrisu/opsu/objects/Circle.java @@ -138,7 +138,7 @@ public class Circle implements HitObject { if (result > -1) { data.addHitError(hitObject.getTime(), x, y, timeDiff); - data.hitResult(hitObject.getTime(), result, this.x, this.y, color, comboEnd, hitObject, 0); + data.hitResult(hitObject.getTime(), result, this.x, this.y, color, comboEnd, hitObject, 0, HitResultType.CIRCLE); return true; } } @@ -154,17 +154,17 @@ public class Circle implements HitObject { if (overlap || 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, 0); + data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, 0, HitResultType.CIRCLE); else // no more points can be scored, so send a miss - data.hitResult(time, GameData.HIT_MISS, x, y, null, comboEnd, hitObject, 0); + data.hitResult(time, GameData.HIT_MISS, x, y, null, comboEnd, hitObject, 0, HitResultType.CIRCLE); 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, 0); + data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, 0, HitResultType.CIRCLE); return true; } } diff --git a/src/itdelatrisu/opsu/objects/HitResultType.java b/src/itdelatrisu/opsu/objects/HitResultType.java new file mode 100644 index 00000000..0f7f4311 --- /dev/null +++ b/src/itdelatrisu/opsu/objects/HitResultType.java @@ -0,0 +1,10 @@ +package itdelatrisu.opsu.objects; + +public enum HitResultType { + CIRCLE, + //SLIDERSTART, + SLIDERTICK, + SLIDEREND, + SLIDEREND_FIRSTOBJECT, + SPINNER +} diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 646e8c12..2dbd40b3 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -278,14 +278,13 @@ public class Slider implements HitObject { else result = GameData.HIT_MISS; - if (currentRepeats % 2 == 0) { // last circle - float[] lastPos = curve.pointAt(1); - data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result, - lastPos[0], lastPos[1], color, comboEnd, hitObject, currentRepeats + 1); - } else { // first circle - data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result, - x, y, color, comboEnd, hitObject, currentRepeats + 1); - } + float[] lastPos = curve.pointAt(1); + data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result, + x, y, color, comboEnd, hitObject, currentRepeats + 1, + currentRepeats % 2 == 0 ? HitResultType.SLIDEREND_FIRSTOBJECT : HitResultType.SLIDEREND, curve); + data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result, + lastPos[0], lastPos[1], color, comboEnd, hitObject, currentRepeats + 1, + currentRepeats % 2 == 0 ? HitResultType.SLIDEREND : HitResultType.SLIDEREND_FIRSTOBJECT); return result; } diff --git a/src/itdelatrisu/opsu/objects/Spinner.java b/src/itdelatrisu/opsu/objects/Spinner.java index 1200418a..1cc6e290 100644 --- a/src/itdelatrisu/opsu/objects/Spinner.java +++ b/src/itdelatrisu/opsu/objects/Spinner.java @@ -191,7 +191,7 @@ public class Spinner implements HitObject { result = GameData.HIT_MISS; data.hitResult(hitObject.getEndTime(), result, width / 2, height / 2, - Color.transparent, true, hitObject, 0); + Color.transparent, true, hitObject, 0, HitResultType.SPINNER); return result; } diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java index 028923be..c09de69f 100644 --- a/src/itdelatrisu/opsu/objects/curves/Curve.java +++ b/src/itdelatrisu/opsu/objects/curves/Curve.java @@ -28,12 +28,12 @@ import org.newdawn.slick.Color; * @author fluddokt (https://github.com/fluddokt) */ public abstract class Curve { + /** The color of this curve. */ + public Color color; + /** The associated OsuHitObject. */ protected OsuHitObject hitObject; - /** The color of this curve. */ - protected Color color; - /** The scaled starting x, y coordinates. */ protected float x, y;