diff --git a/res/spinner-rpm.png b/res/spinner-rpm.png new file mode 100644 index 00000000..f44ed0f8 Binary files /dev/null and b/res/spinner-rpm.png differ diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index cf1f6e20..93ee852e 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -39,6 +39,9 @@ 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 fade. */ + public static final int HITRESULT_FADE_TIME = 500; + /** Time, in milliseconds, for a hit error tick to fade. */ private static final int HIT_ERROR_FADE_TIME = 5000; @@ -207,6 +210,9 @@ public class GameData { /** Combo color. */ public Color color; + /** Whether the hit object was a spinner. */ + public boolean isSpinner; + /** Alpha level (for fading out). */ public float alpha = 1f; @@ -217,13 +223,15 @@ 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 */ - public OsuHitObjectResult(int time, int result, float x, float y, Color color) { + public OsuHitObjectResult(int time, int result, float x, float y, Color color, boolean isSpinner) { this.time = time; this.result = result; this.x = x; this.y = y; this.color = color; + this.isSpinner = isSpinner; } } @@ -233,6 +241,9 @@ public class GameData { /** Displayed game score (for animation, slightly behind score). */ private long scoreDisplay; + /** Displayed game score percent (for animation, slightly behind score percent). */ + private float scorePercentDisplay; + /** Current health bar percentage. */ private float health; @@ -311,6 +322,7 @@ public class GameData { public void clear() { score = 0; scoreDisplay = 0; + scorePercentDisplay = 0f; health = 100f; healthDisplay = 100f; hitResultCount = new int[HIT_MAX]; @@ -435,7 +447,7 @@ public class GameData { * @param scale the scale to apply * @param rightAlign align right (true) or left (false) */ - private void drawSymbolString(String str, int x, int y, float scale, boolean rightAlign) { + public void drawSymbolString(String str, int x, int y, float scale, boolean rightAlign) { char[] c = str.toCharArray(); int cx = x; if (rightAlign) { @@ -466,7 +478,7 @@ public class GameData { * @param fixedsize the width to use for all symbols * @param rightAlign align right (true) or left (false) */ - private void drawFixedSizeSymbolString(String str, int x, int y, float scale, float fixedsize, boolean rightAlign) { + public void drawFixedSizeSymbolString(String str, int x, int y, float scale, float fixedsize, boolean rightAlign) { char[] c = str.toCharArray(); int cx = x; if (rightAlign) { @@ -506,11 +518,9 @@ public class GameData { // score percentage int symbolHeight = getScoreSymbolImage('0').getHeight(); - float scorePercent = getScorePercent(); drawSymbolString( - String.format((scorePercent < 10f) ? "0%.2f%%" : "%.2f%%", scorePercent), - width - marginX, symbolHeight, 0.60f, true - ); + String.format((scorePercentDisplay < 10f) ? "0%.2f%%" : "%.2f%%", scorePercentDisplay), + width - marginX, symbolHeight, 0.60f, true); // map progress circle g.setAntiAlias(true); @@ -771,20 +781,27 @@ public class GameData { * @param trackPosition the current track position */ public void drawHitResults(int trackPosition) { - final int fadeDelay = 500; - Iterator iter = hitResultList.iterator(); while (iter.hasNext()) { OsuHitObjectResult hitResult = iter.next(); - if (hitResult.time + fadeDelay > trackPosition) { + if (hitResult.time + HITRESULT_FADE_TIME > trackPosition) { + // hit result hitResults[hitResult.result].setAlpha(hitResult.alpha); - hitResult.alpha = 1 - ((float) (trackPosition - hitResult.time) / fadeDelay); hitResults[hitResult.result].drawCentered(hitResult.x, hitResult.y); + hitResults[hitResult.result].setAlpha(1f); + + // spinner + if (hitResult.isSpinner && hitResult.result != HIT_MISS) { + Image spinnerOsu = GameImage.SPINNER_OSU.getImage(); + spinnerOsu.setAlpha(hitResult.alpha); + spinnerOsu.drawCentered(width / 2, height / 4); + spinnerOsu.setAlpha(1f); + } // hit lighting - if (Options.isHitLightingEnabled() && hitResult.result != HIT_MISS && + else if (Options.isHitLightingEnabled() && hitResult.result != HIT_MISS && hitResult.result != HIT_SLIDER30 && hitResult.result != HIT_SLIDER10) { - float scale = 1f + ((trackPosition - hitResult.time) / (float) fadeDelay); + 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); @@ -795,6 +812,8 @@ public class GameData { scaledLighting1.draw(hitResult.x - (scaledLighting1.getWidth() / 2f), hitResult.y - (scaledLighting1.getHeight() / 2f), hitResult.color); } + + hitResult.alpha = 1 - ((float) (trackPosition - hitResult.time) / HITRESULT_FADE_TIME); } else iter.remove(); } @@ -901,7 +920,7 @@ public class GameData { } /** - * Updates the score, health, and combo burst displays based on a delta value. + * Updates displayed elements based on a delta value. * @param delta the delta interval since the last call */ public void updateDisplays(int delta) { @@ -912,6 +931,20 @@ public class GameData { scoreDisplay = score; } + // score percent display + float scorePercent = getScorePercent(); + if (scorePercentDisplay != scorePercent) { + if (scorePercentDisplay < scorePercent) { + scorePercentDisplay += (scorePercent - scorePercentDisplay) * delta / 50f + 0.01f; + if (scorePercentDisplay > scorePercent) + scorePercentDisplay = scorePercent; + } else { + scorePercentDisplay -= (scorePercentDisplay - scorePercent) * delta / 50f + 0.01f; + if (scorePercentDisplay < scorePercent) + scorePercentDisplay = scorePercent; + } + } + // health display if (healthDisplay != health) { float shift = delta / 15f; @@ -1027,7 +1060,7 @@ public class GameData { if (!Options.isPerfectHitBurstEnabled()) ; // hide perfect hit results else - hitResultList.add(new OsuHitObjectResult(time, result, x, y, null)); + hitResultList.add(new OsuHitObjectResult(time, result, x, y, null, false)); } } @@ -1040,9 +1073,10 @@ public class GameData { * @param color the combo color * @param end true if this is the last hit object in the combo * @param hitSound the object's hit sound + * @param isSpinner whether the hit object was a spinner */ public void hitResult(int time, int result, float x, float y, Color color, - boolean end, byte hitSound) { + boolean end, byte hitSound, boolean isSpinner) { int hitValue = 0; boolean perfectHit = false; switch (result) { @@ -1109,7 +1143,7 @@ public class GameData { if (perfectHit && !Options.isPerfectHitBurstEnabled()) ; // hide perfect hit results else - hitResultList.add(new OsuHitObjectResult(time, result, x, y, color)); + hitResultList.add(new OsuHitObjectResult(time, result, x, y, color, isSpinner)); } /** diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index b000861b..335ce97a 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -134,6 +134,12 @@ public enum GameImage { SPINNER_SPIN ("spinner-spin", "png"), SPINNER_CLEAR ("spinner-clear", "png"), SPINNER_OSU ("spinner-osu", "png"), + SPINNER_RPM ("spinner-rpm", "png", false, true) { + @Override + protected Image process_sub(Image img, int w, int h) { + return img.getScaledCopy((SCORE_0.getImage().getHeight() * 1.05f) / img.getHeight()); + } + }, // Game Data COMBO_BURST ("comboburst", "comboburst-%d", "png"), diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java index d29470de..18c73c31 100644 --- a/src/itdelatrisu/opsu/objects/Circle.java +++ b/src/itdelatrisu/opsu/objects/Circle.java @@ -141,7 +141,7 @@ public class Circle implements HitObject { data.hitResult( hitObject.getTime(), result, hitObject.getX(), hitObject.getY(), - color, comboEnd, hitObject.getHitSoundType() + color, comboEnd, hitObject.getHitSoundType(), false ); return true; } @@ -161,17 +161,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, hitSound); + data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitSound, false); else // no more points can be scored, so send a miss - data.hitResult(time, GameData.HIT_MISS, x, y, null, comboEnd, hitSound); + data.hitResult(time, GameData.HIT_MISS, x, y, null, comboEnd, hitSound, false); 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, hitSound); + data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitSound, false); return true; } } diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 0076a060..4129b5ca 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -255,10 +255,10 @@ public class Slider implements HitObject { 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.getHitSoundType()); + lastPos[0],lastPos[1], color, comboEnd, hitObject.getHitSoundType(), false); } else { // first circle data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result, - hitObject.getX(), hitObject.getY(), color, comboEnd, hitObject.getHitSoundType()); + hitObject.getX(), hitObject.getY(), color, comboEnd, hitObject.getHitSoundType(), false); } return result; diff --git a/src/itdelatrisu/opsu/objects/Spinner.java b/src/itdelatrisu/opsu/objects/Spinner.java index 27a5116d..7a16f5bc 100644 --- a/src/itdelatrisu/opsu/objects/Spinner.java +++ b/src/itdelatrisu/opsu/objects/Spinner.java @@ -106,9 +106,6 @@ public class Spinner implements HitObject { int timeDiff = hitObject.getTime() - trackPosition; boolean spinnerComplete = (rotations >= rotationsNeeded); - // TODO: draw "OSU!" image after spinner ends - //GameImage.SPINNER_OSU.getImage().drawCentered(width / 2, height / 4); - // darken screen g.setColor(Utils.COLOR_BLACK_ALPHA); g.fillRect(0, 0, width, height); @@ -116,6 +113,13 @@ public class Spinner implements HitObject { if (timeDiff > 0) return; + // rpm (TODO: make this work for Auto/Spun-Out mods) + int rpm = Math.abs(Math.round(sumVelocity / storedVelocities.length * 60)); + Image rpmImg = GameImage.SPINNER_RPM.getImage(); + rpmImg.drawCentered(width / 2f, height - rpmImg.getHeight() / 2f); + data.drawSymbolString(Integer.toString(rpm), (int) ((width + rpmImg.getWidth() * 0.95f) / 2f), + (int) (height - data.getScoreSymbolImage('0').getHeight() * 1.025f), 1f, true); + // spinner meter (subimage) Image spinnerMetre = GameImage.SPINNER_METRE.getImage(); int spinnerMetreY = (spinnerComplete) ? 0 : (int) (spinnerMetre.getHeight() * (1 - (rotations / rotationsNeeded))); @@ -138,10 +142,6 @@ public class Spinner implements HitObject { if (extraRotations > 0) data.drawSymbolNumber(extraRotations * 1000, width / 2, height * 2 / 3, 1.0f); } - - // TODO: add rpm meter at bottom of spinner - // TODO 2: make this work for Auto/Spun-Out mods -// int rpm = Math.abs(Math.round(sumVelocity / storedVelocities.length * 60)); } /** @@ -150,22 +150,21 @@ public class Spinner implements HitObject { */ private int hitResult() { // TODO: verify ratios - int result; float ratio = rotations / rotationsNeeded; if (ratio >= 1.0f || GameMod.AUTO.isActive() || GameMod.SPUN_OUT.isActive()) { result = GameData.HIT_300; SoundController.playSound(SoundEffect.SPINNEROSU); - } else if (ratio >= 0.8f) + } else if (ratio >= 0.9f) result = GameData.HIT_100; - else if (ratio >= 0.5f) + else if (ratio >= 0.75f) result = GameData.HIT_50; else result = GameData.HIT_MISS; data.hitResult(hitObject.getEndTime(), result, width / 2, height / 2, - Color.transparent, true, (byte) -1); + Color.transparent, true, (byte) -1, true); return result; }