diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index 7a051a53..11aac0a3 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -913,7 +913,9 @@ public class GameData { float oldColorAlpha = hitResult.color.a; Colors.WHITE_FADE.a = alpha; hitResult.color.a = alpha; - hitResult.curve.draw(hitResult.color); + if (!Options.isShrinkingSliders()) { + hitResult.curve.draw(hitResult.color); + } Colors.WHITE_FADE.a = oldWhiteAlpha; hitResult.color.a = oldColorAlpha; } diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index 37f6efb9..cfc6a366 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -502,7 +502,14 @@ public class Options { FORCE_DEFAULT_PLAYFIELD ("Force Default Playfield", "ForceDefaultPlayfield", "Override the song background with the default playfield background.", false), IGNORE_BEATMAP_SKINS ("Ignore All Beatmap Skins", "IgnoreBeatmapSkins", "Never use skin element overrides provided by beatmaps.", false), SNAKING_SLIDERS ("Snaking sliders", "SnakingSliders", "Sliders gradually snake out from their starting point.", true), + SHRINKING_SLIDERS ("Shrinking sliders", "ShrinkingSliders", "Sliders shrinks when sliderball passes - unstable!", false), FALLBACK_SLIDERS ("Fallback sliders", "FallbackSliders", "Enable this if sliders won't render", false), + MERGING_SLIDERS ("Merging sliders", "MergingSliders", "Merge sliders (aka knorkesliders) - unstable!", false) { + @Override + public boolean showCondition() { + return !FALLBACK_SLIDERS.bool; + } + }, SHOW_HIT_LIGHTING ("Show Hit Lighting", "HitLighting", "Adds an effect behind hit explosions.", true), SHOW_COMBO_BURSTS ("Show Combo Bursts", "ComboBurst", "A character image is displayed at combo milestones.", true), SHOW_PERFECT_HIT ("Show Perfect Hits", "PerfectHit", "Whether to show perfect hit result bursts (300s, slider ticks).", true), @@ -1775,6 +1782,9 @@ public class Options { public static boolean isFallbackSliders() { return GameOption.FALLBACK_SLIDERS.getBooleanValue(); } + public static boolean isShrinkingSliders() { return GameOption.SHRINKING_SLIDERS.getBooleanValue(); } + public static boolean isMergingSliders() { return !isFallbackSliders() && GameOption.MERGING_SLIDERS.getBooleanValue(); } + /** * Returns the fixed circle size override, if any. * @return the CS value (0, 10], 0f if disabled diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 7a2c95b0..cb893ea4 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -122,6 +122,10 @@ public class Slider extends GameObject { private int tickExpand = 0; private final int TICKEXPAND = 200; + public int baseSliderFrom; + + private boolean reversed; + /** * Initializes the Slider data type with images and dimensions. * @param container the game container @@ -193,6 +197,9 @@ public class Slider extends GameObject { @Override public void draw(Graphics g, int trackPosition, boolean mirror) { + if (trackPosition > getEndTime()) { + return; + } Color orig = color; if (mirror) { color = mirrorColor; @@ -217,10 +224,9 @@ public class Slider extends GameObject { if (GameMod.HIDDEN.isActive() && trackPosition > getTime()) { curveAlpha = Math.max(0f, 1f - ((float) (trackPosition - getTime()) / (getEndTime() - getTime())) * 1.05f); } - curveColor.a = curveAlpha; - float curveInterval = Options.isSliderSnaking() ? alpha : 1f; - curve.draw(curveColor, curveInterval); + curveColor.a = curveAlpha; + boolean isCurveCompletelyDrawn = drawSliderTrack(trackPosition, alpha); color.a = alpha; g.pushTransform(); @@ -246,19 +252,8 @@ public class Slider extends GameObject { // ticks if (ticksT != null) { - float tickScale = 0.5f + 0.5f * AnimationEquation.OUT_BACK.calc(decorationsAlpha); - Image tick = GameImage.SLIDER_TICK.getImage().getScaledCopy(tickScale); - for (int i = 0; i < ticksT.length; i++) { - Vec2f c = curve.pointAt(ticksT[i]); - Colors.WHITE_FADE.a = Math.min(curveAlpha, decorationsAlpha); - g.pushTransform(); - if (mirror) { - g.rotate(c.x, c.y, -180f); - } - tick.drawCentered(c.x, c.y, Colors.WHITE_FADE); - g.popTransform(); - Colors.WHITE_FADE.a = alpha; - } + drawSliderTicks(g, trackPosition, curveAlpha, decorationsAlpha, mirror); + Colors.WHITE_FADE.a = alpha; } g.pushTransform(); @@ -285,7 +280,7 @@ public class Slider extends GameObject { g.popTransform(); // repeats - if (curveInterval == 1.0f) { + if (isCurveCompletelyDrawn) { for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) { if (hitObject.getRepeatCount() - 1 > tcurRepeat) { Image arrow = GameImage.REVERSEARROW.getImage(); @@ -369,6 +364,70 @@ public class Slider extends GameObject { color = orig; } + private void drawSliderTicks(Graphics g, int trackPosition, float curveAlpha, float decorationsAlpha, boolean mirror) { + float tickScale = 0.5f + 0.5f * AnimationEquation.OUT_BACK.calc(decorationsAlpha); + Image tick = GameImage.SLIDER_TICK.getImage().getScaledCopy(tickScale); + + // calculate which ticks need to be drawn (don't draw if sliderball crossed it) + int min = 0; + int max = ticksT.length; + if (trackPosition > getTime()) { + for (int i = 0; i < ticksT.length; ) { + if (((trackPosition - getTime()) % sliderTime) / sliderTime < ticksT[i]) { + break; + } + min = ++i; + } + } + if (currentRepeats % 2 == 1) { + max -= min; + min = 0; + } + + for (int i = min; i < max; i++) { + Vec2f c = curve.pointAt(ticksT[i]); + Colors.WHITE_FADE.a = Math.min(curveAlpha, decorationsAlpha); + g.pushTransform(); + if (mirror) { + g.rotate(c.x, c.y, -180f); + } + tick.drawCentered(c.x, c.y, Colors.WHITE_FADE); + g.popTransform(); + } + } + + private boolean drawSliderTrack(int trackPosition, float snakingSliderProgress) { + float curveIntervalTo = Options.isSliderSnaking() ? snakingSliderProgress : 1f; + float curveIntervalFrom = 0f; + if (Options.isShrinkingSliders()) { + float sliderprogress = (trackPosition - getTime() - (sliderTime * (repeats - 1))) / sliderTime; + if (sliderprogress > 0) { + curveIntervalFrom = sliderprogress; + } + } + if (Options.isMergingSliders()) { + if (Options.isShrinkingSliders() && curveIntervalFrom > 0) { + int curvelen = curve.getCurvePoints().length; + if (repeats % 2 == 0) { + game.spliceSliderCurve(baseSliderFrom + (int) ((1f - curveIntervalFrom) * curvelen), baseSliderFrom + curvelen); + } else { + game.setSlidercurveFrom(baseSliderFrom + (int) (curveIntervalFrom * curvelen) + 1); + } + } + game.setSlidercurveTo(baseSliderFrom + (int) (curveIntervalTo * curve.getCurvePoints().length)); + } else { + if (Options.isShrinkingSliders() && curveIntervalFrom > 0) { + int curvelen = curve.getCurvePoints().length; + if (repeats % 2 == 0) { + curve.splice((int) ((1f - curveIntervalFrom) * curvelen), curvelen); + curveIntervalFrom = 0f; + } + } + curve.draw(curveColor, curveIntervalFrom, curveIntervalTo); + } + return curveIntervalTo == 1f; + } + /** * Calculates the slider hit result. * @return the hit result (GameData.HIT_* constants) @@ -550,6 +609,9 @@ public class Slider extends GameObject { // calculate and send slider result hitResult(); + if (Options.isMergingSliders()) { + game.setSlidercurveFrom(baseSliderFrom + curve.getCurvePoints().length + 1); + } return true; } diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java index 2a881b3a..f357e7e3 100644 --- a/src/itdelatrisu/opsu/objects/curves/Curve.java +++ b/src/itdelatrisu/opsu/objects/curves/Curve.java @@ -60,7 +60,11 @@ public abstract class Curve { private CurveRenderState renderState; /** Points along the curve (set by inherited classes). */ - protected Vec2f[] curve; + public Vec2f[] curve; + + public Vec2f[] getCurvePoints() { + return curve; + } private Color fallbackSliderColor = new Color(20, 20, 20); @@ -117,29 +121,32 @@ public abstract class Curve { * Draws the full curve to the graphics context. * @param color the color filter */ - public void draw(Color color) { draw(color, 1f); } + public void draw(Color color) { draw(color, 0f, 1f); } /** * Draws the curve in the range [0, t] (where the full range is [0, 1]) to the graphics context. * @param color the color filter - * @param t set the curve interval to [0, t] + * @param t1 interval to draw from + * @param t2 interval to draw to */ - public void draw(Color color, float t) { + public void draw(Color color, float t1, float t2) { if (curve == null) return; - t = Utils.clamp(t, 0f, 1f); + t1 = Utils.clamp(t1, 0f, 1f); + t2 = Utils.clamp(t2, 0f, 1f); // peppysliders if (Options.isFallbackSliders() || Options.getSkin().getSliderStyle() == Skin.STYLE_PEPPYSLIDER || !mmsliderSupported) { - int drawUpTo = (int) (curve.length * t); + int drawFrom = (int) (curve.length * t1); + int drawUpTo = (int) (curve.length * t2); Image hitCircle = GameImage.HITCIRCLE.getImage(); Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage(); - for (int i = 0; i < drawUpTo; i++) + for (int i = drawFrom; i < drawUpTo; i++) hitCircleOverlay.drawCentered(curve[i].x, curve[i].y, Colors.WHITE_FADE); float a = fallbackSliderColor.a; fallbackSliderColor.a = color.a; - for (int i = 0; i < drawUpTo; i++) + for (int i = drawFrom; i < drawUpTo; i++) hitCircle.drawCentered(curve[i].x, curve[i].y, fallbackSliderColor); fallbackSliderColor.a = a; } @@ -148,10 +155,14 @@ public abstract class Curve { else { if (renderState == null) renderState = new CurveRenderState(hitObject, curve); - renderState.draw(color, borderColor, t); + renderState.draw(color, borderColor, t1, t2); // TODO } } + public void splice(int from, int to) { + renderState.splice(from, to); + } + /** * Returns the angle of the first control point. */ diff --git a/src/itdelatrisu/opsu/render/CurveRenderState.java b/src/itdelatrisu/opsu/render/CurveRenderState.java index 34080c0c..35b0e4f7 100644 --- a/src/itdelatrisu/opsu/render/CurveRenderState.java +++ b/src/itdelatrisu/opsu/render/CurveRenderState.java @@ -20,6 +20,7 @@ package itdelatrisu.opsu.render; import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.beatmap.HitObject; +import itdelatrisu.opsu.objects.Circle; import itdelatrisu.opsu.objects.curves.Vec2f; import java.nio.ByteBuffer; @@ -59,11 +60,14 @@ public class CurveRenderState { /** The HitObject associated with the curve to be drawn. */ protected HitObject hitObject; - /** The points along the curve to be drawn. */ protected Vec2f[] curve; - + /** The point to which the curve has last been rendered into the texture (as an index into {@code curve}). */ private int lastPointDrawn; + private int firstPointDrawn; + + private int spliceFrom; + private int spliceTo; /** * Set the width and height of the container that Curves get drawn into. @@ -85,7 +89,7 @@ public class CurveRenderState { /** * Undo the static state. Static state setup caused by calls to - * {@link #draw(org.newdawn.slick.Color, org.newdawn.slick.Color, float)} + * {@link #draw(org.newdawn.slick.Color, org.newdawn.slick.Color, float, float)} * are undone. */ public static void shutdown() { @@ -99,9 +103,24 @@ public class CurveRenderState { * @param curve the points along the curve to be drawn */ public CurveRenderState(HitObject hitObject, Vec2f[] curve) { - fbo = null; this.hitObject = hitObject; this.curve = curve; + FrameBufferCache cache = FrameBufferCache.getInstance(); + Rendertarget mapping = cache.get(hitObject); + if (mapping == null) + mapping = cache.insert(hitObject); + fbo = mapping; + createVertexBuffer(fbo.getVbo()); + //write impossible value to make sure the fbo is cleared + lastPointDrawn = -1; + spliceFrom = spliceTo = -1; + } + + public void splice(int from, int to) { + spliceFrom = from * 2; + spliceTo = to * 2; + firstPointDrawn = -1; // force redraw + lastPointDrawn = -1; // force redraw } /** @@ -110,30 +129,17 @@ public class CurveRenderState { * runs it just draws the cached copy to the screen. * @param color tint of the curve * @param borderColor the curve border color - * @param t the point up to which the curve should be drawn (in the interval [0, 1]) + * @param t2 the point up to which the curve should be drawn (in the interval [0, 1]) */ - public void draw(Color color, Color borderColor, float t) { - t = Utils.clamp(t, 0.0f, 1.0f); + public void draw(Color color, Color borderColor, float t1, float t2) { + t1 = Utils.clamp(t1, 0.0f, 1.0f); + t2 = Utils.clamp(t2, 0.0f, 1.0f); float alpha = color.a; - // if this curve hasn't been drawn, draw it and cache the result - if (fbo == null) { - FrameBufferCache cache = FrameBufferCache.getInstance(); - Rendertarget mapping = cache.get(hitObject); - if (mapping == null) - mapping = cache.insert(hitObject); - fbo = mapping; - createVertexBuffer(fbo.getVbo()); - //write impossible value to make sure the fbo is cleared - lastPointDrawn = -1; - } + int drawFrom = (int) (t1 * curve.length); + int drawUpTo = (int) (t2 * curve.length); - int drawUpTo = (int) (t * curve.length); - - if (lastPointDrawn != drawUpTo) { - if (drawUpTo == lastPointDrawn) - return; - + if (lastPointDrawn != drawUpTo || firstPointDrawn != drawFrom) { int oldFb = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT); int oldTex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); //glGetInteger requires a buffer of size 16, even though just 4 @@ -147,8 +153,14 @@ public class CurveRenderState { GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); } - - this.renderCurve(color, borderColor, lastPointDrawn, drawUpTo); + if (firstPointDrawn != drawFrom) { + GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); + firstPointDrawn = drawFrom; + this.renderCurve(color, borderColor, drawFrom, drawUpTo, true); + } else { + this.renderCurve(color, borderColor, lastPointDrawn, drawUpTo, false); + } lastPointDrawn = drawUpTo; color.a = 1f; @@ -270,19 +282,27 @@ public class CurveRenderState { private void createVertexBuffer(int bufferID) { int arrayBufferBinding = GL11.glGetInteger(GL15.GL_ARRAY_BUFFER_BINDING); FloatBuffer buff = BufferUtils.createByteBuffer(4 * (4 + 2) * (2 * curve.length - 1) * (NewCurveStyleState.DIVIDES + 2)).asFloatBuffer(); - for (int i = 0; i < curve.length; ++i) { + if (curve.length > 0) { + fillCone(buff, curve[0].x, curve[0].y); + } + for (int i = 1; i < curve.length; ++i) { float x = curve[i].x; float y = curve[i].y; fillCone(buff, x, y); - if (i != 0) { - float last_x = curve[i - 1].x; - float last_y = curve[i - 1].y; - double diff_x = x - last_x; - double diff_y = y - last_y; + float last_x = curve[i - 1].x; + float last_y = curve[i - 1].y; + double diff_x = x - last_x; + double diff_y = y - last_y; + float dist = Utils.distance(x, y, last_x, last_y); + if (dist < Circle.diameter / 8) { x = (float) (x - diff_x / 2); y = (float) (y - diff_y / 2); - fillCone(buff, x, y); + } else { + // don't mind me + x = -100f; + y = -100f; } + fillCone(buff, x, y); } buff.flip(); GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, bufferID); @@ -295,7 +315,7 @@ public class CurveRenderState { * @param color the color of the curve * @param borderColor the curve border color */ - private void renderCurve(Color color, Color borderColor, int from, int to) { + private void renderCurve(Color color, Color borderColor, int from, int to, boolean clearFirst) { staticState.initGradient(); RenderState state = saveRenderState(); staticState.initShaderProgram(); @@ -310,8 +330,15 @@ public class CurveRenderState { //2*4 is for skipping the first 2 floats (u,v) GL20.glVertexAttribPointer(staticState.attribLoc, 4, GL11.GL_FLOAT, false, 6 * 4, 2 * 4); GL20.glVertexAttribPointer(staticState.texCoordLoc, 2, GL11.GL_FLOAT, false, 6 * 4, 0); - for (int i = from * 2; i < to * 2 - 1; ++i) + if (clearFirst) { + GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); + } + for (int i = from * 2; i < to * 2 - 1; ++i) { + if (spliceFrom <= i && i <= spliceTo) { + continue; + } GL11.glDrawArrays(GL11.GL_TRIANGLE_FAN, i * (NewCurveStyleState.DIVIDES + 2), NewCurveStyleState.DIVIDES + 2); + } GL11.glFlush(); GL20.glDisableVertexAttribArray(staticState.texCoordLoc); GL20.glDisableVertexAttribArray(staticState.attribLoc); diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index d47ba2ec..6ea75fd9 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -52,9 +52,7 @@ import itdelatrisu.opsu.ui.*; import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.io.File; -import java.util.IdentityHashMap; -import java.util.LinkedList; -import java.util.Stack; +import java.util.*; import org.lwjgl.input.Keyboard; import org.lwjgl.opengl.Display; @@ -72,6 +70,7 @@ import org.newdawn.slick.state.transition.EasedFadeOutTransition; import org.newdawn.slick.state.transition.EmptyTransition; import org.newdawn.slick.state.transition.FadeInTransition; import yugecin.opsudance.*; +import yugecin.opsudance.objects.curves.FakeCombinedCurve; import yugecin.opsudance.ui.SBOverlay; /** @@ -281,6 +280,8 @@ public class Game extends BasicGameState { private final Cursor mirrorCursor; private final SBOverlay sbOverlay; + private FakeCombinedCurve knorkesliders; + public Game(int state) { this.state = state; mirrorCursor = new Cursor(true); @@ -1453,8 +1454,22 @@ public class Game extends BasicGameState { } this.leadInTime += epiImgTime; SoundController.mute(false); + + if (Options.isMergingSliders()) { + // let's create knorkesliders + List curvepoints = new ArrayList<>(); + for (GameObject gameObject : gameObjects) { + if (gameObject.isSlider()) { + ((Slider) gameObject).baseSliderFrom = curvepoints.size(); + curvepoints.addAll(Arrays.asList(((Slider) gameObject).getCurve().getCurvePoints())); + } + } + knorkesliders = new FakeCombinedCurve(curvepoints.toArray(new Vec2f[curvepoints.size()])); + } } + slidercurveFrom = 0; + slidercurveTo = 0; Dancer.instance.setGameObjects(gameObjects); sbOverlay.setGameObjects(gameObjects); @@ -1495,12 +1510,32 @@ public class Game extends BasicGameState { GameMod.loadModState(previousMods); } + private float slidercurveFrom; + private float slidercurveTo; + + public void setSlidercurveFrom(int slidercurveFrom) { + float pos = (float) slidercurveFrom / knorkesliders.getCurvePoints().length; + this.slidercurveFrom = Math.max(pos, this.slidercurveFrom); + } + + public void setSlidercurveTo(int slidercurveTo) { + float pos = (float) slidercurveTo / knorkesliders.getCurvePoints().length; + this.slidercurveTo = Math.max(pos, this.slidercurveTo); + } + + public void spliceSliderCurve(int from, int to) { + this.knorkesliders.splice(from, to); + } + /** * Draws hit objects, hit results, and follow points. * @param g the graphics context * @param trackPosition the track position */ private void drawHitObjects(Graphics g, int trackPosition) { + if (Options.isMergingSliders()) { + knorkesliders.draw(Color.white, this.slidercurveFrom, this.slidercurveTo); + } // include previous object in follow points int lastObjectIndex = -1; if (objectIndex > 0 && objectIndex < beatmap.objects.length && diff --git a/src/itdelatrisu/opsu/states/OptionsMenu.java b/src/itdelatrisu/opsu/states/OptionsMenu.java index a41cb1a5..6f0cb44b 100644 --- a/src/itdelatrisu/opsu/states/OptionsMenu.java +++ b/src/itdelatrisu/opsu/states/OptionsMenu.java @@ -82,7 +82,9 @@ public class OptionsMenu extends BasicGameState { GameOption.FORCE_DEFAULT_PLAYFIELD, GameOption.IGNORE_BEATMAP_SKINS, GameOption.SNAKING_SLIDERS, + GameOption.SHRINKING_SLIDERS, GameOption.FALLBACK_SLIDERS, + GameOption.MERGING_SLIDERS, GameOption.SHOW_HIT_LIGHTING, GameOption.SHOW_COMBO_BURSTS, GameOption.SHOW_PERFECT_HIT, diff --git a/src/yugecin/opsudance/objects/curves/FakeCombinedCurve.java b/src/yugecin/opsudance/objects/curves/FakeCombinedCurve.java new file mode 100644 index 00000000..788c3886 --- /dev/null +++ b/src/yugecin/opsudance/objects/curves/FakeCombinedCurve.java @@ -0,0 +1,46 @@ +/* + * opsu!dance - fork of opsu! with cursordance auto + * Copyright (C) 2016 yugecin + * + * opsu!dance 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!dance 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!dance. If not, see . + */ +package yugecin.opsudance.objects.curves; + +import itdelatrisu.opsu.beatmap.HitObject; +import itdelatrisu.opsu.objects.curves.Curve; +import itdelatrisu.opsu.objects.curves.Vec2f; + +public class FakeCombinedCurve extends Curve { + + public FakeCombinedCurve(Vec2f[] points) { + super(new HitObject(0, 0, 0), false); + this.curve = points; + } + + @Override + public Vec2f pointAt(float t) { + return null; + } + + @Override + public float getEndAngle() { + return 0; + } + + @Override + public float getStartAngle() { + return 0; + } + +}