/*
* opsu! - an open-source osu! client
* Copyright (C) 2014-2017 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 approachTime = (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() - approachTime;
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;
}
}
}