From e62822ab5fd2b413943e290c5d7c574abbb112c3 Mon Sep 17 00:00:00 2001 From: Jeffrey Han <itdelatrisu@gmail.com> Date: Sat, 7 Jan 2017 04:40:17 -0500 Subject: [PATCH] Implemented osu! HP algorithms. (base: ppy/osu-iPhone@e72787f) Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com> --- src/itdelatrisu/opsu/GameData.java | 119 +++++------ src/itdelatrisu/opsu/Utils.java | 17 ++ .../beatmap/BeatmapHPDropRateCalculator.java | 199 ++++++++++++++++++ src/itdelatrisu/opsu/beatmap/Health.java | 167 +++++++++++++++ src/itdelatrisu/opsu/objects/Spinner.java | 19 +- src/itdelatrisu/opsu/states/Game.java | 38 ++-- 6 files changed, 470 insertions(+), 89 deletions(-) create mode 100644 src/itdelatrisu/opsu/beatmap/BeatmapHPDropRateCalculator.java create mode 100644 src/itdelatrisu/opsu/beatmap/Health.java diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index db6af0cc..74a8e125 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -23,6 +23,7 @@ import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.beatmap.Beatmap; +import itdelatrisu.opsu.beatmap.Health; import itdelatrisu.opsu.beatmap.HitObject; import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.objects.curves.Curve; @@ -47,9 +48,6 @@ * Holds game data and renders all related elements. */ 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 remain existent. */ public static final int HITRESULT_TIME = 833; @@ -149,9 +147,11 @@ public Image getMenuImage() { HIT_300G = 6, // Geki HIT_SLIDER10 = 7, HIT_SLIDER30 = 8, - HIT_MAX = 9, // not a hit result - HIT_SLIDER_REPEAT = 10, // not a hit result - HIT_ANIMATION_RESULT = 11; // not a hit result + HIT_MAX = 9, + HIT_SLIDER_REPEAT = 10, + HIT_ANIMATION_RESULT = 11, + HIT_SPINNERSPIN = 12, + HIT_SPINNERBONUS = 13; /** Hit result-related images (indexed by HIT_* constants to HIT_MAX). */ private Image[] hitResults; @@ -298,19 +298,12 @@ public HitObjectResult(int time, int result, float x, float y, Color color, /** Displayed game score percent (for animation, slightly behind score percent). */ private float scorePercentDisplay; - /** Current health bar percentage. */ - private float health; - - /** Displayed health (for animation, slightly behind health). */ - private float healthDisplay; + /** Health. */ + private Health health = new Health(); /** The difficulty multiplier used in the score formula. */ private int difficultyMultiplier = 2; - /** Beatmap HPDrainRate value. (0:easy ~ 10:hard) */ - @SuppressWarnings("unused") - private float drainRate = 5f; - /** Default text symbol images. */ private Image[] defaultSymbols; @@ -383,10 +376,8 @@ public void clear() { score = 0; scoreDisplay = 0; scorePercentDisplay = 0f; - health = 100f; - healthDisplay = 100f; + health.reset(); hitResultCount = new int[HIT_MAX]; - drainRate = 5f; if (hitResultList != null) { for (HitObjectResult hitResult : hitResultList) { if (hitResult.curve != null) @@ -477,12 +468,6 @@ public void loadImages() { */ public Image getScoreSymbolImage(char c) { return scoreSymbols.get(c); } - /** - * Sets the health drain rate. - * @param drainRate the new drain rate [0-10] - */ - public void setDrainRate(float drainRate) { this.drainRate = drainRate; } - /** * Sets the array of hit result offsets. * @param hitResultOffset the time offset array (of size {@link #HIT_MAX}) @@ -713,7 +698,7 @@ public void drawGameElements(Graphics g, boolean breakPeriod, boolean firstObjec if (!breakPeriod && !relaxAutoPilot) { // scorebar - float healthRatio = healthDisplay / 100f; + float healthRatio = health.getHealthDisplay() / 100f; if (firstObject) { // gradually move ki before map begins if (firstObjectTime >= 1500 && trackPosition < firstObjectTime - 500) healthRatio = (float) trackPosition / (firstObjectTime - 500); @@ -736,9 +721,9 @@ public void drawGameElements(Graphics g, boolean breakPeriod, boolean firstObjec colourCropped.setAlpha(1f); Image ki = null; - if (health >= 50f) + if (health.getHealth() >= 50f) ki = GameImage.SCOREBAR_KI.getImage(); - else if (health >= 25f) + else if (health.getHealth() >= 25f) ki = GameImage.SCOREBAR_KI_DANGER.getImage(); else ki = GameImage.SCOREBAR_KI_DANGER2.getImage(); @@ -1104,32 +1089,35 @@ private void drawHitAnimations(HitObjectResult hitResult, int trackPosition) { } /** - * Changes health by a given percentage, modified by drainRate. - * @param percent the health percentage + * Returns the current health percentage. */ - public void changeHealth(float percent) { - // TODO: drainRate formula - health += percent; - if (health > 100f) - health = 100f; - else if (health < 0f) - health = 0f; - } + public float getHealthPercent() { return health.getHealth(); } /** - * Returns the current health percentage. + * Sets the health modifiers. + * @param hpDrainRate the HP drain rate + * @param hpMultiplierNormal the normal HP multiplier + * @param hpMultiplierComboEnd the combo-end HP multiplier */ - public float getHealth() { return health; } + public void setHealthModifiers(float hpDrainRate, float hpMultiplierNormal, float hpMultiplierComboEnd) { + health.setModifiers(hpDrainRate, hpMultiplierNormal, hpMultiplierComboEnd); + } /** * Returns false if health is zero. * If "No Fail" or "Auto" mods are active, this will always return true. */ public boolean isAlive() { - return (health > 0f || GameMod.NO_FAIL.isActive() || GameMod.AUTO.isActive() || + return (health.getHealth() > 0f || GameMod.NO_FAIL.isActive() || GameMod.AUTO.isActive() || GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive()); } + /** + * Changes health by a raw value. + * @param value the health value + */ + public void changeHealth(float value) { health.changeHealth(value); } + /** * Changes score by a raw value (not affected by other modifiers). * @param value the score value @@ -1237,18 +1225,7 @@ public void updateDisplays(int delta) { } // health display - if (healthDisplay != health) { - float shift = delta / 15f; - if (healthDisplay < health) { - healthDisplay += shift; - if (healthDisplay > health) - healthDisplay = health; - } else { - healthDisplay -= shift; - if (healthDisplay < health) - healthDisplay = health; - } - } + health.update(delta); // combo burst if (comboBurstIndex > -1 && Options.isComboBurstEnabled()) { @@ -1328,7 +1305,7 @@ private void resetComboStreak() { SoundController.playSound(SoundEffect.COMBOBREAK); combo = 0; if (GameMod.SUDDEN_DEATH.isActive()) - health = 0f; + health.setHealth(0f); } /** @@ -1370,7 +1347,6 @@ public void sendSliderTickResult(int time, int result, float x, float y, HitObje switch (result) { case HIT_SLIDER30: hitValue = 30; - changeHealth(2f); SoundController.playHitSound( hitObject.getEdgeHitSoundType(repeat), hitObject.getSampleSet(repeat), @@ -1378,7 +1354,6 @@ public void sendSliderTickResult(int time, int result, float x, float y, HitObje break; case HIT_SLIDER10: hitValue = 10; - changeHealth(1f); SoundController.playHitSound(HitSound.SLIDERTICK); break; case HIT_MISS: @@ -1392,6 +1367,7 @@ public void sendSliderTickResult(int time, int result, float x, float y, HitObje // calculate score and increment combo streak score += hitValue; incrementComboStreak(); + health.changeHealthForHit(result); if (!Options.isPerfectHitBurstEnabled()) ; // hide perfect hit results @@ -1401,6 +1377,29 @@ public void sendSliderTickResult(int time, int result, float x, float y, HitObje fullObjectCount++; } + /** + * Handles a spinner spin result. + * @param result the hit result (HIT_* constants) + */ + public void sendSpinnerSpinResult(int result) { + int hitValue = 0; + switch (result) { + case HIT_SPINNERSPIN: + hitValue = 100; + SoundController.playSound(SoundEffect.SPINNERSPIN); + break; + case HIT_SPINNERBONUS: + hitValue = 1100; + SoundController.playSound(SoundEffect.SPINNERBONUS); + break; + default: + return; + } + + score += hitValue; + health.changeHealthForHit(result); + } + /** * Returns the score for a hit based on the following score formula: * <p> @@ -1474,11 +1473,9 @@ private int handleHitResult(int time, int result, float x, float y, Color color, switch (result) { case HIT_300: hitValue = 300; - changeHealth(5f); break; case HIT_100: hitValue = 100; - changeHealth(2f); comboEnd |= 1; break; case HIT_50: @@ -1487,7 +1484,6 @@ private int handleHitResult(int time, int result, float x, float y, Color color, break; case HIT_MISS: hitValue = 0; - changeHealth(-10f); comboEnd |= 2; resetComboStreak(); break; @@ -1505,6 +1501,7 @@ private int handleHitResult(int time, int result, float x, float y, Color color, if (!noIncrementCombo) incrementComboStreak(); } + health.changeHealthForHit(result); hitResultCount[result]++; fullObjectCount++; @@ -1512,16 +1509,16 @@ private int handleHitResult(int time, int result, float x, float y, Color color, if (end) { if (comboEnd == 0) { result = HIT_300G; - changeHealth(15f); + health.changeHealthForHit(HIT_300G); hitResultCount[result]++; } else if ((comboEnd & 2) == 0) { if (result == HIT_100) { result = HIT_100K; - changeHealth(10f); + health.changeHealthForHit(HIT_100K); hitResultCount[result]++; } else if (result == HIT_300) { result = HIT_300K; - changeHealth(10f); + health.changeHealthForHit(HIT_300K); hitResultCount[result]++; } } diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 9e85899d..90c5253f 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -226,6 +226,23 @@ public static float lerp(float a, float b, float t) { return a * (1 - t) + b * t; } + /** + * Maps a difficulty value to the given range. + * @param difficulty the difficulty value + * @param min the min + * @param mid the mid + * @param max the max + * @author peppy (ppy/osu-iPhone:OsuFunctions.m) + */ + public static float mapDifficultyRange(float difficulty, float min, float mid, float max) { + if (difficulty > 5f) + return mid + (max - mid) * (difficulty - 5f) / 5f; + else if (difficulty < 5f) + return mid - (mid - min) * (5f - difficulty) / 5f; + else + return mid; + } + /** * Returns true if a game input key is pressed (mouse/keyboard left/right). * @return true if pressed diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapHPDropRateCalculator.java b/src/itdelatrisu/opsu/beatmap/BeatmapHPDropRateCalculator.java new file mode 100644 index 00000000..9e2033b3 --- /dev/null +++ b/src/itdelatrisu/opsu/beatmap/BeatmapHPDropRateCalculator.java @@ -0,0 +1,199 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015, 2016 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 <http://www.gnu.org/licenses/>. + */ + +package itdelatrisu.opsu.beatmap; + +import itdelatrisu.opsu.GameData; +import itdelatrisu.opsu.Utils; +import itdelatrisu.opsu.db.BeatmapDB; + +/** + * osu's HP drop rate algorithm. + * + * @author peppy (ppy/osu-iPhone:OsuFiletype.m) + */ +public class BeatmapHPDropRateCalculator { + /** The beatmap. */ + private final Beatmap beatmap; + + /** The HP drain rate. */ + private final float hpDrainRate; + + /** The overall difficulty. */ + private final float overallDifficulty; + + /** The HP drop rate. */ + private float hpDropRate; + + /** The normal HP multiplier. */ + private float hpMultiplierNormal; + + /** The combo-end HP multiplier. */ + private float hpMultiplierComboEnd; + + /** + * Constructor. Call {@link #calculate()} to run all computations. + * <p> + * If any parts of the beatmap have not yet been loaded (e.g. timing points, + * hit objects), they will be loaded here. + * @param beatmap the beatmap + * @param hpDrainRate the HP drain rate + */ + public BeatmapHPDropRateCalculator(Beatmap beatmap, float hpDrainRate, float overallDifficulty) { + this.beatmap = beatmap; + this.hpDrainRate = hpDrainRate; + this.overallDifficulty = overallDifficulty; + if (beatmap.timingPoints == null) + BeatmapDB.load(beatmap, BeatmapDB.LOAD_ARRAY); + BeatmapParser.parseHitObjects(beatmap); + } + + /** Returns the HP drop rate. */ + public float getHpDropRate() { return hpDropRate; } + + /** Returns the normal HP multiplier. */ + public float getHpMultiplierNormal() { return hpMultiplierNormal; } + + /** Returns the combo-end HP multiplier. */ + public float getHpMultiplierComboEnd() { return hpMultiplierComboEnd; } + + /** Calculates the HP drop rate for the beatmap. */ + public void calculate() { + float lowestHpEver = Utils.mapDifficultyRange(hpDrainRate, 195, 160, 60); + float lowestHpComboEnd = Utils.mapDifficultyRange(hpDrainRate, 198, 170, 80); + float lowestHpEnd = Utils.mapDifficultyRange(hpDrainRate, 198, 180, 80); + float hpRecoveryAvailable = Utils.mapDifficultyRange(hpDrainRate, 8, 4, 0); + int difficultyPreEmpt = (int) Utils.mapDifficultyRange(overallDifficulty, 1800, 1200, 450); + + float testDrop = 0.05f; + Health health = new Health(); + hpMultiplierNormal = hpMultiplierComboEnd = 1.0f; + + while (true) { + health.reset(); + health.setModifiers(hpDrainRate, hpMultiplierNormal, hpMultiplierComboEnd); + double lowestHp = health.getRawHealth(); + int lastTime = beatmap.objects[0].getTime() - difficultyPreEmpt; + int comboTooLowCount = 0; + boolean fail = false; + int timingPointIndex = 0; + float beatLengthBase = 1f, beatLength = 1f; + + for (int i = 0; i < beatmap.objects.length; i++) { + HitObject hitObject = beatmap.objects[i]; + + // breaks + int breakTime = 0; + if (beatmap.breaks != null) { + for (int j = 0; j < beatmap.breaks.size(); j += 2) { + int breakStart = beatmap.breaks.get(j), breakEnd = beatmap.breaks.get(j+1); + if (breakStart >= lastTime && breakEnd <= hitObject.getTime()) { + breakTime = breakEnd - breakStart; + break; + } + } + } + health.changeHealth(-testDrop * (hitObject.getTime() - lastTime - breakTime)); + + // pass beatLength to hit objects + int hitObjectTime = hitObject.getTime(); + while (timingPointIndex < beatmap.timingPoints.size()) { + TimingPoint timingPoint = beatmap.timingPoints.get(timingPointIndex); + if (timingPoint.getTime() > hitObjectTime) + break; + if (!timingPoint.isInherited()) + beatLengthBase = beatLength = timingPoint.getBeatLength(); + else + beatLength = beatLengthBase * timingPoint.getSliderMultiplier(); + timingPointIndex++; + } + + // compute end time + int endTime; + if (hitObject.isCircle()) + endTime = hitObject.getTime(); + else if (hitObject.isSlider()) { + float sliderTime = hitObject.getSliderTime(beatmap.sliderMultiplier, beatLength); + float sliderTimeTotal = sliderTime * hitObject.getRepeatCount(); + endTime = hitObject.getTime() + (int) sliderTimeTotal; + } else + endTime = hitObject.getEndTime(); + lastTime = endTime; + + if (health.getRawHealth() < lowestHp) + lowestHp = health.getRawHealth(); + + if (health.getRawHealth() <= lowestHpEver) { + fail = true; + testDrop *= 0.96f; + break; + } + + health.changeHealth(-testDrop * (endTime - hitObject.getTime())); + + // hit objects + if (hitObject.isSlider()) { + float tickLengthDiv = 100f * beatmap.sliderMultiplier / beatmap.sliderTickRate / (beatLength / beatLengthBase); + int tickCount = (int) Math.ceil(hitObject.getPixelLength() / tickLengthDiv) - 1; + for (int j = 0; j < hitObject.getRepeatCount(); j++) + health.changeHealthForHit(GameData.HIT_SLIDER30); + for (int j = 0; j < tickCount * hitObject.getRepeatCount(); j++) + health.changeHealthForHit(GameData.HIT_SLIDER10); + } else if (hitObject.isSpinner()) { + float spinsPerMinute = 100 + (beatmap.overallDifficulty * 15); + int rotationsNeeded = (int) (spinsPerMinute * (hitObject.getEndTime() - hitObject.getTime()) / 60000f); + for (int j = 0; j < rotationsNeeded; j++) + health.changeHealthForHit(GameData.HIT_SPINNERSPIN); + } + health.changeHealthForHit(GameData.HIT_300); + if (i == beatmap.objects.length - 1 || beatmap.objects[i + 1].isNewCombo()) { + health.changeHealthForHit(GameData.HIT_300G); + if (health.getRawHealth() < lowestHpComboEnd) { + if (++comboTooLowCount > 2) { + fail = true; + hpMultiplierNormal *= 1.03; + hpMultiplierComboEnd *= 1.07; + break; + } + } + } + } + + if (!fail && health.getRawHealth() < lowestHpEnd) { + fail = true; + testDrop *= 0.94f; + hpMultiplierNormal *= 1.01; + hpMultiplierComboEnd *= 1.01; + } + + double recovery = (health.getUncappedRawHealth() - Health.HP_MAX) / beatmap.objects.length; + if (!fail && recovery < hpRecoveryAvailable) { + fail = true; + testDrop *= 0.96; + hpMultiplierNormal *= 1.01; + hpMultiplierComboEnd *= 1.02; + } + + if (fail) + continue; + + hpDropRate = testDrop; + break; + } + } +} diff --git a/src/itdelatrisu/opsu/beatmap/Health.java b/src/itdelatrisu/opsu/beatmap/Health.java new file mode 100644 index 00000000..28f93869 --- /dev/null +++ b/src/itdelatrisu/opsu/beatmap/Health.java @@ -0,0 +1,167 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015, 2016 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 <http://www.gnu.org/licenses/>. + */ + +package itdelatrisu.opsu.beatmap; + +import itdelatrisu.opsu.GameData; +import itdelatrisu.opsu.Utils; + +/** + * Health bar. + * + * @author peppy (ppy/osu-iPhone:OsuAppDelegate.m,OsuFunctions.h) + */ +public class Health { + /** Maximum HP. */ + public static final float HP_MAX = 200f; + + /** HP increase values. */ + private static final float + HP_50 = 0.4f, + HP_100 = 2.2f, + HP_300 = 6f, + HP_100K = 10f, // 100-Katu + HP_300K = 10f, // 300-Katu + HP_300G = 14f, // Geki + HP_SLIDER10 = 3f, + HP_SLIDER30 = 4f, + HP_SPINNERSPIN = 1.7f, + HP_SPINNERBONUS = 2f; + + /** Current health. */ + private float health; + + /** Displayed health (for animation, slightly behind health). */ + private float healthDisplay; + + /** Uncapped health. */ + private float healthUncapped; + + /** The HP drain rate (0:easy ~ 10:hard) */ + private float hpDrainRate; + + /** The normal HP multiplier. */ + private float hpMultiplierNormal; + + /** The combo-end HP multiplier. */ + private float hpMultiplierComboEnd; + + /** Constructor. */ + public Health() { + reset(); + } + + /** + * Sets the health modifiers. + * @param hpDrainRate the HP drain rate + * @param hpMultiplierNormal the normal HP multiplier + * @param hpMultiplierComboEnd the combo-end HP multiplier + */ + public void setModifiers(float hpDrainRate, float hpMultiplierNormal, float hpMultiplierComboEnd) { + this.hpDrainRate = hpDrainRate; + this.hpMultiplierNormal = hpMultiplierNormal; + this.hpMultiplierComboEnd = hpMultiplierComboEnd; + } + + /** + * Updates displayed health based on a delta value. + * @param delta the delta interval since the last call + */ + public void update(int delta) { + float multiplier = delta / 32f; + if (healthDisplay < health) + healthDisplay = Utils.clamp(healthDisplay + (health - healthDisplay) * multiplier, 0f, health); + else if (healthDisplay > health) { + if (health < 10f) + multiplier *= 10f; + healthDisplay = Utils.clamp(healthDisplay - (healthDisplay - health) * multiplier, health, HP_MAX); + } + } + + /** Returns the current health percentage. */ + public float getHealth() { return health / HP_MAX * 100f; } + + /** Returns the displayed health percentage. */ + public float getHealthDisplay() { return healthDisplay / HP_MAX * 100f; } + + /** Returns the current health value. */ + public float getRawHealth() { return health; } + + /** Returns the current uncapped health value. */ + public float getUncappedRawHealth() { return healthUncapped; } + + /** Sets the current health value. */ + public void setHealth(float value) { health = healthUncapped = value; } + + /** Changes the current health by the given value. */ + public void changeHealth(float value) { + health = Utils.clamp(health + value, 0f, HP_MAX); + healthUncapped += value; + } + + /** + * Changes the current health value based on a hit. + * @param type the hit type (HIT_* constants) + */ + public void changeHealthForHit(int type) { + switch (type) { + case GameData.HIT_MISS: + changeHealth(Utils.mapDifficultyRange(hpDrainRate, -6, -25, -40)); + break; + case GameData.HIT_50: + changeHealth(hpMultiplierNormal * Utils.mapDifficultyRange(hpDrainRate, HP_50 * 8, HP_50, HP_50)); + break; + case GameData.HIT_100: + changeHealth(hpMultiplierNormal * Utils.mapDifficultyRange(hpDrainRate, HP_100 * 8, HP_100, HP_100)); + break; + case GameData.HIT_300: + changeHealth(hpMultiplierNormal * HP_300); + break; + case GameData.HIT_100K: + changeHealth(hpMultiplierComboEnd * HP_100K); + break; + case GameData.HIT_300K: + changeHealth(hpMultiplierComboEnd * HP_300K); + break; + case GameData.HIT_300G: + changeHealth(hpMultiplierComboEnd * HP_300G); + break; + case GameData.HIT_SLIDER10: + changeHealth(hpMultiplierNormal * HP_SLIDER10); + break; + case GameData.HIT_SLIDER30: + changeHealth(hpMultiplierNormal * HP_SLIDER30); + break; + case GameData.HIT_SPINNERSPIN: + changeHealth(hpMultiplierNormal * HP_SPINNERSPIN); + break; + case GameData.HIT_SPINNERBONUS: + changeHealth(hpMultiplierNormal * HP_SPINNERBONUS); + break; + default: + break; + } + } + + /** Resets health. */ + public void reset() { + health = healthDisplay = healthUncapped = HP_MAX; + hpDrainRate = 5f; + hpMultiplierNormal = hpMultiplierComboEnd = 1f; + } +} diff --git a/src/itdelatrisu/opsu/objects/Spinner.java b/src/itdelatrisu/opsu/objects/Spinner.java index 9eb12d62..c625b5d7 100644 --- a/src/itdelatrisu/opsu/objects/Spinner.java +++ b/src/itdelatrisu/opsu/objects/Spinner.java @@ -336,8 +336,6 @@ else if (angleDiff > Math.PI) drawnRPM = (int) (Math.abs(rotationPerSec * 60)); rotate(rotationAngle); - if (Math.abs(rotationAngle) > 0.00001f) - data.changeHealth(DELTA_UPDATE_TIME * GameData.HP_DRAIN_MULTIPLIER); } //TODO may need to update 1 more time when the spinner ends? @@ -380,21 +378,12 @@ private void rotate(float angle) { // added one whole rotation... if (Math.floor(newRotations) > rotations) { - //TODO seems to give 1100 points per spin but also an extra 100 for some spinners - if (newRotations > rotationsNeeded) { // extra rotations - data.changeScore(1000); - SoundController.playSound(SoundEffect.SPINNERBONUS); - } - data.changeScore(100); - SoundController.playSound(SoundEffect.SPINNERSPIN); + if (newRotations > rotationsNeeded) // extra rotations + data.sendSpinnerSpinResult(GameData.HIT_SPINNERBONUS); + else + data.sendSpinnerSpinResult(GameData.HIT_SPINNERSPIN); } - // extra 100 for some spinners (mostly wrong) -// if (Math.floor(newRotations + 0.5f) > rotations + 0.5f) { -// if (newRotations + 0.5f > rotationsNeeded) // extra rotations -// data.changeScore(100); -// } - rotations = newRotations; } diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 21ff371f..04981adb 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -31,6 +31,7 @@ import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.beatmap.Beatmap; +import itdelatrisu.opsu.beatmap.BeatmapHPDropRateCalculator; import itdelatrisu.opsu.beatmap.BeatmapParser; import itdelatrisu.opsu.beatmap.HitObject; import itdelatrisu.opsu.beatmap.TimingPoint; @@ -291,6 +292,12 @@ public enum Restart { /** Timer after game has finished, before changing states. */ private AnimatedValue gameFinishedTimer = new AnimatedValue(2500, 0, 1, AnimationEquation.LINEAR); + /** The HP drop rate. */ + private float hpDropRate = 0.05f; + + /** The last track position. */ + private int lastTrackPosition = 0; + /** Music position bar background colors. */ private static final Color MUSICBAR_NORMAL = new Color(12, 9, 10, 0.25f), @@ -506,7 +513,7 @@ else if (breakIndex > 1) { trackPosition - breakTime > 2000 && trackPosition - breakTime < 5000) { // show break start - if (data.getHealth() >= 50) { + if (data.getHealthPercent() >= 50) { GameImage.SECTION_PASS.getImage().drawCentered(width / 2f, height / 2f); if (!breakSound) { SoundController.playSound(SoundEffect.SECTIONPASS); @@ -765,8 +772,8 @@ else if (!container.hasFocus()) { // "Easy" mod: multiple "lives" if (GameMod.EASY.isActive() && deathTime > -1) { - if (data.getHealth() < 99f) { - data.changeHealth(delta / 10f); + if (data.getHealthPercent() < 99f) { + data.changeHealth(delta / 5f); data.updateDisplays(delta); return; } @@ -826,6 +833,8 @@ else if (!gameFinished) { } } + lastTrackPosition = trackPosition; + // update in-game scoreboard if (previousScores != null && trackPosition > firstObjectTime) { // show scoreboard if selected, and always in break @@ -944,7 +953,9 @@ else if (replayFrames != null) { } // drain health - data.changeHealth(delta * -1 * GameData.HP_DRAIN_MULTIPLIER); + if (lastTrackPosition > 0) + data.changeHealth((trackPosition - lastTrackPosition) * -1 * hpDropRate); + if (!data.isAlive()) { // "Easy" mod if (GameMod.EASY.isActive() && !GameMod.SUDDEN_DEATH.isActive()) { @@ -1099,6 +1110,7 @@ else if (key == Options.getGameKeyRight()) ; objectIndex--; lastReplayTime = beatmap.objects[objectIndex].getTime(); + lastTrackPosition = checkpoint; } catch (SlickException e) { ErrorHandler.error("Failed to load checkpoint.", e, false); } @@ -1170,6 +1182,7 @@ else if (Options.isReplaySeekingEnabled() && !GameMod.AUTO.isActive() && musicPo SoundController.mute(true); // mute sounds while seeking float pos = (y - musicBarY) / musicBarHeight * beatmap.endTime; MusicController.setPosition((int) pos); + lastTrackPosition = (int) pos; isSeeking = true; } return; @@ -1665,6 +1678,7 @@ public void resetGameData() { scoreboardStarStream.clear(); gameFinished = false; gameFinishedTimer.setTime(0); + lastTrackPosition = 0; System.gc(); } @@ -1769,19 +1783,17 @@ private void setMapModifiers() { // overallDifficulty (hit result time offsets) hitResultOffset = new int[GameData.HIT_MAX]; - hitResultOffset[GameData.HIT_300] = (int) (79.5f - (overallDifficulty * 6)); - hitResultOffset[GameData.HIT_100] = (int) (139.5f - (overallDifficulty * 8)); - hitResultOffset[GameData.HIT_50] = (int) (199.5f - (overallDifficulty * 10)); + hitResultOffset[GameData.HIT_300] = (int) Utils.mapDifficultyRange(overallDifficulty, 80, 50, 20); + hitResultOffset[GameData.HIT_100] = (int) Utils.mapDifficultyRange(overallDifficulty, 140, 100, 60); + hitResultOffset[GameData.HIT_50] = (int) Utils.mapDifficultyRange(overallDifficulty, 200, 150, 100); hitResultOffset[GameData.HIT_MISS] = (int) (500 - (overallDifficulty * 10)); - //final float mult = 0.608f; - //hitResultOffset[GameData.HIT_300] = (int) ((128 - (overallDifficulty * 9.6)) * mult); - //hitResultOffset[GameData.HIT_100] = (int) ((224 - (overallDifficulty * 12.8)) * mult); - //hitResultOffset[GameData.HIT_50] = (int) ((320 - (overallDifficulty * 16)) * mult); - //hitResultOffset[GameData.HIT_MISS] = (int) ((1000 - (overallDifficulty * 10)) * mult); data.setHitResultOffset(hitResultOffset); // HPDrainRate (health change) - data.setDrainRate(HPDrainRate); + BeatmapHPDropRateCalculator hpCalc = new BeatmapHPDropRateCalculator(beatmap, HPDrainRate, overallDifficulty); + hpCalc.calculate(); + hpDropRate = hpCalc.getHpDropRate(); + data.setHealthModifiers(HPDrainRate, hpCalc.getHpMultiplierNormal(), hpCalc.getHpMultiplierComboEnd()); // difficulty multiplier (scoring) data.calculateDifficultyMultiplier(beatmap.HPDrainRate, beatmap.circleSize, beatmap.overallDifficulty);