diff --git a/src/itdelatrisu/opsu/OsuHitObject.java b/src/itdelatrisu/opsu/OsuHitObject.java index 1605286b..3ada129b 100644 --- a/src/itdelatrisu/opsu/OsuHitObject.java +++ b/src/itdelatrisu/opsu/OsuHitObject.java @@ -72,9 +72,27 @@ public class OsuHitObject { /** The container height. */ private static int containerHeight; + /** + * Return stack position modifier in pixels. + * @return stack position modifier. + */ + public static float getStackOffset() { return stackOffset; } + + /** + * Sets stack position modifier in pixels + * @param offset position modifier. + */ + public static void setStackOffset(float offset) { stackOffset = offset; } + + /** The offset per stack. */ + private static float stackOffset; + /** Starting coordinates. */ private float x, y; + /** Hit object index in current stack. */ + private int stack; + /** Start time (in ms). */ private int time; @@ -249,18 +267,30 @@ public class OsuHitObject { /** * Returns the scaled starting x coordinate. */ - public float getScaledX() { return x * xMultiplier + xOffset; } + public float getScaledX() { return (x - stack * stackOffset) * xMultiplier + xOffset; } /** * Returns the scaled starting y coordinate. */ public float getScaledY() { if (GameMod.HARD_ROCK.isActive()) - return containerHeight - (y * yMultiplier + yOffset); + return containerHeight - ((y - stack * stackOffset) * yMultiplier + yOffset); else - return y * yMultiplier + yOffset; + return (y - stack * stackOffset) * yMultiplier + yOffset; } + /** + * Sets the hit object index in current stack. + * @param stack index in stack + */ + public void setStack(int stack) { this.stack = stack; } + + /** + * Returns hit object index in current stack. + * @return index in stack + */ + public int getStack() { return stack; } + /** * Returns the start time. * @return the start time (in ms) @@ -331,7 +361,7 @@ public class OsuHitObject { float[] x = new float[sliderX.length]; for (int i = 0; i < x.length; i++) - x[i] = sliderX[i] * xMultiplier + xOffset; + x[i] = (sliderX[i] - stack * stackOffset) * xMultiplier + xOffset; return x; } @@ -346,10 +376,10 @@ public class OsuHitObject { float[] y = new float[sliderY.length]; if (GameMod.HARD_ROCK.isActive()) { for (int i = 0; i < y.length; i++) - y[i] = containerHeight - (sliderY[i] * yMultiplier + yOffset); + y[i] = containerHeight - ((sliderY[i] - stack * stackOffset) * yMultiplier + yOffset); } else { for (int i = 0; i < y.length; i++) - y[i] = sliderY[i] * yMultiplier + yOffset; + y[i] = (sliderY[i] - stack * stackOffset) * yMultiplier + yOffset; } return y; } diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 41091324..a16d1311 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -250,6 +250,20 @@ public class Utils { return val; } + /** + * + * @param x1 The x-component of the first point + * @param y1 The y-component of the first point + * @param x2 The x-component of the second point + * @param y2 The y-component of the second point + * @return Euclidean distance between points (x1,y1) and (x2,y2) + */ + public static float distance(float x1, float y1, float x2, float y2) { + float v1 = Math.abs(x1 - x2); + float v2 = Math.abs(y1 - y2); + return (float) Math.sqrt((v1 * v1) + (v2 * v2)); + } + /** * Returns true if a game input key is pressed (mouse/keyboard left/right). * @return true if pressed diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java index faff2c02..b4e2e4b8 100644 --- a/src/itdelatrisu/opsu/objects/Circle.java +++ b/src/itdelatrisu/opsu/objects/Circle.java @@ -183,4 +183,13 @@ public class Circle implements HitObject { @Override public int getEndTime() { return hitObject.getTime(); } + + @Override + public OsuHitObject getHitObject() { return hitObject; } + + @Override + public void updatePosition() { + this.x = hitObject.getScaledX(); + this.y = hitObject.getScaledY(); + } } diff --git a/src/itdelatrisu/opsu/objects/DummyObject.java b/src/itdelatrisu/opsu/objects/DummyObject.java index fc67bb10..a8ae3458 100644 --- a/src/itdelatrisu/opsu/objects/DummyObject.java +++ b/src/itdelatrisu/opsu/objects/DummyObject.java @@ -53,6 +53,15 @@ public class DummyObject implements HitObject { @Override public boolean mousePressed(int x, int y, int trackPosition) { return false; } + @Override + public OsuHitObject getHitObject() { return hitObject; } + + @Override + public void updatePosition() { + this.x = hitObject.getScaledX(); + this.y = hitObject.getScaledY(); + } + @Override public float[] getPointAt(int trackPosition) { return new float[] { x, y }; } diff --git a/src/itdelatrisu/opsu/objects/HitObject.java b/src/itdelatrisu/opsu/objects/HitObject.java index c46c464b..13631a21 100644 --- a/src/itdelatrisu/opsu/objects/HitObject.java +++ b/src/itdelatrisu/opsu/objects/HitObject.java @@ -18,6 +18,7 @@ package itdelatrisu.opsu.objects; +import itdelatrisu.opsu.OsuHitObject; import org.newdawn.slick.Graphics; /** @@ -64,4 +65,15 @@ public interface HitObject { * @return the end time, in milliseconds */ public int getEndTime(); + + /** + * Return associated OsuHitObject. + * @return hit object as OsuHitObject + */ + public OsuHitObject getHitObject(); + + /** + * Updates position of hit object. + */ + public void updatePosition(); } diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 1023f6a4..ac4f4a6e 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -146,6 +146,21 @@ public class Slider implements HitObject { this.curve = new CircumscribedCircle(hitObject, color); else this.curve = new LinearBezier(hitObject, color); + + // slider time calculations + this.sliderTime = game.getBeatLength() * (hitObject.getPixelLength() / sliderMultiplier) / 100f; + this.sliderTimeTotal = sliderTime * hitObject.getRepeatCount(); + + // 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; + } } @Override @@ -313,25 +328,6 @@ public class Slider implements HitObject { @Override public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed, int trackPosition) { 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; - } - } - int[] hitResultOffset = game.getHitResultOffsets(); boolean isAutoMod = GameMod.AUTO.isActive(); @@ -455,6 +451,20 @@ public class Slider implements HitObject { return false; } + @Override + public OsuHitObject getHitObject() { return hitObject; } + + @Override + public void updatePosition() { + this.x = hitObject.getScaledX(); + this.y = hitObject.getScaledY(); + + if (hitObject.getSliderType() == OsuHitObject.SLIDER_PASSTHROUGH && hitObject.getSliderX().length == 2) + this.curve = new CircumscribedCircle(hitObject, color); + else + this.curve = new LinearBezier(hitObject, color); + } + @Override public float[] getPointAt(int trackPosition) { if (trackPosition <= hitObject.getTime()) diff --git a/src/itdelatrisu/opsu/objects/Spinner.java b/src/itdelatrisu/opsu/objects/Spinner.java index 1829f7fc..e3e25241 100644 --- a/src/itdelatrisu/opsu/objects/Spinner.java +++ b/src/itdelatrisu/opsu/objects/Spinner.java @@ -263,6 +263,12 @@ public class Spinner implements HitObject { return false; } + @Override + public OsuHitObject getHitObject() { return hitObject; } + + @Override + public void updatePosition() {} + @Override public float[] getPointAt(int trackPosition) { // get spinner time diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index b1ab778c..17c5e6c4 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -80,12 +80,18 @@ public class Game extends BasicGameState { /** Replay. */ REPLAY, /** Health is zero: no-continue/force restart. */ - LOSE; + LOSE } /** Minimum time before start of song, in milliseconds, to process skip-related actions. */ private static final int SKIP_OFFSET = 2000; + /** Tolerance in case if hit object is not snapped to the grid */ + private static final float STACK_LENIENCE = 3f; + + /** Stack time window of the previous object in ms. */ + private static final int STACK_TIMEOUT = 1000; + /** The associated OsuFile object. */ private OsuFile osu; @@ -1027,6 +1033,21 @@ public class Game extends BasicGameState { Color color = osu.combo[hitObject.getComboIndex()]; + // pass beatLength to hit objects + int hitObjectTime = hitObject.getTime(); + int timingPointIndex = 0; + while (timingPointIndex < osu.timingPoints.size()) { + OsuTimingPoint timingPoint = osu.timingPoints.get(timingPointIndex); + if (timingPoint.getTime() <= hitObjectTime) { + if (!timingPoint.isInherited()) + beatLengthBase = beatLength = timingPoint.getBeatLength(); + else + beatLength = beatLengthBase * timingPoint.getSliderMultiplier(); + } else + break; + timingPointIndex++; + } + try { if (hitObject.isCircle()) hitObjects[i] = new Circle(hitObject, this, data, color, comboEnd); @@ -1043,6 +1064,72 @@ public class Game extends BasicGameState { } } + // stack calculation + for (int i = hitObjects.length - 1; i > 0; i--) { + OsuHitObject hitObjectI = hitObjects[i].getHitObject(); + + // already calculated + if (hitObjectI.getStack() != 0 || hitObjectI.isSpinner()) + continue; + + // search for hit objects in stack + for (int n = i -1; n >= 0; n--) { + OsuHitObject hitObjectN = hitObjects[n].getHitObject(); + + if (hitObjectN.isSpinner()) + continue; + + // check if in range stack calculation + float timeI = hitObjectI.getTime() - (STACK_TIMEOUT * osu.stackLeniency); + float timeN = hitObjectN.isSlider() ? hitObjects[n].getEndTime() : hitObjectN.getTime(); + if (timeI > timeN) + break; + + if (hitObjectN.isSlider()) { + // possible special case. if slider end in the stack, all next hit objects in stack moves right down + Slider slider = (Slider) hitObjects[n]; + float x1 = hitObjects[i].getPointAt(hitObjectI.getTime())[0]; + float y1 = hitObjects[i].getPointAt(hitObjectI.getTime())[1]; + float x2 = slider.getPointAt(slider.getEndTime())[0]; + float y2 = slider.getPointAt(slider.getEndTime())[1]; + float distance = Utils.distance(x1, y1, x2, y2); + + // check if hit object part of this stack + if (distance < STACK_LENIENCE * OsuHitObject.getXMultiplier()) { + int offset = hitObjectI.getStack() - hitObjectN.getStack() + 1; + for (int j = n + 1; j <= i; j++) { + OsuHitObject hitObjectJ = hitObjects[j].getHitObject(); + x1 = hitObjects[j].getPointAt(hitObjectJ.getTime())[0]; + y1 = hitObjects[j].getPointAt(hitObjectJ.getTime())[1]; + distance = Utils.distance(x1, y1, x2, y2); + + if (distance < STACK_LENIENCE * OsuHitObject.getXMultiplier()) { + // hit object below slider end + hitObjectJ.setStack(hitObjectJ.getStack() - offset); + } + } + // slider end always start of the stack. reset calculation + break; + } + } + // no special case. stack moves up left + float x1 = hitObjectI.getX(); + float y1 = hitObjectI.getY(); + float x2 = hitObjectN.getX(); + float y2 = hitObjectN.getY(); + float distance = Utils.distance(x1, y1, x2, y2); + if (distance < STACK_LENIENCE * OsuHitObject.getXMultiplier()) { + hitObjectN.setStack(hitObjectI.getStack() + 1); + hitObjectI = hitObjectN; + } + } + } + + // update hit objects positions + for (int i = 0; i < hitObjects.length; i++) { + hitObjects[i].updatePosition(); + } + // load the first timingPoint if (!osu.timingPoints.isEmpty()) { OsuTimingPoint timingPoint = osu.timingPoints.get(0); @@ -1331,6 +1418,10 @@ public class Game extends BasicGameState { if (Options.getFixedHP() > 0f) HPDrainRate = Options.getFixedHP(); + // Stack modifier scales with hit object size + int diameter = (int) (104 - (circleSize * 8)); + OsuHitObject.setStackOffset(diameter * 0.05f); + // initialize objects Circle.init(container, circleSize); Slider.init(container, circleSize, osu);