/* * opsu! - an open-source osu! client * Copyright (C) 2014, 2015 Jeffrey Han * * opsu! is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * opsu! is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with opsu!. If not, see . */ package itdelatrisu.opsu.objects; import java.util.Iterator; import java.util.LinkedList; import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameMod; import itdelatrisu.opsu.GameData; import itdelatrisu.opsu.OsuFile; import itdelatrisu.opsu.OsuHitObject; import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.states.Game; import org.newdawn.slick.Animation; import org.newdawn.slick.Color; import org.newdawn.slick.GameContainer; import org.newdawn.slick.Graphics; import org.newdawn.slick.Image; /** * Data type representing a slider object. */ public class Slider implements HitObject { /** Slider ball animation. */ private static Animation sliderBall; /** Slider movement speed multiplier. */ private static float sliderMultiplier = 1.0f; /** Rate at which slider ticks are placed. */ private static float sliderTickRate = 1.0f; /** The associated OsuHitObject. */ private OsuHitObject hitObject; /** The associated Game object. */ private Game game; /** The associated GameData object. */ private GameData data; /** The color of this slider. */ private Color color; /** The underlying Bezier object. */ private Curve bezier; /** The time duration of the slider, in milliseconds. */ private float sliderTime = 0f; /** The time duration of the slider including repeats, in milliseconds. */ private float sliderTimeTotal = 0f; /** Whether or not the result of the initial hit circle has been processed. */ private boolean sliderClicked = false; /** Whether or not to show the follow circle. */ private boolean followCircleActive = false; /** Whether or not the slider result ends the combo streak. */ private boolean comboEnd; /** The number of repeats that have passed so far. */ private int currentRepeats = 0; /** The t values of the slider ticks. */ private float[] ticksT; /** The tick index in the ticksT[] array. */ private int tickIndex = 0; /** Number of ticks hit and tick intervals so far. */ private int ticksHit = 0, tickIntervals = 1; private abstract class Curve{ /** * Returns the point on the curve at a value t. * @param t the t value [0, 1] * @return the point [x, y] */ public abstract float[] pointAt(float t); /** * Draws the full Bezier curve to the graphics context. */ public abstract void draw(); /** * Returns the angle of the first control point. */ public abstract float getEndAngle(); /** * Returns the angle of the last control point. */ public abstract float getStartAngle(); } /** * A two dimensional vector */ private class Vec2f{ float x, y; /** * Constructor of the (nx, ny) Vector * @param nx * @param ny */ public Vec2f(float nx, float ny) { x=nx; y=ny; } /** * Constructor of the (0,0) Vector */ public Vec2f() { } /** * Finds the midpoint between this Vector and "o" Vector * @param o the other Vector * @return midpoint vector */ public Vec2f midPoint(Vec2f o){ return new Vec2f((x+o.x)/2, (y+o.y)/2); } /** * Subtracts the "o" vector from this vector * @param o the other Vector * @return itself for chaining */ public Vec2f sub(Vec2f o){ x-=o.x; y-=o.y; return this; } /** * Sets this Vector to the normal of this Vector * @return itself for chaining */ public Vec2f nor(){ float nx = -y, ny =x; x=nx; y=ny; return this; } /** * Makes a new Vector that is a copy of this Vector * @return a copy of this Vector */ public Vec2f cpy(){ return new Vec2f(x, y); } /** * Adds nx to the x component and ny to the y component of this Vector * @param nx * @param ny * @return */ public Vec2f add(float nx, float ny) { x+=nx; y+=ny; return this; } /** * Finds the length of this Vector * @return the length of this Vector */ public float len() { return (float) Math.sqrt(x*x + y*y); } /** * Compares this vector to another Vector * @param o the Other Vector * @return true if the two Vector are numerically equal */ public boolean equals(Vec2f o){ return x==o.x && y==o.y; } } /** * Representation of a curve along a Circumscribed Circle of three points. * http://en.wikipedia.org/wiki/Circumscribed_circle */ private class CircumscribedCircle extends Curve{ /** The center of the Circumscribed Circle */ Vec2f circleCenter; /** The radius of the Circumscribed Circle */ float radius; /** * The three points to create the Circumscribed Circle from */ Vec2f start ,mid ,end; /** The three angles relative to the circle center */ float startAng,endAng,midAng; /** The start and end angles for drawing */ float drawStartAngle,drawEndAngle; /** Two times Pi or one full circle in radians */ final float TWO_PI = (float) (2*Math.PI); /** Pi divided by two or a quarter of a circle in radians */ final float HALF_PI = (float) (Math.PI/2); /** The number of steps in the curve to draw */ private float step; /** * Constructor */ public CircumscribedCircle(){ this.step = hitObject.getPixelLength() / 5; //construct the three points start = new Vec2f(getX(0), getY(0)); mid = new Vec2f(getX(1), getY(1)); end = new Vec2f(getX(2), getY(2)); //find the circle center Vec2f mida = start.midPoint(mid); Vec2f midb = end.midPoint(mid); Vec2f nora = mid.cpy().sub(start).nor(); Vec2f norb = mid.cpy().sub(end).nor(); circleCenter = intersect(mida, nora, midb, norb); //find the angles relative to the circle center Vec2f startAngPoint = start.cpy().sub(circleCenter); Vec2f midAngPoint = mid.cpy().sub(circleCenter); Vec2f endAngPoint = end.cpy().sub(circleCenter); startAng = (float) Math.atan2(startAngPoint.y, startAngPoint.x); midAng = (float) Math.atan2(midAngPoint.y, midAngPoint.x); endAng = (float) Math.atan2(endAngPoint.y, endAngPoint.x); //find angles that passes thru midAng if(!isIn(startAng,midAng,endAng)){ if(Math.abs(startAng+TWO_PI-endAng)startAng){ endAng=startAng+arcAng; }else{ endAng=startAng-arcAng; } //finds the angles to draw for repeats drawEndAngle = (float) ((endAng+(startAng>endAng?HALF_PI:-HALF_PI)) * 180 / Math.PI); drawStartAngle = (float) ((startAng+(startAng>endAng?-HALF_PI:HALF_PI)) * 180 / Math.PI); } /** * Checks to see if "b" is between "a" and "c" * @param a * @param b * @param c * @return true if b is between a and c */ private boolean isIn(float a,float b,float c){ return (b>a && bc); } /** * Finds the point of intersection between two parametric lines of A = a + ta*t and B = b + tb*u * http://gamedev.stackexchange.com/questions/44720/line-intersection-from-parametric-equation * @param a the initial position of the line A * @param ta the direction of the line A * @param b the initial position of the line B * @param tb the direction of the line B * @return the point at which the two lines interssect */ private Vec2f intersect(Vec2f a, Vec2f ta, Vec2f b, Vec2f tb) { // xy = a + ta * t = b + tb * u // t =(b + tb*u -a)/ta //t(x) == t(y) //(b.x + tb.x*u -a.x)/ta.x = (b.y + tb.y*u -a.y)/ta.y // b.x*ta.y + tb.x*u*ta.y -a.x*ta.y = b.y*ta.x + tb.y*u*ta.x -a.y*ta.x // tb.x*u*ta.y - tb.y*u*ta.x= b.y*ta.x -a.y*ta.x -b.x*ta.y +a.x*ta.y //u *(tb.x*ta.y - tb.y*ta.x) = (b.y-a.y)ta.x +(a.x-b.x)ta.y //u = ((b.y-a.y)ta.x +(a.x-b.x)ta.y) / (tb.x*ta.y - tb.y*ta.x); float des = tb.x*ta.y - tb.y*ta.x; if(Math.abs(des)<0.00001f){ throw new Error("parallel "); } float u = ((b.y-a.y)*ta.x + (a.x-b.x)*ta.y) / des; return b.cpy().add(tb.x*u,tb.y*u); } @Override public float[] pointAt(float t) { float ang = lerp(startAng, endAng, t); return new float[]{(float) (Math.cos(ang)*radius+circleCenter.x),(float) (Math.sin(ang)*radius+circleCenter.y)}; } @Override public void draw() { Image hitCircle = GameImage.HITCIRCLE.getImage(); Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage(); // draw overlay and hit circle for(int i=0; i 1) { float[] c1 = pointAt(0f); float[] c2 = pointAt(step); startAngle = (float) (Math.atan2(c2[1] - c1[1], c2[0] - c1[0]) * 180 / Math.PI); c1 = pointAt(1f); c2 = pointAt(1f - step); endAngle = (float) (Math.atan2(c2[1] - c1[1], c2[0] - c1[0]) * 180 / Math.PI); } } /** * Returns the x coordinate of the control point at index i. */ private float getX(int i) { return (i == 0) ? hitObject.getX() : hitObject.getSliderX()[i - 1]; } /** * Returns the y coordinate of the control point at index i. */ private float getY(int i) { return (i == 0) ? hitObject.getY() : hitObject.getSliderY()[i - 1]; } /** * Returns the angle of the first control point. */ private float getStartAngle() { return startAngle; } /** * Returns the angle of the last control point. */ private float getEndAngle() { return endAngle; } /** * Calculates the factorial of a number. */ private long factorial(int n) { return (n <= 1 || n > 20) ? 1 : n * factorial(n - 1); } /** * Calculates the Bernstein polynomial. * @param i the index * @param n the degree of the polynomial (i.e. number of points) * @param t the t value [0, 1] */ private double bernstein(int i, int n, float t) { return factorial(n) / (factorial(i) * factorial(n-i)) * Math.pow(t, i) * Math.pow(1-t, n-i); } /** * Returns the point on the Bezier curve at a value t. * For curves of order greater than 4, points will be generated along * a path of overlapping cubic (at most) Beziers. * @param t the t value [0, 1] * @return the point [x, y] */ public float[] pointAt(float t) { float[] c = { 0f, 0f }; int n = order - 1; if (n < 4) { // normal curve for (int i = 0; i <= n; i++) { c[0] += getX(i) * bernstein(i, n, t); c[1] += getY(i) * bernstein(i, n, t); } } else { // split curve into path // TODO: this is probably wrong... int segmentCount = (n / 3) + 1; int segment = (int) Math.floor(t * segmentCount); int startIndex = 3 * segment; int segmentOrder = Math.min(startIndex + 3, n) - startIndex; float segmentT = (t * segmentCount) - segment; for (int i = 0; i <= segmentOrder; i++) { c[0] += getX(i + startIndex) * bernstein(i, segmentOrder, segmentT); c[1] += getY(i + startIndex) * bernstein(i, segmentOrder, segmentT); } } return c; } /** * Draws the full Bezier curve to the graphics context. */ public void draw() { Image hitCircle = GameImage.HITCIRCLE.getImage(); Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage(); // draw overlay and hit circle for (int i = curveX.length - 1; i >= 0; i--) Utils.drawCentered(hitCircleOverlay, curveX[i], curveY[i], Utils.COLOR_WHITE_FADE); for (int i = curveX.length - 1; i >= 0; i--) Utils.drawCentered(hitCircle, curveX[i], curveY[i], color); } } /** * Representation of a Bezier curve with equal distant points. * http://pomax.github.io/bezierinfo/#tracing */ private class LinearBezier extends Curve{ /** The angles of the first and last control points for drawing. */ private float startAngle, endAngle; /** List of Bezier curves in the set of points */ LinkedList beziers = new LinkedList(); /** Points along the curve at equal distance. */ Vec2f[] curve; /** The number of points along the curve */ int ncurve; /** * Constructor */ public LinearBezier(){ //splits points into different beziers if has the same points(Red points) int npoints = hitObject.getSliderX().length + 1; //The number of control points LinkedList points = new LinkedList(); // a temporary list of points to separete different bezier curves Vec2f lastPoi = null; for(int i=0; i=2){ beziers.add(new Bezier2(points.toArray(new Vec2f[0]))); } points.clear(); } points.add(tpoi); lastPoi = tpoi; } if(points.size()<2){ //Ending on a red point (probably) just ignore //throw new Error("trying to continue Beziers with less than 2 points"); }else{ beziers.add(new Bezier2(points.toArray(new Vec2f[0]))); points.clear(); } //find the length of all beziers //int totalDistance = 0; //for(Bezier2 bez : beziers){ // totalDistance += bez.totalDistance(); //} //now try to creates points the are equal distance to eachother ncurve = (int) (hitObject.getPixelLength()/5f); curve = new Vec2f[ncurve+1]; float distanceAt = 0; Iterator ita = beziers.iterator(); int curPoint=0; Bezier2 curBezier=ita.next(); Vec2f lastCurve = curBezier.curve[0]; float lastDistanceAt = 0; //length of Bezier should equal pixel length (in 640x480) float pixelLength = hitObject.getPixelLength()*OsuHitObject.getMultiplier(); //For each distance, try to get in between the two points that is between it. for(int i=0;i= curBezier.ncurve){ if(ita.hasNext()){ curBezier = ita.next(); curPoint = 0; }else{ curPoint = curBezier.ncurve -1; } } } Vec2f thisCurve = curBezier.curve[curPoint]; //interpolate the point between the two closest distances if(distanceAt-lastDistanceAt > 1){ float t = (prefDistance-lastDistanceAt)/(float)(distanceAt-lastDistanceAt); curve[i] = new Vec2f( lerp(lastCurve.x,thisCurve.x,t), lerp(lastCurve.y,thisCurve.y,t)); //System.out.println("Dis "+i+" "+prefDistance+" "+lastDistanceAt+" "+distanceAt+" "+curPoint+" "+t); }else{ curve[i] = thisCurve; } } //if (hitObject.getRepeatCount() > 1) { Vec2f c1 = curve[0]; int cnt = 1; Vec2f c2 = curve[cnt++]; while(c2.cpy().sub(c1).len()<1){ c2 = curve[cnt++]; } startAngle = (float) (Math.atan2(c2.y - c1.y, c2.x - c1.x) * 180 / Math.PI); c1 = curve[ncurve-1]; cnt= ncurve-2; c2 = curve[cnt]; while(c2.cpy().sub(c1).len()<1){ c2 = curve[cnt--]; } endAngle = (float) (Math.atan2(c2.y - c1.y, c2.x - c1.x) * 180 / Math.PI); //} //System.out.println("Total Distance: "+totalDistance+" "+distanceAt+" "+beziers.size()+" "+hitObject.getPixelLength()+" "+hitObject.xMultiplier); } @Override public float[] pointAt(float t) { float index = t * ncurve; if((int)index>=ncurve){ Vec2f poi = curve[ncurve-1]; return new float[]{poi.x, poi.y}; } Vec2f poi = curve[(int)index]; float t2 = index - (int)index; Vec2f poi2 = curve[(int)index+1]; return new float[]{lerp(poi.x,poi2.x,t2),lerp(poi.y,poi2.y,t2)}; } @Override public void draw() { Image hitCircle = GameImage.HITCIRCLE.getImage(); Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage(); // draw overlay and hit circle for (int i = curve.length - 2; i >= 0; i--) Utils.drawCentered(hitCircleOverlay, curve[i].x, curve[i].y, Utils.COLOR_WHITE_FADE); for (int i = curve.length - 2; i >= 0; i--) Utils.drawCentered(hitCircle, curve[i].x, curve[i].y, color); } @Override public float getEndAngle() { return endAngle; } @Override public float getStartAngle() { return startAngle; } } /** * Representation of a Bezier curve with the distance between each point calculated. */ private class Bezier2{ /** The control points of the Bezier curve */ Vec2f[] points; /** Points along the curve of the Bezier curve */ Vec2f[] curve; /** distance between this point of the curve and the last point */ float[] curveDis; /** The number of points along the curve */ int ncurve; /** The total distances of this Bezier */ float totalDistance; /* * Constructor */ public Bezier2(Vec2f[] points) { this.points = points; //approximate by finding the length of all points(which should be the max possible length of the curve) float approxlength = 0; for(int i=0;i 20) ? 1 : n * factorial(n - 1); } /** * Calculates the Bernstein polynomial. * @param i the index * @param n the degree of the polynomial (i.e. number of points) * @param t the t value [0, 1] */ private double bernstein(int i, int n, float t) { return factorial(n) / (factorial(i) * factorial(n-i)) * Math.pow(t, i) * Math.pow(1-t, n-i); } } /** * Linear interpolation of a and b at t * @param a * @param b * @param t * @return */ private float lerp(float a, float b, float t){ return a*(1-t) + b*t; } /** * "a recursive method to evaluate polynomials in Bernstein form or Bezier curves" * http://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm */ private float deCasteljau (float[] a, int i, int order, float t){ if(order==0) return a[i]; return lerp( deCasteljau(a,i,order-1,t), deCasteljau(a,i+1,order-1,t), t); } /** * Returns the x coordinate of the control point at index i. */ private float getX(int i) { return (i == 0) ? hitObject.getX() : hitObject.getSliderX()[i - 1]; } /** * Returns the y coordinate of the control point at index i. */ private float getY(int i) { return (i == 0) ? hitObject.getY() : hitObject.getSliderY()[i - 1]; } /** * Initializes the Slider data type with images and dimensions. * @param container the game container * @param circleSize the map's circleSize value * @param osu the associated OsuFile object */ public static void init(GameContainer container, float circleSize, OsuFile osu) { int diameter = (int) (96 - (circleSize * 8)); diameter = diameter * container.getWidth() / 640; // convert from Osupixels (640x480) // slider ball Image[] sliderBallImages; if (GameImage.SLIDER_BALL.hasSkinImages() || (!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null)) sliderBallImages = GameImage.SLIDER_BALL.getImages(); else sliderBallImages = new Image[]{ GameImage.SLIDER_BALL.getImage() }; for (int i = 0; i < sliderBallImages.length; i++) sliderBallImages[i] = sliderBallImages[i].getScaledCopy(diameter * 118 / 128, diameter * 118 / 128); sliderBall = new Animation(sliderBallImages, 60); GameImage.SLIDER_FOLLOWCIRCLE.setImage(GameImage.SLIDER_FOLLOWCIRCLE.getImage().getScaledCopy(diameter * 259 / 128, diameter * 259 / 128)); GameImage.REVERSEARROW.setImage(GameImage.REVERSEARROW.getImage().getScaledCopy(diameter, diameter)); GameImage.SLIDER_TICK.setImage(GameImage.SLIDER_TICK.getImage().getScaledCopy(diameter / 4, diameter / 4)); sliderMultiplier = osu.sliderMultiplier; sliderTickRate = osu.sliderTickRate; } /** * Constructor. * @param hitObject the associated OsuHitObject * @param game the associated Game object * @param data the associated GameData object * @param color the color of this circle * @param comboEnd true if this is the last hit object in the combo */ public Slider(OsuHitObject hitObject, Game game, GameData data, Color color, boolean comboEnd) { this.hitObject = hitObject; this.game = game; this.data = data; this.color = color; this.comboEnd = comboEnd; if(hitObject.getSliderType() == 'P' && hitObject.getSliderX().length==2){ this.bezier = new CircumscribedCircle(); }else { this.bezier = new LinearBezier(); } } @Override public void draw(int trackPosition, boolean currentObject, Graphics g) { float x = hitObject.getX(), y = hitObject.getY(); float[] sliderX = hitObject.getSliderX(), sliderY = hitObject.getSliderY(); int timeDiff = hitObject.getTime() - trackPosition; float approachScale = (timeDiff >= 0) ? 1 + (timeDiff * 2f / game.getApproachTime()) : 1f; float alpha = (approachScale > 3.3f) ? 0f : 1f - (approachScale - 1f) / 2.7f; float oldAlpha = color.a; float oldAlphaFade = Utils.COLOR_WHITE_FADE.a; color.a = alpha; Utils.COLOR_WHITE_FADE.a = alpha; // bezier bezier.draw(); // ticks if (currentObject && ticksT != null) { Image tick = GameImage.SLIDER_TICK.getImage(); for (int i = 0; i < ticksT.length; i++) { float[] c = bezier.pointAt(ticksT[i]); tick.drawCentered(c[0], c[1]); } } Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage(); Image hitCircle = GameImage.HITCIRCLE.getImage(); // end circle //int lastIndex = sliderX.length - 1; float[] endPos = bezier.pointAt(1); Utils.drawCentered(hitCircle, endPos[0], endPos[1], color); Utils.drawCentered(hitCircleOverlay, endPos[0], endPos[1], Utils.COLOR_WHITE_FADE); // start circle Utils.drawCentered(hitCircleOverlay, x, y, Utils.COLOR_WHITE_FADE); Utils.drawCentered(hitCircle, x, y, color); if (sliderClicked) ; // don't draw current combo number if already clicked else data.drawSymbolNumber(hitObject.getComboNumber(), x, y, hitCircle.getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight()); color.a = oldAlpha; Utils.COLOR_WHITE_FADE.a = oldAlphaFade; // repeats for(int tcurRepeat = currentRepeats; tcurRepeat<=currentRepeats+1; tcurRepeat++){ if (hitObject.getRepeatCount() - 1 > tcurRepeat) { Image arrow = GameImage.REVERSEARROW.getImage(); if(tcurRepeat != currentRepeats){ float t = getT(trackPosition, true); arrow.setAlpha((float) (t-Math.floor(t))); }else{ arrow.setAlpha(1f); } if (tcurRepeat % 2 == 0) { // last circle arrow.setRotation(bezier.getEndAngle()); arrow.drawCentered(endPos[0], endPos[1]); } else { // first circle arrow.setRotation(bezier.getStartAngle()); arrow.drawCentered(x, y); } } } if (timeDiff >= 0) { // approach circle Utils.drawCentered(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale), x, y, color); } else { float[] c = bezier.pointAt(getT(trackPosition, false)); // slider ball Utils.drawCentered(sliderBall, c[0], c[1]); // follow circle if (followCircleActive) GameImage.SLIDER_FOLLOWCIRCLE.getImage().drawCentered(c[0], c[1]); } } /** * Calculates the slider hit result. * @return the hit result (GameData.HIT_* constants) */ private int hitResult() { int lastIndex = hitObject.getSliderX().length - 1; float tickRatio = (float) ticksHit / tickIntervals; int result; if (tickRatio >= 1.0f) result = GameData.HIT_300; else if (tickRatio >= 0.5f) result = GameData.HIT_100; else if (tickRatio > 0f) result = GameData.HIT_50; else result = GameData.HIT_MISS; if (currentRepeats % 2 == 0) {// last circle float[] lastPos = bezier.pointAt(1); data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result, lastPos[0],lastPos[1], color, comboEnd, hitObject.getHitSoundType()); }else // first circle data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result, hitObject.getX(), hitObject.getY(), color, comboEnd, hitObject.getHitSoundType()); return result; } @Override public boolean mousePressed(int x, int y) { if (sliderClicked) // first circle already processed return false; double distance = Math.hypot(hitObject.getX() - x, hitObject.getY() - y); int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2; if (distance < circleRadius) { int trackPosition = MusicController.getPosition(); int timeDiff = Math.abs(trackPosition - hitObject.getTime()); int[] hitResultOffset = game.getHitResultOffsets(); int result = -1; if (timeDiff < hitResultOffset[GameData.HIT_50]) { result = GameData.HIT_SLIDER30; ticksHit++; } else if (timeDiff < hitResultOffset[GameData.HIT_MISS]) result = GameData.HIT_MISS; //else not a hit if (result > -1) { sliderClicked = true; data.sliderTickResult(hitObject.getTime(), result, hitObject.getX(), hitObject.getY(), hitObject.getHitSoundType()); return true; } } return false; } @Override public boolean update(boolean overlap, int delta, int mouseX, int mouseY) { int repeatCount = hitObject.getRepeatCount(); // slider time and tick calculations if (sliderTimeTotal == 0f) { // slider time this.sliderTime = game.getBeatLength() * (hitObject.getPixelLength() / sliderMultiplier) / 100f; this.sliderTimeTotal = sliderTime * repeatCount; // ticks float tickLengthDiv = 100f * sliderMultiplier / sliderTickRate / game.getTimingPointMultiplier(); int tickCount = (int) Math.ceil(hitObject.getPixelLength() / tickLengthDiv) - 1; if (tickCount > 0) { this.ticksT = new float[tickCount]; float tickTOffset = 1f / (tickCount + 1); float t = tickTOffset; for (int i = 0; i < tickCount; i++, t += tickTOffset) ticksT[i] = t; } } byte hitSound = hitObject.getHitSoundType(); int trackPosition = MusicController.getPosition(); int[] hitResultOffset = game.getHitResultOffsets(); int lastIndex = hitObject.getSliderX().length - 1; boolean isAutoMod = GameMod.AUTO.isActive(); if (!sliderClicked) { int time = hitObject.getTime(); // start circle time passed if (trackPosition > time + hitResultOffset[GameData.HIT_50]) { sliderClicked = true; if (isAutoMod) { // "auto" mod: catch any missed notes due to lag ticksHit++; data.sliderTickResult(time, GameData.HIT_SLIDER30, hitObject.getX(), hitObject.getY(), hitSound); } else data.sliderTickResult(time, GameData.HIT_MISS, hitObject.getX(), hitObject.getY(), hitSound); } // "auto" mod: send a perfect hit result else if (isAutoMod) { if (Math.abs(trackPosition - time) < hitResultOffset[GameData.HIT_300]) { ticksHit++; sliderClicked = true; data.sliderTickResult(time, GameData.HIT_SLIDER30, hitObject.getX(), hitObject.getY(), hitSound); } } } // end of slider if (overlap || trackPosition > hitObject.getTime() + sliderTimeTotal) { tickIntervals++; // "auto" mod: send a perfect hit result if (isAutoMod) ticksHit++; // check if cursor pressed and within end circle else if (Utils.isGameKeyPressed()) { float[] c = bezier.pointAt(getT(trackPosition, false)); double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY); int followCircleRadius = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getWidth() / 2; if (distance < followCircleRadius) ticksHit++; } // calculate and send slider result hitResult(); return true; } // repeats boolean isNewRepeat = false; if (repeatCount - 1 > currentRepeats) { float t = getT(trackPosition, true); if (Math.floor(t) > currentRepeats) { currentRepeats++; tickIntervals++; isNewRepeat = true; } } // ticks boolean isNewTick = false; if (ticksT != null && tickIntervals < (ticksT.length * (currentRepeats + 1)) + repeatCount && tickIntervals < (ticksT.length * repeatCount) + repeatCount) { float t = getT(trackPosition, true); if (t - Math.floor(t) >= ticksT[tickIndex]) { tickIntervals++; tickIndex = (tickIndex + 1) % ticksT.length; isNewTick = true; } } // holding slider... float[] c = bezier.pointAt(getT(trackPosition, false)); double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY); int followCircleRadius = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getWidth() / 2; if ((Utils.isGameKeyPressed() && distance < followCircleRadius) || isAutoMod) { // mouse pressed and within follow circle followCircleActive = true; data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER); // held during new repeat if (isNewRepeat) { ticksHit++; if (currentRepeats % 2 > 0) // last circle data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30, hitObject.getSliderX()[lastIndex], hitObject.getSliderY()[lastIndex], hitSound); else // first circle data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30, c[0], c[1], hitSound); } // held during new tick if (isNewTick) { ticksHit++; data.sliderTickResult(trackPosition, GameData.HIT_SLIDER10, c[0], c[1], (byte) -1); } } else { followCircleActive = false; if (isNewRepeat) data.sliderTickResult(trackPosition, GameData.HIT_MISS, 0, 0, (byte) -1); if (isNewTick) data.sliderTickResult(trackPosition, GameData.HIT_MISS, 0, 0, (byte) -1); } return false; } /** * Returns the t value based on the given track position. * @param trackPosition the current track position * @param raw if false, ensures that the value lies within [0, 1] by looping repeats * @return the t value: raw [0, repeats] or looped [0, 1] */ private float getT(int trackPosition, boolean raw) { float t = (trackPosition - hitObject.getTime()) / sliderTime; if (raw) return t; else { float floor = (float) Math.floor(t); return (floor % 2 == 0) ? t - floor : floor + 1 - t; } } }