diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index 1dace8b6..e3c1f3f7 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -26,7 +26,6 @@ import itdelatrisu.opsu.beatmap.Beatmap; import itdelatrisu.opsu.beatmap.HitObject; import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.objects.curves.Curve; -import itdelatrisu.opsu.objects.curves.Vec2f; import itdelatrisu.opsu.replay.Replay; import itdelatrisu.opsu.replay.ReplayFrame; import itdelatrisu.opsu.ui.Colors; @@ -151,7 +150,8 @@ public class GameData { HIT_SLIDER10 = 7, HIT_SLIDER30 = 8, HIT_MAX = 9, // not a hit result - HIT_SLIDER_REPEAT = 10; // 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). */ private Image[] hitResults; @@ -232,7 +232,7 @@ public class GameData { private LinkedBlockingDeque hitErrorList; /** Hit object types, used for drawing results. */ - public enum HitObjectType { CIRCLE, SLIDERTICK, SLIDER_FIRST, SLIDER_LAST, SPINNER } + public enum HitObjectType { CIRCLE, SLIDERTICK, SLIDER_FIRST, SLIDER_CURVE, SLIDER_LAST, SPINNER } /** Hit result helper class. */ private class HitObjectResult { @@ -892,50 +892,9 @@ public class GameData { lighting.drawCentered(hitResult.x, hitResult.y, hitResult.color); } - // hit animation - if (hitResult.result != HIT_MISS && ( - 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; - hitResult.curve.draw(hitResult.color); - Colors.WHITE_FADE.a = oldWhiteAlpha; - hitResult.color.a = oldColorAlpha; - } - - // 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 @@ -963,6 +922,66 @@ public class GameData { } } + /** + * Draw the hit animations: circles, reversearrows, slider curves fading out and/or expanding + * @param hitResult the hitresult which holds information about the kind of animation to draw + * @param trackPosition the current track position (in ms) + */ + private void drawHitAnimations(HitObjectResult hitResult, int trackPosition) { + if (hitResult.hitResultType == HitObjectType.SLIDER_CURVE && 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; + + // slider curve + float oldWhiteAlpha = Colors.WHITE_FADE.a; + float oldColorAlpha = hitResult.color.a; + Colors.WHITE_FADE.a = alpha; + hitResult.color.a = alpha; + hitResult.curve.draw(hitResult.color); + Colors.WHITE_FADE.a = oldWhiteAlpha; + hitResult.color.a = oldColorAlpha; + return; + } + + if (hitResult.result == HIT_MISS) { + return; + } + + if (hitResult.hitResultType != HitObjectType.CIRCLE + && hitResult.hitResultType != HitObjectType.SLIDER_FIRST + && hitResult.hitResultType != HitObjectType.SLIDER_LAST) { + return; + } + + // 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); + } + // 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); + } + /** * Changes health by a given percentage, modified by drainRate. * @param percent the health percentage @@ -1195,6 +1214,14 @@ public class GameData { hitResultList.add(new HitObjectResult(time, HIT_SLIDER_REPEAT, x, y, color, type, curve, true, true)); } + public void sendSliderCurveResult(int time, Color color, Curve curve) { + hitResultList.add(new HitObjectResult(time, HIT_SLIDER_REPEAT, 0f, 0f, color, HitObjectType.SLIDER_CURVE, curve, true, true)); + } + + public void sendAnimationResult(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 @@ -1396,14 +1423,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)); - } } /** diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 3830726f..2258204c 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -180,6 +180,7 @@ public class Slider implements GameObject { @Override public void draw(Graphics g, int trackPosition) { int timeDiff = hitObject.getTime() - trackPosition; + final int repeats = hitObject.getRepeatCount(); final int approachTime = game.getApproachTime(); final int fadeInTime = game.getFadeInTime(); float scale = timeDiff / (float) approachTime; @@ -205,15 +206,38 @@ public class Slider implements GameObject { float curveInterval = Options.isSliderSnaking() ? alpha : 1f; curve.draw(color,curveInterval); - // end circle - Vec2f endCircPos = curve.pointAt(curveInterval); - hitCircle.drawCentered(endCircPos.x, endCircPos.y, color); - hitCircleOverlay.drawCentered(endCircPos.x, endCircPos.y, Colors.WHITE_FADE); + // end circle, only draw if ball still has to go there + if (curveInterval == 1f && currentRepeats < repeats - (repeats % 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); + } - // start circle - hitCircle.drawCentered(x, y, color); - if (!overlayAboveNumber) - hitCircleOverlay.drawCentered(x, 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, only draw if ball still has to go there + if (!sliderClickedInitial || currentRepeats < repeats - (repeats % 2 == 1 ? 1 : 0)) { + hitCircle.drawCentered(x, y, firstCircleColor); + if (!overlayAboveNumber || sliderClickedInitial) + hitCircleOverlay.drawCentered(x, y, startCircleOverlayColor); + } color.a = alpha; @@ -231,44 +255,42 @@ public class Slider implements GameObject { alpha = Math.min(alpha, hiddenAlpha); } } - if (sliderClickedInitial) - ; // don't draw current combo number if already clicked - else - data.drawSymbolNumber(hitObject.getComboNumber(), x, y, - hitCircle.getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha); - if (overlayAboveNumber) { - oldWhiteFadeAlpha = Colors.WHITE_FADE.a; - Colors.WHITE_FADE.a = sliderAlpha; - hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE); - Colors.WHITE_FADE.a = oldWhiteFadeAlpha; + // draw combonumber 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) { + startCircleOverlayColor.a = sliderAlpha; + hitCircleOverlay.drawCentered(x, y, startCircleOverlayColor); + } } // repeats if (curveInterval == 1.0f) { - for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) { - if (hitObject.getRepeatCount() - 1 > tcurRepeat) { - Image arrow = GameImage.REVERSEARROW.getImage(); - // bouncing animation - //arrow = arrow.getScaledCopy((float) (1 + 0.2d * ((trackPosition + sliderTime * tcurRepeat) % 292) / 292)); - float colorLuminance = 0.299f*color.r + 0.587f*color.g + 0.114f*color.b; - Color arrowColor = colorLuminance < 0.8f ? Color.white : Color.black; - 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 < repeats - 1; tcurRepeat++) { + Image arrow = GameImage.REVERSEARROW.getImage(); + // bouncing animation + //arrow = arrow.getScaledCopy((float) (1 + 0.2d * ((trackPosition + sliderTime * tcurRepeat) % 292) / 292)); + float colorLuminance = 0.299f*color.r + 0.587f*color.g + 0.114f*color.b; + Color arrowColor = colorLuminance < 0.8f ? Color.white : Color.black; + 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, arrowColor); + } else { + // first circle + arrow.setRotation(curve.getStartAngle()); + arrow.drawCentered(x, y, arrowColor); } } } @@ -363,6 +385,24 @@ public class Slider implements GameObject { } } + /** + * 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) @@ -441,7 +481,7 @@ public class Slider implements GameObject { } data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result, cx, cy, color, comboEnd, hitObject, type, sliderHeldToEnd, - currentRepeats + 1, curve, sliderHeldToEnd); + currentRepeats + 1, null, sliderHeldToEnd); return result; } @@ -460,8 +500,11 @@ public class Slider implements GameObject { if (timeDiff < hitResultOffset[GameData.HIT_50]) { result = GameData.HIT_SLIDER30; ticksHit++; - } else if (timeDiff < hitResultOffset[GameData.HIT_MISS]) + data.sendAnimationResult(trackPosition, this.x, this.y, color, true); + } else if (timeDiff < hitResultOffset[GameData.HIT_MISS]) { result = GameData.HIT_MISS; + data.sendAnimationResult(trackPosition, this.x, this.y, color, false); + } //else not a hit if (result > -1) { @@ -489,8 +532,11 @@ public class Slider implements GameObject { if (isAutoMod) { // "auto" mod: catch any missed notes due to lag ticksHit++; data.sliderTickResult(time, GameData.HIT_SLIDER30, x, y, hitObject, currentRepeats); - } else + data.sendAnimationResult(time, x, y, color, true); + } else { data.sliderTickResult(time, GameData.HIT_MISS, x, y, hitObject, currentRepeats); + data.sendAnimationResult(trackPosition, x, y, color, false); + } } // "auto" mod: send a perfect hit result @@ -499,6 +545,7 @@ public class Slider implements GameObject { ticksHit++; sliderClickedInitial = true; data.sliderTickResult(time, GameData.HIT_SLIDER30, x, y, hitObject, currentRepeats); + data.sendAnimationResult(time, x, y, color, true); } } @@ -530,6 +577,9 @@ public class Slider implements GameObject { // calculate and send slider result hitResult(); + + // send 'curve fade out' hit result + data.sendSliderCurveResult(getEndTime(), color, curve); return true; } @@ -549,23 +599,6 @@ public class Slider implements GameObject { tickIndex = 0; isNewRepeat = true; tickExpandTime = TICK_EXPAND_TIME; - - // 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; - } - float colorLuminance = 0.299f*color.r + 0.587f*color.g + 0.114f*color.b; - Color arrowColor = colorLuminance < 0.8f ? Color.white : Color.black; - data.sendRepeatSliderResult(trackPosition, posX, posY, arrowColor, curve, type); } } @@ -593,13 +626,26 @@ public class Slider implements GameObject { // held during new repeat if (isNewRepeat) { ticksHit++; + + HitObjectType type; + float posX, posY; 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); + 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.sliderTickResult(trackPosition, GameData.HIT_SLIDER30, + posX, posY, hitObject, currentRepeats); + + // send hit result, to fade out reversearrow + float colorLuminance = 0.299f*color.r + 0.587f*color.g + 0.114f*color.b; + Color arrowColor = colorLuminance < 0.8f ? Color.white : Color.black; + data.sendRepeatSliderResult(trackPosition, posX, posY, arrowColor, curve, type); } // held during new tick