opsu-dance/src/itdelatrisu/opsu/GameScore.java
Jeffrey Han 56123363b3 Implemented dynamic main menu backgrounds.
- This can be switched off in the configuration file.
- Credits: https://osu.ppy.sh/forum/t/98954

Other changes:
- Removed OsuFile setters/getters from Game state, replaced with a 'getOsuFile()' method in MusicController (a more logical location).

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
2014-07-02 03:02:11 -04:00

857 lines
25 KiB
Java

/*
* opsu! - an open-source osu! client
* Copyright (C) 2014 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;
import itdelatrisu.opsu.states.Options;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import org.newdawn.slick.Color;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.util.Log;
/**
* Holds score data and renders all score-related elements.
*/
public class GameScore {
/**
* Letter grades.
*/
public static final int
GRADE_SS = 0,
GRADE_SSH = 1, // silver
GRADE_S = 2,
GRADE_SH = 3, // silver
GRADE_A = 4,
GRADE_B = 5,
GRADE_C = 6,
GRADE_D = 7,
GRADE_MAX = 8; // not a grade
/**
* Hit result types.
*/
public static final int
HIT_MISS = 0,
HIT_50 = 1,
HIT_100 = 2,
HIT_300 = 3,
HIT_100K = 4, // 100-Katu
HIT_300K = 5, // 300-Katu
HIT_300G = 6, // Geki
HIT_SLIDER10 = 7,
HIT_SLIDER30 = 8,
HIT_MAX = 9; // not a hit result
/**
* Hit result-related images (indexed by HIT_* constants).
*/
private Image[] hitResults;
/**
* Counts of each hit result so far.
*/
private int[] hitResultCount;
/**
* Total number of hit objects so far, not including Katu/Geki (for calculating grade).
*/
private int objectCount;
/**
* Total objects including slider hits/ticks (for determining Full Combo status).
*/
private int fullObjectCount;
/**
* The current combo streak.
*/
private int combo;
/**
* The max combo streak obtained.
*/
private int comboMax;
/**
* Hit result types accumulated this streak (bitmask), for Katu/Geki status.
* <ul>
* <li>&1: 100
* <li>&2: 50/Miss
* </ul>
*/
private byte comboEnd;
/**
* Combo burst images.
*/
private Image[] comboBurstImages;
/**
* Index of the current combo burst image.
*/
private int comboBurstIndex;
/**
* Alpha level of the current combo burst image (for fade out).
*/
private float comboBurstAlpha;
/**
* Current x coordinate of the combo burst image (for sliding animation).
*/
private int comboBurstX;
/**
* List of hit result objects associated with hit objects.
*/
private LinkedList<OsuHitObjectResult> hitResultList;
/**
* Hit result helper class.
*/
private class OsuHitObjectResult {
public int time; // object start time
public int result; // hit result
public float x, y; // object coordinates
public Color color; // combo color
public float alpha = 1f; // alpha level (for fade out)
/**
* Constructor.
* @param time the result's starting track position
* @param result the hit result (HIT_* constants)
* @param x the center x coordinate
* @param y the center y coordinate
* @param color the color of the hit object
*/
public OsuHitObjectResult(int time, int result, float x, float y, Color color) {
this.time = time;
this.result = result;
this.x = x;
this.y = y;
this.color = color;
}
}
/**
* Current game score.
*/
private long score;
/**
* Current health bar percentage.
*/
private float health;
/**
* Beatmap HPDrainRate value. (0:easy ~ 10:hard)
*/
private float drainRate = 5f;
/**
* Beatmap OverallDifficulty value. (0:easy ~ 10:hard)
*/
private float difficulty = 5f;
/**
* Scorebar-related images.
*/
private Image
bgImage, // background (always rendered)
colourImage, // health bar (cropped)
kiImage, // end image (50~100% health)
kiDangerImage, // end image (25~50% health)
kiDanger2Image; // end image (0~25% health)
/**
* Ranking screen images.
*/
private Image
rankingPanel, // panel to display text in
perfectImage, // display if full combo
rankingImage, // styled text "Ranking"
comboImage, // styled text "Combo"
accuracyImage; // styled text "Accuracy"
/**
* Default text symbol images.
*/
private Image[] defaultSymbols;
/**
* Score text symbol images.
*/
private HashMap<Character, Image> scoreSymbols;
/**
* Letter grade images (large and small sizes).
*/
private Image[] gradesLarge, gradesSmall;
/**
* Lighting effects, displayed behind hit object results (optional).
*/
private Image lighting, lighting1;
/**
* Container dimensions.
*/
private int width, height;
/**
* Constructor.
* @param width container width
* @param height container height
*/
public GameScore(int width, int height) {
this.width = width;
this.height = height;
hitResults = new Image[HIT_MAX];
defaultSymbols = new Image[10];
scoreSymbols = new HashMap<Character, Image>(14);
gradesLarge = new Image[GRADE_MAX];
gradesSmall = new Image[GRADE_MAX];
comboBurstImages = new Image[4];
clear();
try {
initializeImages();
} catch (Exception e) {
Log.error("Failed to initialize images.", e);
}
}
/**
* Clears all data and re-initializes object.
*/
public void clear() {
score = 0;
health = 100f;
hitResultCount = new int[HIT_MAX];
hitResultList = new LinkedList<OsuHitObjectResult>();
objectCount = 0;
fullObjectCount = 0;
combo = 0;
comboMax = 0;
comboEnd = 0;
comboBurstIndex = -1;
}
/**
* Initialize all images tied to this object.
* @throws SlickException
*/
private void initializeImages() throws SlickException {
// scorebar
setScorebarImage(
new Image("scorebar-bg.png"),
new Image("scorebar-colour.png"),
new Image("scorebar-ki.png"),
new Image("scorebar-kidanger.png"),
new Image("scorebar-kidanger2.png")
);
// text symbol images
for (int i = 0; i <= 9; i++) {
defaultSymbols[i] = new Image(String.format("default-%d.png", i));
scoreSymbols.put(Character.forDigit(i, 10), new Image(String.format("score-%d.png", i)));
}
scoreSymbols.put(',', new Image("score-comma.png"));
scoreSymbols.put('.', new Image("score-dot.png"));
scoreSymbols.put('%', new Image("score-percent.png"));
scoreSymbols.put('x', new Image("score-x.png"));
// hit result images
hitResults[HIT_MISS] = new Image("hit0.png");
hitResults[HIT_50] = new Image("hit50.png");
hitResults[HIT_100] = new Image("hit100.png");
hitResults[HIT_300] = new Image("hit300.png");
hitResults[HIT_100K] = new Image("hit100k.png");
hitResults[HIT_300K] = new Image("hit300k.png");
hitResults[HIT_300G] = new Image("hit300g.png");
hitResults[HIT_SLIDER10] = new Image("sliderpoint10.png");
hitResults[HIT_SLIDER30] = new Image("sliderpoint30.png");
// combo burst images
for (int i = 0; i <= 3; i++)
comboBurstImages[i] = new Image(String.format("comboburst-%d.png", i));
// lighting image
try {
lighting = new Image("lighting.png");
lighting1 = new Image("lighting1.png");
} catch (Exception e) {
// optional
}
// letter grade images
String[] grades = { "X", "XH", "S", "SH", "A", "B", "C", "D" };
for (int i = 0; i < grades.length; i++) {
gradesLarge[i] = new Image(String.format("ranking-%s.png", grades[i]));
gradesSmall[i] = new Image(String.format("ranking-%s-small.png", grades[i]));
}
// ranking screen elements
setRankingImage(
new Image("ranking-panel.png"),
new Image("ranking-perfect.png"),
new Image("ranking-title.png"),
new Image("ranking-maxcombo.png"),
new Image("ranking-accuracy.png")
);
}
/**
* Sets a background, health bar, and end image.
* @param bgImage background image
* @param colourImage health bar image
* @param kiImage end image
*/
public void setScorebarImage(Image bg, Image colour,
Image ki, Image kiDanger, Image kiDanger2) {
int bgWidth = width / 2;
this.bgImage = bg.getScaledCopy(bgWidth, bg.getHeight());
this.colourImage = colour.getScaledCopy(bgWidth, colour.getHeight());
this.kiImage = ki;
this.kiDangerImage = kiDanger;
this.kiDanger2Image = kiDanger2;
}
/**
* Sets a ranking panel, full combo, and ranking/combo/accuracy text image.
* @param rankingPanel ranking panel image
* @param perfectImage full combo image
* @param rankingImage styled text "Ranking"
* @param comboImage styled text "Combo"
* @param accuracyImage styled text "Accuracy"
*/
public void setRankingImage(Image rankingPanel, Image perfectImage,
Image rankingImage, Image comboImage, Image accuracyImage) {
this.rankingPanel = rankingPanel.getScaledCopy((height * 0.63f) / rankingPanel.getHeight());
this.perfectImage = perfectImage.getScaledCopy((height * 0.16f) / perfectImage.getHeight());
this.rankingImage = rankingImage.getScaledCopy((height * 0.15f) / rankingImage.getHeight());
this.comboImage = comboImage.getScaledCopy((height * 0.05f) / comboImage.getHeight());
this.accuracyImage = accuracyImage.getScaledCopy((height * 0.05f) / accuracyImage.getHeight());
}
/**
* Returns a default/score text symbol image for a character.
*/
public Image getDefaultSymbolImage(int i) { return defaultSymbols[i]; }
public Image getScoreSymbolImage(char c) { return scoreSymbols.get(c); }
/**
* Sets or returns the health drain rate.
*/
public void setDrainRate(float drainRate) { this.drainRate = drainRate; }
public float getDrainRate() { return drainRate; }
/**
* Sets or returns the difficulty.
*/
public void setDifficulty(float difficulty) { this.difficulty = difficulty; }
public float getDifficulty() { return difficulty; }
/**
* Draws a number with defaultSymbols.
* @param n the number to draw
* @param x the center x coordinate
* @param y the center y coordinate
* @param scale the scale to apply
*/
public void drawSymbolNumber(int n, float x, float y, float scale) {
int length = (int) (Math.log10(n) + 1);
float digitWidth = getDefaultSymbolImage(0).getWidth() * scale;
float cx = x + ((length - 1) * (digitWidth / 2));
for (int i = 0; i < length; i++) {
getDefaultSymbolImage(n % 10).getScaledCopy(scale).drawCentered(cx, y);
cx -= digitWidth;
n /= 10;
}
}
/**
* Draws a string of scoreSymbols.
* @param str the string to draw
* @param x the starting x coordinate
* @param y the y coordinate
* @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) {
char[] c = str.toCharArray();
int cx = x;
if (rightAlign) {
for (int i = c.length - 1; i >= 0; i--) {
Image digit = getScoreSymbolImage(c[i]);
if (scale != 1.0f)
digit = digit.getScaledCopy(scale);
cx -= digit.getWidth();
digit.draw(cx, y);
}
} else {
for (int i = 0; i < c.length; i++) {
Image digit = getScoreSymbolImage(c[i]);
if (scale != 1.0f)
digit = digit.getScaledCopy(scale);
digit.draw(cx, y);
cx += digit.getWidth();
}
}
}
/**
* Draws game elements: scorebar, score, score percentage, combo count, and combo burst.
* @param g the graphics context
* @param mapLength the length of the beatmap (in ms)
* @param breakPeriod if true, will not draw scorebar and combo elements, and will draw grade
* @param firstObject true if the first hit object's start time has not yet passed
*/
public void drawGameElements(Graphics g, int mapLength, boolean breakPeriod, boolean firstObject) {
// score
drawSymbolString(String.format("%08d", score),
width - 2, 0, 1.0f, true);
// score percentage
String scorePercentage = String.format("%02.2f%%", getScorePercent());
drawSymbolString(scorePercentage, width - 2, getScoreSymbolImage('0').getHeight(), 0.75f, true);
// map progress circle
g.setAntiAlias(true);
g.setLineWidth(2f);
g.setColor(Color.white);
int circleX = width - (getScoreSymbolImage('0').getWidth() * scorePercentage.length());
int circleY = getScoreSymbolImage('0').getHeight();
float circleDiameter = getScoreSymbolImage('0').getHeight() * 0.75f;
g.drawOval(circleX, circleY, circleDiameter, circleDiameter);
int firstObjectTime = MusicController.getOsuFile().objects[0].time;
int trackPosition = MusicController.getPosition();
if (trackPosition > firstObjectTime) {
g.fillArc(circleX, circleY, circleDiameter, circleDiameter,
-90, -90 + (int) (360f * (trackPosition - firstObjectTime) / mapLength)
);
}
if (!breakPeriod) {
// scorebar
float healthRatio = health / 100f;
if (firstObject) { // gradually move ki before map begins
if (firstObjectTime >= 1500 && trackPosition < firstObjectTime - 500)
healthRatio = (float) trackPosition / (firstObjectTime - 500);
}
bgImage.draw(0, 0);
Image colourCropped = colourImage.getSubImage(0, 0, (int) (colourImage.getWidth() * healthRatio), colourImage.getHeight());
colourCropped.draw(0, bgImage.getHeight() / 4f);
if (health >= 50f)
kiImage.drawCentered(colourCropped.getWidth(), kiImage.getHeight() / 2f);
else if (health >= 25f)
kiDangerImage.drawCentered(colourCropped.getWidth(), kiDangerImage.getHeight() / 2f);
else
kiDanger2Image.drawCentered(colourCropped.getWidth(), kiDanger2Image.getHeight() / 2f);
// combo burst
if (comboBurstIndex != -1 && comboBurstAlpha > 0f) {
Image comboBurst = comboBurstImages[comboBurstIndex];
comboBurst.setAlpha(comboBurstAlpha);
comboBurstImages[comboBurstIndex].draw(comboBurstX, height - comboBurst.getHeight());
}
// combo count
if (combo > 0) // 0 isn't a combo
drawSymbolString(String.format("%dx", combo), 10, height - 10 - getScoreSymbolImage('0').getHeight(), 1.0f, false);
} else {
// grade
Image grade = gradesSmall[getGrade()];
float gradeScale = circleY * 0.75f / grade.getHeight();
gradesSmall[getGrade()].getScaledCopy(gradeScale).draw(
circleX - grade.getWidth(), circleY
);
}
}
/**
* Draws ranking elements: score, results, ranking.
* @param g the graphics context
* @param width the width of the container
* @param height the height of the container
*/
public void drawRankingElements(Graphics g, int width, int height) {
// grade
Image grade = gradesLarge[getGrade()];
float gradeScale = (height * 0.5f) / grade.getHeight();
grade = grade.getScaledCopy(gradeScale);
grade.draw(width - grade.getWidth(), height * 0.09f);
// header & "Ranking" text
float rankingHeight = (rankingImage.getHeight() * 0.75f) + 3;
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.fillRect(0, 0, width, rankingHeight);
rankingImage.draw((width * 0.97f) - rankingImage.getWidth(), 0);
// ranking panel
int rankingPanelWidth = rankingPanel.getWidth();
int rankingPanelHeight = rankingPanel.getHeight();
rankingPanel.draw(0, rankingHeight - (rankingHeight / 10f));
float symbolTextScale = (height / 15f) / getScoreSymbolImage('0').getHeight();
float rankResultScale = (height * 0.03f) / hitResults[HIT_300].getHeight();
// score
drawSymbolString((score / 100000000 == 0) ? String.format("%08d", score) : Long.toString(score),
(int) (width * 0.18f), height / 6, symbolTextScale, false);
// result counts
float resultInitialX = rankingPanelWidth * 0.20f;
float resultInitialY = rankingHeight + (rankingPanelHeight * 0.27f) + (rankingHeight / 10f);
float resultHitInitialX = rankingPanelWidth * 0.05f;
float resultHitInitialY = resultInitialY + (getScoreSymbolImage('0').getHeight() * symbolTextScale);
float resultOffsetX = rankingPanelWidth / 2f;
float resultOffsetY = rankingPanelHeight * 0.2f;
int[] rankDrawOrder = { HIT_300, HIT_300G, HIT_100, HIT_100K, HIT_50, HIT_MISS };
int[] rankResultOrder = {
hitResultCount[HIT_300], hitResultCount[HIT_300G],
hitResultCount[HIT_100], hitResultCount[HIT_100K] + hitResultCount[HIT_300K],
hitResultCount[HIT_50], hitResultCount[HIT_MISS]
};
for (int i = 0; i < rankDrawOrder.length; i += 2) {
hitResults[rankDrawOrder[i]].getScaledCopy(rankResultScale).draw(
resultHitInitialX, resultHitInitialY - (hitResults[rankDrawOrder[i]].getHeight() * rankResultScale) + (resultOffsetY * (i / 2)));
hitResults[rankDrawOrder[i+1]].getScaledCopy(rankResultScale).draw(
resultHitInitialX + resultOffsetX, resultHitInitialY - (hitResults[rankDrawOrder[i]].getHeight() * rankResultScale) + (resultOffsetY * (i / 2)));
drawSymbolString(String.format("%dx", rankResultOrder[i]),
(int) resultInitialX, (int) (resultInitialY + (resultOffsetY * (i / 2))), symbolTextScale, false);
drawSymbolString(String.format("%dx", rankResultOrder[i+1]),
(int) (resultInitialX + resultOffsetX), (int) (resultInitialY + (resultOffsetY * (i / 2))), symbolTextScale, false);
}
// combo and accuracy
float textY = rankingHeight + (rankingPanelHeight * 0.87f) - (rankingHeight / 10f);
float numbersX = comboImage.getWidth() * .07f;
float numbersY = textY + comboImage.getHeight() * 0.7f;
comboImage.draw(width * 0.01f, textY);
accuracyImage.draw(rankingPanelWidth / 2f, textY);
drawSymbolString(String.format("%dx", comboMax),
(int) (width * 0.01f + numbersX), (int) numbersY, symbolTextScale, false);
drawSymbolString(String.format("%02.2f%%", getScorePercent()),
(int) (rankingPanelWidth / 2f + numbersX), (int) numbersY, symbolTextScale, false);
// full combo
if (combo == fullObjectCount)
perfectImage.draw(width * 0.08f, (height * 0.99f) - perfectImage.getHeight());
}
/**
* Draws stored hit results and removes them from the list as necessary.
* @param trackPosition the current track position
*/
public void drawHitResults(int trackPosition) {
int fadeDelay = 500;
Iterator<OsuHitObjectResult> iter = hitResultList.iterator();
while (iter.hasNext()) {
OsuHitObjectResult hitResult = iter.next();
if (hitResult.time + fadeDelay > trackPosition) {
hitResults[hitResult.result].setAlpha(hitResult.alpha);
hitResult.alpha = 1 - ((float) (trackPosition - hitResult.time) / fadeDelay);
hitResults[hitResult.result].drawCentered(hitResult.x, hitResult.y);
// hit lighting
if (Options.isHitLightingEnabled() && lighting != null &&
hitResult.result != HIT_MISS && hitResult.result != HIT_SLIDER30 && hitResult.result != HIT_SLIDER10) {
float scale = 1f + ((trackPosition - hitResult.time) / (float) fadeDelay);
Image scaledLighting = lighting.getScaledCopy(scale);
scaledLighting.draw(hitResult.x - (scaledLighting.getWidth() / 2f),
hitResult.y - (scaledLighting.getHeight() / 2f),
hitResult.color);
if (lighting1 != null) {
Image scaledLighting1 = lighting1.getScaledCopy(scale);
scaledLighting1.draw(hitResult.x - (scaledLighting1.getWidth() / 2f),
hitResult.y - (scaledLighting1.getHeight() / 2f),
hitResult.color);
}
}
} else
iter.remove();
}
}
/**
* Changes health by a given percentage, modified by drainRate.
*/
public void changeHealth(float percent) {
// TODO: drainRate formula
health += percent;
if (health > 100f)
health = 100f;
if (health < 0f)
health = 0f;
}
/**
* Returns health percentage.
*/
public float getHealth() { return health; }
/**
* 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 ||
Options.isModActive(Options.MOD_NO_FAIL) ||
Options.isModActive(Options.MOD_AUTO));
}
/**
* Changes score by a raw value (not affected by other modifiers).
*/
public void changeScore(int value) { score += value; }
/**
* Returns score percentage (raw score only).
*/
private float getScorePercent() {
float percent = 0;
if (objectCount > 0)
percent = ((hitResultCount[HIT_50] * 50) + (hitResultCount[HIT_100] * 100)
+ (hitResultCount[HIT_300] * 300)) / (objectCount * 300f) * 100f;
return percent;
}
/**
* Returns (current) letter grade.
*/
private int getGrade() {
if (objectCount < 1) // avoid division by zero
return GRADE_D;
// TODO: silvers
float percent = getScorePercent();
float hit300ratio = hitResultCount[HIT_300] * 100f / objectCount;
float hit50ratio = hitResultCount[HIT_50] * 100f / objectCount;
boolean noMiss = (hitResultCount[HIT_MISS] == 0);
if (percent >= 100f)
return GRADE_SS;
else if (hit300ratio >= 90f && hit50ratio < 1.0f && noMiss)
return GRADE_S;
else if ((hit300ratio >= 80f && noMiss) || hit300ratio >= 90f)
return GRADE_A;
else if ((hit300ratio >= 70f && noMiss) || hit300ratio >= 80f)
return GRADE_B;
else if (hit300ratio >= 60f)
return GRADE_C;
else
return GRADE_D;
}
/**
* Updates combo burst data based on a delta value.
*/
public void updateComboBurst(int delta) {
if (comboBurstIndex > -1 && Options.isComboBurstEnabled()) {
int leftX = 0;
int rightX = width - comboBurstImages[comboBurstIndex].getWidth();
if (comboBurstX < leftX) {
comboBurstX += (delta / 2f);
if (comboBurstX > leftX)
comboBurstX = leftX;
} else if (comboBurstX > rightX) {
comboBurstX -= (delta / 2f);
if (comboBurstX < rightX)
comboBurstX = rightX;
} else if (comboBurstAlpha > 0f) {
comboBurstAlpha -= (delta / 1200f);
if (comboBurstAlpha < 0f)
comboBurstAlpha = 0f;
}
}
}
/**
* Increases the combo streak by one.
*/
private void incrementComboStreak() {
combo++;
if (combo > comboMax)
comboMax = combo;
// combo bursts (at 30, 60, 100+50x)
if (Options.isComboBurstEnabled() &&
(combo == 30 || combo == 60 || (combo >= 100 && combo % 50 == 0))) {
if (combo == 30)
comboBurstIndex = 0;
else
comboBurstIndex = (comboBurstIndex + 1) % comboBurstImages.length;
comboBurstAlpha = 0.8f;
if ((comboBurstIndex % 2) == 0)
comboBurstX = width;
else
comboBurstX = comboBurstImages[0].getWidth() * -1;
}
}
/**
* Resets the combo streak to zero.
*/
private void resetComboStreak() {
if (combo >= 20)
SoundController.playSound(SoundController.SOUND_COMBOBREAK);
combo = 0;
if (Options.isModActive(Options.MOD_SUDDEN_DEATH))
health = 0f;
}
/**
* Handles a slider tick result.
* @param time the tick start time
* @param result the hit result (HIT_* constants)
* @param x the x coordinate
* @param y the y coordinate
* @param hitSound the object's hit sound
*/
public void sliderTickResult(int time, int result, float x, float y, byte hitSound) {
int hitValue = 0;
switch (result) {
case HIT_SLIDER30:
hitValue = 30;
incrementComboStreak();
changeHealth(1f);
SoundController.playHitSound(hitSound);
break;
case HIT_SLIDER10:
hitValue = 10;
incrementComboStreak();
SoundController.playHitSound(SoundController.HIT_SLIDERTICK);
break;
case HIT_MISS:
resetComboStreak();
break;
default:
return;
}
fullObjectCount++;
if (hitValue > 0) {
score += hitValue;
hitResultList.add(new OsuHitObjectResult(time, result, x, y, null));
}
}
/**
* Handles a hit result.
* @param time the object start time
* @param result the hit result (HIT_* constants)
* @param x the x coordinate
* @param y the y coordinate
* @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
*/
public void hitResult(int time, int result, float x, float y, Color color,
boolean end, byte hitSound) {
int hitValue = 0;
switch (result) {
case HIT_300:
hitValue = 300;
changeHealth(5f);
objectCount++;
break;
case HIT_100:
hitValue = 100;
changeHealth(2f);
comboEnd |= 1;
objectCount++;
break;
case HIT_50:
hitValue = 50;
comboEnd |= 2;
objectCount++;
break;
case HIT_MISS:
hitValue = 0;
changeHealth(-10f);
comboEnd |= 2;
resetComboStreak();
objectCount++;
break;
default:
return;
}
if (hitValue > 0) {
SoundController.playHitSound(hitSound);
// game mod score multipliers
float modMultiplier = 1f;
if (Options.isModActive(Options.MOD_NO_FAIL))
modMultiplier *= 0.5f;
if (Options.isModActive(Options.MOD_HARD_ROCK))
modMultiplier *= 1.06f;
if (Options.isModActive(Options.MOD_SPUN_OUT))
modMultiplier *= 0.9f;
// not implemented:
// EASY (0.5x), HALF_TIME (0.3x),
// DOUBLE_TIME (1.12x), HIDDEN (1.06x), FLASHLIGHT (1.12x)
/**
* [SCORE FORMULA]
* Score = Hit Value + Hit Value * (Combo * Difficulty * Mod) / 25
* - Hit Value: hit result (50, 100, 300), slider ticks, spinner bonus
* - Combo: combo before this hit - 1 (minimum 0)
* - Difficulty: the beatmap difficulty
* - Mod: mod multipliers
*/
score += (hitValue + (hitValue * (Math.max(combo - 1, 0) * difficulty * modMultiplier) / 25));
incrementComboStreak();
}
hitResultCount[result]++;
fullObjectCount++;
// last element in combo: check for Geki/Katu
if (end) {
if (comboEnd == 0) {
result = HIT_300G;
changeHealth(15f);
hitResultCount[result]++;
} else if ((comboEnd & 2) == 0) {
if (result == HIT_100) {
result = HIT_100K;
changeHealth(10f);
hitResultCount[result]++;
} else if (result == HIT_300) {
result = HIT_300K;
changeHealth(10f);
hitResultCount[result]++;
}
}
comboEnd = 0;
}
hitResultList.add(new OsuHitObjectResult(time, result, x, y, color));
}
}