/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 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 .
*/
package itdelatrisu.opsu;
import itdelatrisu.opsu.audio.HitSound;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.replay.ReplayFrame;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.io.File;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.LinkedBlockingDeque;
import org.newdawn.slick.Animation;
import org.newdawn.slick.Color;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import yugecin.opsudance.utils.SlickUtil;
/**
* 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;
/** Time, in milliseconds, for a hit result to fade. */
public static final int HITRESULT_FADE_TIME = 500;
/** Time, in milliseconds, for a hit circle to fade. */
public static final int HITCIRCLE_FADE_TIME = 300;
public static final int FOLLOWCIRCLE_FADE_TIME = HITCIRCLE_FADE_TIME / 2;
/** Duration, in milliseconds, of a combo pop effect. */
private static final int COMBO_POP_TIME = 250;
/** Time, in milliseconds, for a hit error tick to fade. */
private static final int HIT_ERROR_FADE_TIME = 5000;
/** Size of a hit circle at the end of the hit animation. */
private static final float HITCIRCLE_ANIM_SCALE = 1.38f;
/** Size of the hit result text at the end of its animation. */
private static final float HITCIRCLE_TEXT_ANIM_SCALE = 1.28f;
/** Time, in milliseconds, for the hit result text to bounce. */
private static final int HITCIRCLE_TEXT_BOUNCE_TIME = 100;
/** Time, in milliseconds, for the hit result text to fade. */
private static final int HITCIRCLE_TEXT_FADE_TIME = 833;
/** Letter grades. */
public enum Grade {
NULL (null, null),
SS (GameImage.RANKING_SS, GameImage.RANKING_SS_SMALL),
SSH (GameImage.RANKING_SSH, GameImage.RANKING_SSH_SMALL), // silver
S (GameImage.RANKING_S, GameImage.RANKING_S_SMALL),
SH (GameImage.RANKING_SH, GameImage.RANKING_SH_SMALL), // silver
A (GameImage.RANKING_A, GameImage.RANKING_A_SMALL),
B (GameImage.RANKING_B, GameImage.RANKING_B_SMALL),
C (GameImage.RANKING_C, GameImage.RANKING_C_SMALL),
D (GameImage.RANKING_D, GameImage.RANKING_D_SMALL);
/** GameImages associated with this grade (large and small sizes). */
private final GameImage large, small;
/** Large-size image scaled for use in song menu. */
private Image menuImage;
/**
* Clears all image references.
* This does NOT destroy images, so be careful of memory leaks!
*/
public static void clearReferences() {
for (Grade grade : Grade.values()) {
grade.menuImage = null;
}
}
public static void destroyImages() {
for (Grade grade : Grade.values()) {
SlickUtil.destroyImage(grade.menuImage);
grade.menuImage = null;
}
}
/**
* Constructor.
* @param large the large size image
* @param small the small size image
*/
Grade(GameImage large, GameImage small) {
this.large = large;
this.small = small;
}
/**
* Returns the large size grade image.
*/
public Image getLargeImage() { return large.getImage(); }
/**
* Returns the small size grade image.
*/
public Image getSmallImage() { return small.getImage(); }
/**
* Returns the large size grade image scaled for song menu use.
*/
public Image getMenuImage() {
if (menuImage != null)
return menuImage;
Image img = getSmallImage();
if (!small.hasBeatmapSkinImage()) // save default image only
this.menuImage = img;
return img;
}
}
/** 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_SLIDER_REPEAT = 10, // not a hit result
HIT_SLIDER_REPEAT_M = 11, // not a hit result
HIT_ANIMATION_RESULT = 12; // not a hit result
/** Hit result-related images (indexed by HIT_* constants to HIT_MAX). */
private Image[] hitResults;
/** Counts of each hit result so far (indexed by HIT_* constants to HIT_MAX). */
private int[] hitResultCount;
/** 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;
/** The current combo pop timer, in milliseconds. */
private int comboPopTime;
/**
* Hit result types accumulated this streak (bitmask), for Katu/Geki status.
*
* - &1: 100
*
- &2: 50/Miss
*
*/
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 float comboBurstX;
/** Time offsets for obtaining each hit result (indexed by HIT_* constants to HIT_MAX). */
private int[] hitResultOffset;
/** List of hit result objects associated with hit objects. */
private LinkedBlockingDeque hitResultList;
/**
* Class to store hit error information.
* @author fluddokt
*/
private class HitErrorInfo {
/** The correct hit time. */
private final int time;
/** The coordinates of the hit. */
@SuppressWarnings("unused")
private final int x, y;
/** The difference between the correct and actual hit times. */
private final int timeDiff;
/**
* Constructor.
* @param time the correct hit time
* @param x the x coordinate of the hit
* @param y the y coordinate of the hit
* @param timeDiff the difference between the correct and actual hit times
*/
public HitErrorInfo(int time, int x, int y, int timeDiff) {
this.time = time;
this.x = x;
this.y = y;
this.timeDiff = timeDiff;
}
}
/** List containing recent hit error information. */
private LinkedBlockingDeque hitErrorList;
/** Hit object types, used for drawing results. */
public enum HitObjectType { CIRCLE, SLIDERTICK, SLIDER_FIRST, SLIDER_LAST, SPINNER }
/** Hit result helper class. */
private class HitObjectResult {
/** Object start time. */
public final int time;
/** Hit result. */
public final int result;
/** Object coordinates. */
public final float x, y;
/** Combo color. */
public final Color color;
/** The type of the hit object. */
public final HitObjectType hitResultType;
/** Slider curve. */
public final Curve curve;
/** Whether or not to expand when animating. */
public final boolean expand;
/** Whether or not to hide the hit result. */
public final boolean hideResult;
/** Alpha level (for fading out). */
public float alpha = 1f;
/**
* 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
* @param hitResultType the hit object type
* @param curve the slider curve (or null if not applicable)
* @param expand whether or not the hit result animation should expand (if applicable)
* @param hideResult whether or not to hide the hit result (but still show the other animations)
*/
public HitObjectResult(int time, int result, float x, float y, Color color,
HitObjectType hitResultType, Curve curve, boolean expand, boolean hideResult) {
this.time = time;
this.result = result;
this.x = x;
this.y = y;
this.color = color;
this.hitResultType = hitResultType;
this.curve = curve;
this.expand = expand;
this.hideResult = hideResult;
}
}
/** Current game score. */
private long score;
/** 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;
/** Displayed health (for animation, slightly behind health). */
private float healthDisplay;
/** 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;
/** Score text symbol images. */
private HashMap scoreSymbols;
/** Scorebar animation. */
private Animation scorebarColour;
/** The associated score data. */
private ScoreData scoreData;
/** The associated replay. */
private Replay replay;
/** Whether this object is used for gameplay (true) or score viewing (false). */
private boolean isGameplay;
/** Container dimensions. */
private int width, height;
/**
* Constructor for gameplay.
* @param width container width
* @param height container height
*/
public GameData(int width, int height) {
this.width = width;
this.height = height;
this.isGameplay = true;
clear();
}
/**
* Constructor for score viewing.
* This will initialize all parameters and images needed for the
* {@link #drawRankingElements(Graphics, Beatmap)} method.
* @param s the ScoreData object
* @param width container width
* @param height container height
*/
public GameData(ScoreData s, int width, int height) {
this.width = width;
this.height = height;
this.isGameplay = false;
this.scoreData = s;
this.score = s.score;
this.comboMax = s.combo;
this.fullObjectCount = (s.perfect) ? s.combo : -1;
this.hitResultCount = new int[HIT_MAX];
hitResultCount[HIT_300] = s.hit300;
hitResultCount[HIT_100] = s.hit100;
hitResultCount[HIT_50] = s.hit50;
hitResultCount[HIT_300G] = s.geki;
hitResultCount[HIT_300K] = 0;
hitResultCount[HIT_100K] = s.katu;
hitResultCount[HIT_MISS] = s.miss;
this.replay = (s.replayString == null) ? null :
new Replay(new File(Options.getReplayDir(), String.format("%s.osr", s.replayString)));
loadImages();
}
/**
* Clears all data and re-initializes object.
*/
public void clear() {
score = 0;
scoreDisplay = 0;
scorePercentDisplay = 0f;
health = 100f;
healthDisplay = 100f;
hitResultCount = new int[HIT_MAX];
drainRate = 5f;
if (hitResultList != null) {
for (HitObjectResult hitResult : hitResultList) {
if (hitResult.curve != null)
hitResult.curve.discardGeometry();
}
}
hitResultList = new LinkedBlockingDeque();
hitErrorList = new LinkedBlockingDeque();
fullObjectCount = 0;
combo = 0;
comboMax = 0;
comboPopTime = COMBO_POP_TIME;
comboEnd = 0;
comboBurstIndex = -1;
scoreData = null;
}
/**
* Loads all game score images.
*/
public void loadImages() {
// gameplay-specific images
if (isGameplay()) {
// combo burst images
if (GameImage.COMBO_BURST.hasBeatmapSkinImages() ||
(!GameImage.COMBO_BURST.hasBeatmapSkinImage() && GameImage.COMBO_BURST.getImages() != null))
comboBurstImages = GameImage.COMBO_BURST.getImages();
else
comboBurstImages = new Image[]{ GameImage.COMBO_BURST.getImage() };
// scorebar-colour animation
Image[] scorebar = GameImage.SCOREBAR_COLOUR.getImages();
scorebarColour = (scorebar != null) ? new Animation(scorebar, 60) : null;
// default symbol images
defaultSymbols = new Image[10];
defaultSymbols[0] = GameImage.DEFAULT_0.getImage();
defaultSymbols[1] = GameImage.DEFAULT_1.getImage();
defaultSymbols[2] = GameImage.DEFAULT_2.getImage();
defaultSymbols[3] = GameImage.DEFAULT_3.getImage();
defaultSymbols[4] = GameImage.DEFAULT_4.getImage();
defaultSymbols[5] = GameImage.DEFAULT_5.getImage();
defaultSymbols[6] = GameImage.DEFAULT_6.getImage();
defaultSymbols[7] = GameImage.DEFAULT_7.getImage();
defaultSymbols[8] = GameImage.DEFAULT_8.getImage();
defaultSymbols[9] = GameImage.DEFAULT_9.getImage();
}
// score symbol images
scoreSymbols = new HashMap(14);
scoreSymbols.put('0', GameImage.SCORE_0.getImage());
scoreSymbols.put('1', GameImage.SCORE_1.getImage());
scoreSymbols.put('2', GameImage.SCORE_2.getImage());
scoreSymbols.put('3', GameImage.SCORE_3.getImage());
scoreSymbols.put('4', GameImage.SCORE_4.getImage());
scoreSymbols.put('5', GameImage.SCORE_5.getImage());
scoreSymbols.put('6', GameImage.SCORE_6.getImage());
scoreSymbols.put('7', GameImage.SCORE_7.getImage());
scoreSymbols.put('8', GameImage.SCORE_8.getImage());
scoreSymbols.put('9', GameImage.SCORE_9.getImage());
scoreSymbols.put(',', GameImage.SCORE_COMMA.getImage());
scoreSymbols.put('.', GameImage.SCORE_DOT.getImage());
scoreSymbols.put('%', GameImage.SCORE_PERCENT.getImage());
scoreSymbols.put('x', GameImage.SCORE_X.getImage());
// hit result images
hitResults = new Image[HIT_MAX];
hitResults[HIT_MISS] = GameImage.HIT_MISS.getImage();
hitResults[HIT_50] = GameImage.HIT_50.getImage();
hitResults[HIT_100] = GameImage.HIT_100.getImage();
hitResults[HIT_300] = GameImage.HIT_300.getImage();
hitResults[HIT_100K] = GameImage.HIT_100K.getImage();
hitResults[HIT_300K] = GameImage.HIT_300K.getImage();
hitResults[HIT_300G] = GameImage.HIT_300G.getImage();
hitResults[HIT_SLIDER10] = GameImage.HIT_SLIDER10.getImage();
hitResults[HIT_SLIDER30] = GameImage.HIT_SLIDER30.getImage();
}
/**
* Returns a default text symbol image for a digit.
* @param i the digit [0-9]
*/
public Image getDefaultSymbolImage(int i) { return defaultSymbols[i]; }
/**
* Returns a score text symbol image for a character.
* @param c the character [0-9,.%x]
*/
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})
*/
public void setHitResultOffset(int[] hitResultOffset) { this.hitResultOffset = hitResultOffset; }
/**
* 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
* @param alpha the alpha level
*/
public void drawSymbolNumber(int n, float x, float y, float scale, float alpha) {
int length = (int) (Math.log10(n) + 1);
float digitWidth = getDefaultSymbolImage(0).getWidth();
if (digitWidth <= 1f) {
return;
}
digitWidth = (digitWidth - Options.getSkin().getHitCircleFontOverlap()) * scale;
float cx = x + ((length - 1) * (digitWidth / 2));
for (int i = 0; i < length; i++) {
Image digit = getDefaultSymbolImage(n % 10).getScaledCopy(scale);
digit.setAlpha(alpha);
digit.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 alpha the alpha level
* @param rightAlign align right (true) or left (false)
*/
public void drawSymbolString(String str, float x, float y, float scale, float alpha, boolean rightAlign) {
char[] c = str.toCharArray();
float 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() + Options.getSkin().getScoreFontOverlap();
digit.setAlpha(alpha);
digit.draw(cx, y);
digit.setAlpha(1f);
}
} else {
for (int i = 0; i < c.length; i++) {
Image digit = getScoreSymbolImage(c[i]);
if (scale != 1.0f)
digit = digit.getScaledCopy(scale);
digit.setAlpha(alpha);
digit.draw(cx, y);
digit.setAlpha(1f);
cx += digit.getWidth() - Options.getSkin().getScoreFontOverlap();
}
}
}
/**
* Draws a string of scoreSymbols of fixed width.
* @param str the string to draw
* @param x the starting x coordinate
* @param y the y coordinate
* @param scale the scale to apply
* @param alpha the alpha level
* @param fixedsize the width to use for all symbols
* @param rightAlign align right (true) or left (false)
*/
public void drawFixedSizeSymbolString(String str, float x, float y, float scale, float alpha, float fixedsize, boolean rightAlign) {
char[] c = str.toCharArray();
float 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 -= fixedsize;
digit.setAlpha(alpha);
digit.draw(cx + (fixedsize - digit.getWidth()) / 2, y);
digit.setAlpha(1f);
}
} else {
for (int i = 0; i < c.length; i++) {
Image digit = getScoreSymbolImage(c[i]);
if (scale != 1.0f)
digit = digit.getScaledCopy(scale);
digit.setAlpha(alpha);
digit.draw(cx + (fixedsize - digit.getWidth()) / 2, y);
digit.setAlpha(1f);
cx += fixedsize;
}
}
}
/**
* Draws game elements:
* scorebar, score, score percentage, map progress circle,
* mod icons, combo count, combo burst, hit error bar, and grade.
* @param g the graphics context
* @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
* @param alpha the alpha level at which to render all elements (except the hit error bar)
*/
@SuppressWarnings("deprecation")
public void drawGameElements(Graphics g, boolean breakPeriod, boolean firstObject, float alpha) {
boolean relaxAutoPilot = (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive());
int margin = (int) (width * 0.008f);
float uiScale = GameImage.getUIscale();
// score
if (!relaxAutoPilot)
drawFixedSizeSymbolString((scoreDisplay < 100000000) ? String.format("%08d", scoreDisplay) : Long.toString(scoreDisplay),
width - margin, 0, 1f, alpha, getScoreSymbolImage('0').getWidth() - 2, true);
// score percentage
int symbolHeight = getScoreSymbolImage('0').getHeight();
if (!relaxAutoPilot)
drawSymbolString(
String.format((scorePercentDisplay < 10f) ? "0%.2f%%" : "%.2f%%", scorePercentDisplay),
width - margin, symbolHeight, 0.60f, alpha, true);
// map progress circle
Beatmap beatmap = MusicController.getBeatmap();
int firstObjectTime = beatmap.objects[0].getTime();
int trackPosition = MusicController.getPosition();
float circleDiameter = symbolHeight * 0.60f;
int circleX = (int) (width - margin - ( // max width: "100.00%"
getScoreSymbolImage('1').getWidth() +
getScoreSymbolImage('0').getWidth() * 4 +
getScoreSymbolImage('.').getWidth() +
getScoreSymbolImage('%').getWidth()
) * 0.60f - circleDiameter);
if (!relaxAutoPilot) {
float oldWhiteAlpha = Colors.WHITE_ALPHA.a;
Colors.WHITE_ALPHA.a = alpha;
g.setAntiAlias(true);
g.setLineWidth(2f);
g.setColor(Colors.WHITE_ALPHA);
g.drawOval(circleX, symbolHeight, circleDiameter, circleDiameter);
if (trackPosition > firstObjectTime) {
// map progress (white)
float progress = Math.min((float) (trackPosition - firstObjectTime) / (beatmap.endTime - firstObjectTime), 1f);
g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter, -90, -90 + (int) (360f * progress));
} else {
// lead-in time (yellow)
float progress = (float) trackPosition / firstObjectTime;
float oldYellowAlpha = Colors.YELLOW_ALPHA.a;
Colors.YELLOW_ALPHA.a *= alpha;
g.setColor(Colors.YELLOW_ALPHA);
g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter, -90 + (int) (360f * progress), -90);
Colors.YELLOW_ALPHA.a = oldYellowAlpha;
}
g.setAntiAlias(false);
Colors.WHITE_ALPHA.a = oldWhiteAlpha;
}
// mod icons
if ((firstObject && trackPosition < firstObjectTime) || GameMod.AUTO.isActive()) {
int modWidth = GameMod.AUTO.getImage().getWidth();
float modX = (width * 0.98f) - modWidth;
int modCount = 0;
for (GameMod mod : GameMod.VALUES_REVERSED) {
if (mod.isActive()) {
mod.getImage().setAlpha(alpha);
mod.getImage().draw(
modX - (modCount * (modWidth / 2f)),
symbolHeight + circleDiameter + 10
);
mod.getImage().setAlpha(1f);
modCount++;
}
}
}
// hit error bar
if (Options.isHitErrorBarEnabled() && !hitErrorList.isEmpty()) {
// fade out with last tick
float hitErrorAlpha = 1f;
Color white = new Color(Color.white);
if (trackPosition - hitErrorList.getFirst().time > HIT_ERROR_FADE_TIME * 0.9f)
hitErrorAlpha = (HIT_ERROR_FADE_TIME - (trackPosition - hitErrorList.getFirst().time)) / (HIT_ERROR_FADE_TIME * 0.1f);
// draw bar
float hitErrorX = width / uiScale / 2;
float hitErrorY = height / uiScale - margin - 10;
float barY = (hitErrorY - 3) * uiScale, barHeight = 6 * uiScale;
float tickY = (hitErrorY - 10) * uiScale, tickHeight = 20 * uiScale;
float oldAlphaBlack = Colors.BLACK_ALPHA.a;
Colors.BLACK_ALPHA.a = hitErrorAlpha;
g.setColor(Colors.BLACK_ALPHA);
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_50]) * uiScale, tickY,
(hitResultOffset[HIT_50] * 2) * uiScale, tickHeight);
Colors.BLACK_ALPHA.a = oldAlphaBlack;
Colors.LIGHT_ORANGE.a = hitErrorAlpha;
g.setColor(Colors.LIGHT_ORANGE);
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_50]) * uiScale, barY,
(hitResultOffset[HIT_50] * 2) * uiScale, barHeight);
Colors.LIGHT_ORANGE.a = 1f;
Colors.LIGHT_GREEN.a = hitErrorAlpha;
g.setColor(Colors.LIGHT_GREEN);
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_100]) * uiScale, barY,
(hitResultOffset[HIT_100] * 2) * uiScale, barHeight);
Colors.LIGHT_GREEN.a = 1f;
Colors.LIGHT_BLUE.a = hitErrorAlpha;
g.setColor(Colors.LIGHT_BLUE);
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_300]) * uiScale, barY,
(hitResultOffset[HIT_300] * 2) * uiScale, barHeight);
Colors.LIGHT_BLUE.a = 1f;
white.a = hitErrorAlpha;
g.setColor(white);
g.fillRect((hitErrorX - 1.5f) * uiScale, tickY, 3 * uiScale, tickHeight);
// draw ticks
float tickWidth = 2 * uiScale;
for (HitErrorInfo info : hitErrorList) {
int time = info.time;
float tickAlpha = 1 - ((float) (trackPosition - time) / HIT_ERROR_FADE_TIME);
white.a = tickAlpha * hitErrorAlpha;
g.setColor(white);
g.fillRect((hitErrorX + info.timeDiff - 1) * uiScale, tickY, tickWidth, tickHeight);
}
}
if (!breakPeriod && !relaxAutoPilot) {
// scorebar
float healthRatio = healthDisplay / 100f;
if (firstObject) { // gradually move ki before map begins
if (firstObjectTime >= 1500 && trackPosition < firstObjectTime - 500)
healthRatio = (float) trackPosition / (firstObjectTime - 500);
}
Image scorebar = GameImage.SCOREBAR_BG.getImage();
Image colour;
if (scorebarColour != null) {
scorebarColour.updateNoDraw(); // TODO deprecated method
colour = scorebarColour.getCurrentFrame();
} else
colour = GameImage.SCOREBAR_COLOUR.getImage();
float colourX = 4 * uiScale, colourY = 15 * uiScale;
Image colourCropped = colour.getSubImage(0, 0, (int) (645 * uiScale * healthRatio), colour.getHeight());
scorebar.setAlpha(alpha);
scorebar.draw(0, 0);
scorebar.setAlpha(1f);
colourCropped.setAlpha(alpha);
colourCropped.draw(colourX, colourY);
colourCropped.setAlpha(1f);
Image ki = null;
if (health >= 50f)
ki = GameImage.SCOREBAR_KI.getImage();
else if (health >= 25f)
ki = GameImage.SCOREBAR_KI_DANGER.getImage();
else
ki = GameImage.SCOREBAR_KI_DANGER2.getImage();
if (comboPopTime < COMBO_POP_TIME)
ki = ki.getScaledCopy(1f + (0.45f * (1f - (float) comboPopTime / COMBO_POP_TIME)));
ki.setAlpha(alpha);
ki.drawCentered(colourX + colourCropped.getWidth(), colourY);
ki.setAlpha(1f);
// 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) {
float comboPop = 1 - ((float) comboPopTime / COMBO_POP_TIME);
float comboPopBack = 1 + comboPop * 0.45f;
float comboPopFront = 1 + comboPop * 0.08f;
String comboString = String.format("%dx", combo);
if (comboPopTime != COMBO_POP_TIME)
drawSymbolString(comboString, margin, height - margin - (symbolHeight * comboPopBack), comboPopBack, 0.5f * alpha, false);
drawSymbolString(comboString, margin, height - margin - (symbolHeight * comboPopFront), comboPopFront, alpha, false);
}
} else if (!relaxAutoPilot) {
// grade
Grade grade = getGrade();
if (grade != Grade.NULL) {
Image gradeImage = grade.getSmallImage();
float gradeScale = symbolHeight * 0.75f / gradeImage.getHeight();
gradeImage = gradeImage.getScaledCopy(gradeScale);
gradeImage.setAlpha(alpha);
gradeImage.draw(circleX - gradeImage.getWidth(), symbolHeight);
}
}
}
/**
* Draws ranking elements: score, results, ranking, game mods.
* @param g the graphics context
* @param beatmap the beatmap
*/
public void drawRankingElements(Graphics g, Beatmap beatmap) {
// TODO Version 2 skins
float rankingHeight = 75;
float scoreTextScale = 1.0f;
float symbolTextScale = 1.15f;
float rankResultScale = 0.5f;
float uiScale = GameImage.getUIscale();
// ranking panel
GameImage.RANKING_PANEL.getImage().draw(0, (int) (rankingHeight * uiScale));
// score
drawFixedSizeSymbolString(
(score < 100000000) ? String.format("%08d", score) : Long.toString(score),
210 * uiScale, (rankingHeight + 50) * uiScale,
scoreTextScale, 1f, getScoreSymbolImage('0').getWidth() * scoreTextScale - 2, false
);
// result counts
float resultInitialX = 130;
float resultInitialY = rankingHeight + 140;
float resultHitInitialX = 65;
float resultHitInitialY = rankingHeight + 182;
float resultOffsetX = 320;
float resultOffsetY = 96;
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).drawCentered(
resultHitInitialX * uiScale,
(resultHitInitialY + (resultOffsetY * (i / 2))) * uiScale);
hitResults[rankDrawOrder[i+1]].getScaledCopy(rankResultScale).drawCentered(
(resultHitInitialX + resultOffsetX) * uiScale,
(resultHitInitialY + (resultOffsetY * (i / 2))) * uiScale);
drawSymbolString(String.format("%dx", rankResultOrder[i]),
resultInitialX * uiScale,
(resultInitialY + (resultOffsetY * (i / 2))) * uiScale,
symbolTextScale, 1f, false);
drawSymbolString(String.format("%dx", rankResultOrder[i+1]),
(resultInitialX + resultOffsetX) * uiScale,
(resultInitialY + (resultOffsetY * (i / 2))) * uiScale,
symbolTextScale, 1f, false);
}
// combo and accuracy
float accuracyX = 295;
float textY = rankingHeight + 425;
float numbersY = textY + 30;
drawSymbolString(String.format("%dx", comboMax),
25 * uiScale, numbersY * uiScale, symbolTextScale, 1f, false);
drawSymbolString(String.format("%02.2f%%", getScorePercent()),
(accuracyX + 20) * uiScale, numbersY * uiScale, symbolTextScale, 1f, false);
GameImage.RANKING_MAXCOMBO.getImage().draw(10 * uiScale, textY * uiScale);
GameImage.RANKING_ACCURACY.getImage().draw(accuracyX * uiScale, textY * uiScale);
// full combo
if (comboMax == fullObjectCount) {
GameImage.RANKING_PERFECT.getImage().draw(
width * 0.08f,
(height * 0.99f) - GameImage.RANKING_PERFECT.getImage().getHeight()
);
}
// grade
Grade grade = getGrade();
if (grade != Grade.NULL)
grade.getLargeImage().draw(width - grade.getLargeImage().getWidth(), rankingHeight);
// header
Image rankingTitle = GameImage.RANKING_TITLE.getImage();
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, 100 * uiScale);
rankingTitle.draw((width * 0.97f) - rankingTitle.getWidth(), 0);
float marginX = width * 0.01f, marginY = height * 0.002f;
Fonts.LARGE.drawString(marginX, marginY,
String.format("%s - %s [%s]", beatmap.getArtist(), beatmap.getTitle(), beatmap.version), Color.white);
Fonts.MEDIUM.drawString(marginX, marginY + Fonts.LARGE.getLineHeight() - 6,
String.format("Beatmap by %s", beatmap.creator), Color.white);
String player = (scoreData.playerName == null) ? "" : String.format(" by %s", scoreData.playerName);
Fonts.MEDIUM.drawString(marginX, marginY + Fonts.LARGE.getLineHeight() + Fonts.MEDIUM.getLineHeight() - 10,
String.format("Played%s on %s.", player, scoreData.getTimeString()), Color.white);
// mod icons
int modWidth = GameMod.AUTO.getImage().getWidth();
float modX = (width * 0.98f) - modWidth;
int modCount = 0;
for (GameMod mod : GameMod.VALUES_REVERSED) {
if ((mod.getBit() & scoreData.mods) > 0) {
mod.getImage().draw(modX - (modCount * (modWidth / 2f)), height / 2f);
modCount++;
}
}
}
/**
* Draws stored hit results and removes them from the list as necessary.
* @param trackPosition the current track position (in ms)
*/
public void drawHitResults(int trackPosition) {
Iterator iter = hitResultList.iterator();
while (iter.hasNext()) {
HitObjectResult hitResult = iter.next();
if (hitResult.time + HITRESULT_TIME > trackPosition) {
// spinner
if (hitResult.hitResultType == HitObjectType.SPINNER && 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
else if (Options.isHitLightingEnabled() && !hitResult.hideResult && hitResult.result != HIT_MISS &&
hitResult.result != HIT_SLIDER30 && hitResult.result != HIT_SLIDER10) {
// TODO: add particle system
Image lighting = GameImage.LIGHTING.getImage();
lighting.setAlpha(hitResult.alpha);
lighting.drawCentered(hitResult.x, hitResult.y, hitResult.color);
}
// hit animations (only draw when the "Hidden" mod is not enabled)
if (!GameMod.HIDDEN.isActive()) {
drawHitAnimations(hitResult, trackPosition);
}
// hit result
if (!hitResult.hideResult && (
hitResult.hitResultType == HitObjectType.CIRCLE ||
hitResult.hitResultType == HitObjectType.SLIDER_FIRST ||
hitResult.hitResultType == HitObjectType.SLIDER_LAST ||
hitResult.hitResultType == HitObjectType.SPINNER)) {
float scaleProgress = AnimationEquation.IN_OUT_BOUNCE.calc(
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_TEXT_BOUNCE_TIME) / HITCIRCLE_TEXT_BOUNCE_TIME);
float scale = 1f + (HITCIRCLE_TEXT_ANIM_SCALE - 1f) * scaleProgress;
float fadeProgress = AnimationEquation.OUT_CUBIC.calc(
(float) Utils.clamp((trackPosition - hitResult.time) - HITCIRCLE_FADE_TIME, 0, HITCIRCLE_TEXT_FADE_TIME) / HITCIRCLE_TEXT_FADE_TIME);
float alpha = 1f - fadeProgress;
Image scaledHitResult = hitResults[hitResult.result].getScaledCopy(scale);
scaledHitResult.setAlpha(alpha);
scaledHitResult.drawCentered(hitResult.x, hitResult.y);
}
hitResult.alpha = 1 - ((float) (trackPosition - hitResult.time) / HITRESULT_FADE_TIME);
} else {
if (hitResult.curve != null)
hitResult.curve.discardGeometry();
iter.remove();
}
}
}
/**
* Draw the hit animations:
* circles, reverse arrows, slider curves (fading out and/or expanding).
* @param hitResult the hit result
* @param trackPosition the current track position (in ms)
*/
private void drawHitAnimations(HitObjectResult hitResult, int trackPosition) {
// fade out slider curve
if (hitResult.result != HIT_SLIDER_REPEAT && hitResult.result != HIT_SLIDER_REPEAT_M && hitResult.curve != null) {
if (!Options.isShrinkingSliders()) {
float progress = AnimationEquation.OUT_CUBIC.calc(
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME) / HITCIRCLE_FADE_TIME);
float alpha = 1f - progress;
float oldWhiteAlpha = Colors.WHITE_FADE.a;
float oldColorAlpha = hitResult.color.a;
Colors.WHITE_FADE.a = hitResult.color.a = alpha;
hitResult.curve.draw(hitResult.color);
Colors.WHITE_FADE.a = oldWhiteAlpha;
hitResult.color.a = oldColorAlpha;
}
// slider follow circle
if (hitResult.expand) {
float progress = AnimationEquation.OUT_CUBIC.calc((float) Utils.clamp(trackPosition - hitResult.time, 0, FOLLOWCIRCLE_FADE_TIME) / FOLLOWCIRCLE_FADE_TIME);
float scale = 1f - 0.2f * progress;
float alpha = 1f - progress;
Image fc = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getScaledCopy(scale);
fc.setAlpha(alpha);
fc.drawCentered(hitResult.x, hitResult.y);
}
if (!Options.isDrawSliderEndCircles()) {
return;
}
}
// miss, don't draw an animation
if (hitResult.result == HIT_MISS) {
return;
}
// not a circle?
if (hitResult.hitResultType != HitObjectType.CIRCLE &&
hitResult.hitResultType != HitObjectType.SLIDER_FIRST &&
hitResult.hitResultType != HitObjectType.SLIDER_LAST) {
return;
}
// hit circles
float progress = AnimationEquation.OUT_CUBIC.calc(
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME) / HITCIRCLE_FADE_TIME);
float scale = (!hitResult.expand) ? 1f : 1f + (HITCIRCLE_ANIM_SCALE - 1f) * progress;
float alpha = 1f - progress;
if (hitResult.result == HIT_SLIDER_REPEAT || hitResult.result == HIT_SLIDER_REPEAT_M) {
// repeats
Image scaledRepeat = GameImage.REVERSEARROW.getImage().getScaledCopy(scale);
scaledRepeat.setAlpha(alpha);
float ang;
if (hitResult.hitResultType == HitObjectType.SLIDER_FIRST) {
ang = hitResult.curve.getStartAngle();
} else {
ang = hitResult.curve.getEndAngle();
}
if (hitResult.result == HIT_SLIDER_REPEAT_M) {
ang += 180f;
}
scaledRepeat.rotate(ang);
scaledRepeat.drawCentered(hitResult.x, hitResult.y, hitResult.color);
if (!Options.isDrawSliderEndCircles()) {
GameImage.HITCIRCLE.getImage().draw(-1000, -1000); // TODO this 'fixes' #114. Why? Get a better solution!
GameImage.HITCIRCLE_OVERLAY.getImage().draw(-1000, -1000);
return;
}
}
Image scaledHitCircle = GameImage.HITCIRCLE.getImage().getScaledCopy(scale);
Image scaledHitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(scale);
scaledHitCircle.setAlpha(alpha);
scaledHitCircleOverlay.setAlpha(alpha);
scaledHitCircle.drawCentered(hitResult.x, hitResult.y, hitResult.color);
scaledHitCircleOverlay.drawCentered(hitResult.x, hitResult.y);
}
/**
* Changes health by a given percentage, modified by drainRate.
* @param percent the health percentage
*/
public void changeHealth(float percent) {
// TODO: drainRate formula
health += percent;
if (health > 100f)
health = 100f;
else if (health < 0f)
health = 0f;
}
/**
* Returns the current 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 || GameMod.NO_FAIL.isActive() || GameMod.AUTO.isActive() ||
GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive());
}
/**
* Changes score by a raw value (not affected by other modifiers).
* @param value the score value
*/
public void changeScore(int value) { score += value; }
/**
* Returns the raw score percentage.
* @param hit300 the number of 300s
* @param hit100 the number of 100s
* @param hit50 the number of 50s
* @param miss the number of misses
* @return the score percentage
*/
public static float getScorePercent(int hit300, int hit100, int hit50, int miss) {
float percent = 0;
int objectCount = hit300 + hit100 + hit50 + miss;
if (objectCount > 0)
percent = (hit300 * 300 + hit100 * 100 + hit50 * 50) / (objectCount * 300f) * 100f;
return percent;
}
/**
* Returns the raw score percentage.
*/
private float getScorePercent() {
return getScorePercent(
hitResultCount[HIT_300], hitResultCount[HIT_100],
hitResultCount[HIT_50], hitResultCount[HIT_MISS]
);
}
/**
* Returns letter grade based on score data,
* or Grade.NULL if no objects have been processed.
* @param hit300 the number of 300s
* @param hit100 the number of 100s
* @param hit50 the number of 50s
* @param miss the number of misses
* @param silver whether or not a silver SS/S should be awarded (if applicable)
* @return the current Grade
*/
public static Grade getGrade(int hit300, int hit100, int hit50, int miss, boolean silver) {
int objectCount = hit300 + hit100 + hit50 + miss;
if (objectCount < 1) // avoid division by zero
return Grade.NULL;
float percent = getScorePercent(hit300, hit100, hit50, miss);
float hit300ratio = hit300 * 100f / objectCount;
float hit50ratio = hit50 * 100f / objectCount;
boolean noMiss = (miss == 0);
if (percent >= 100f)
return (silver) ? Grade.SSH : Grade.SS;
else if (hit300ratio >= 90f && hit50ratio < 1.0f && noMiss)
return (silver) ? Grade.SH : 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;
}
/**
* Returns letter grade based on score data,
* or {@code Grade.NULL} if no objects have been processed.
*/
private Grade getGrade() {
boolean silver = (scoreData == null) ?
(GameMod.HIDDEN.isActive() || GameMod.FLASHLIGHT.isActive()) :
(scoreData.mods & (GameMod.HIDDEN.getBit() | GameMod.FLASHLIGHT.getBit())) != 0;
return getGrade(
hitResultCount[HIT_300], hitResultCount[HIT_100],
hitResultCount[HIT_50], hitResultCount[HIT_MISS],
silver
);
}
/**
* Updates displayed elements based on a delta value.
* @param delta the delta interval since the last call
*/
public void updateDisplays(int delta) {
// score display
if (scoreDisplay < score) {
scoreDisplay += (score - scoreDisplay) * delta / 50 + 1;
if (scoreDisplay > score)
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;
if (healthDisplay < health) {
healthDisplay += shift;
if (healthDisplay > health)
healthDisplay = health;
} else {
healthDisplay -= shift;
if (healthDisplay < health)
healthDisplay = health;
}
}
// combo burst
if (comboBurstIndex > -1 && Options.isComboBurstEnabled()) {
int leftX = 0;
int rightX = width - comboBurstImages[comboBurstIndex].getWidth();
if (comboBurstX < leftX) {
comboBurstX += (delta / 2f) * GameImage.getUIscale();
if (comboBurstX > leftX)
comboBurstX = leftX;
} else if (comboBurstX > rightX) {
comboBurstX -= (delta / 2f) * GameImage.getUIscale();
if (comboBurstX < rightX)
comboBurstX = rightX;
} else if (comboBurstAlpha > 0f) {
comboBurstAlpha -= (delta / 1200f);
if (comboBurstAlpha < 0f)
comboBurstAlpha = 0f;
}
}
// combo pop
comboPopTime += delta;
if (comboPopTime > COMBO_POP_TIME)
comboPopTime = COMBO_POP_TIME;
// hit error bar
if (Options.isHitErrorBarEnabled()) {
int trackPosition = MusicController.getPosition();
Iterator iter = hitErrorList.iterator();
while (iter.hasNext()) {
HitErrorInfo info = iter.next();
if (Math.abs(info.timeDiff) >= hitResultOffset[GameData.HIT_50] ||
info.time + HIT_ERROR_FADE_TIME <= trackPosition)
iter.remove();
}
}
}
/**
* Returns the current combo streak.
*/
public int getComboStreak() { return combo; }
/**
* Increases the combo streak by one.
*/
private void incrementComboStreak() {
combo++;
comboPopTime = 0;
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 (Options.getSkin().isComboBurstRandom())
comboBurstIndex = (int) (Math.random() * comboBurstImages.length);
else {
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 && !(GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive()))
SoundController.playSound(SoundEffect.COMBOBREAK);
combo = 0;
if (GameMod.SUDDEN_DEATH.isActive())
health = 0f;
}
/**
* Handles a slider repeat result (animation only: arrow).
* @param time the repeat time
* @param x the x coordinate
* @param y the y coordinate
* @param color the arrow color
* @param curve the slider curve
* @param type the hit object type
*/
public void sendSliderRepeatResult(int time, float x, float y, Color color, Curve curve, HitObjectType type) {
hitResultList.add(new HitObjectResult(time, HIT_SLIDER_REPEAT, x, y, color, type, curve, true, true));
if (!Options.isMirror()) {
return;
}
float[] m = Utils.mirrorPoint(x, y);
hitResultList.add(new HitObjectResult(time, HIT_SLIDER_REPEAT_M, m[0], m[1], color, type, curve, true, true));
}
/**
* Handles a slider start result (animation only: initial circle).
* @param time the hit time
* @param x the x coordinate
* @param y the y coordinate
* @param color the slider color
* @param expand whether or not the hit result animation should expand
*/
public void sendSliderStartResult(int time, float x, float y, Color color, Color mirrorColor, boolean expand) {
hitResultList.add(new HitObjectResult(time, HIT_ANIMATION_RESULT, x, y, color, HitObjectType.CIRCLE, null, expand, true));
if (!Options.isMirror()) {
return;
}
float[] m = Utils.mirrorPoint(x, y);
hitResultList.add(new HitObjectResult(time, HIT_ANIMATION_RESULT, m[0], m[1], mirrorColor, HitObjectType.CIRCLE, null, expand, true));
}
/**
* 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 hitObject the hit object
* @param repeat the current repeat number
*/
public void sendSliderTickResult(int time, int result, float x, float y, HitObject hitObject, int repeat) {
int hitValue = 0;
switch (result) {
case HIT_SLIDER30:
hitValue = 30;
changeHealth(2f);
SoundController.playHitSound(
hitObject.getEdgeHitSoundType(repeat),
hitObject.getSampleSet(repeat),
hitObject.getAdditionSampleSet(repeat));
break;
case HIT_SLIDER10:
hitValue = 10;
changeHealth(1f);
SoundController.playHitSound(HitSound.SLIDERTICK);
break;
case HIT_MISS:
resetComboStreak();
break;
default:
return;
}
if (hitValue > 0) {
// calculate score and increment combo streak
score += hitValue;
incrementComboStreak();
if (!Options.isPerfectHitBurstEnabled())
; // hide perfect hit results
else
hitResultList.add(new HitObjectResult(time, result, x, y, null, HitObjectType.SLIDERTICK, null, false, false));
}
fullObjectCount++;
}
/**
* Returns the score for a hit based on the following 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 difficulty setting (see {@link #calculateDifficultyMultiplier(float, float, float)})
*
- Mod: mod multipliers
*
* @param hitValue the hit value
* @param hitObject the hit object
* @return the score value
* @see https://osu.ppy.sh/wiki/Score
*/
private int getScoreForHit(int hitValue, HitObject hitObject) {
int comboMultiplier = Math.max(combo - 1, 0);
if (hitObject.isSlider())
comboMultiplier++;
return (hitValue + (int)(hitValue * (comboMultiplier * difficultyMultiplier * GameMod.getScoreMultiplier()) / 25));
}
/**
* Computes and stores the difficulty multiplier used in the score formula.
* @param drainRate the raw HP drain rate value
* @param circleSize the raw circle size value
* @param overallDifficulty the raw overall difficulty value
* @see https://osu.ppy.sh/wiki/Score#How_to_calculate_the_Difficulty_multiplier
*/
public void calculateDifficultyMultiplier(float drainRate, float circleSize, float overallDifficulty) {
// TODO: find the actual formula (osu!wiki is wrong)
// seems to be based on hit object density? (total objects / time)
// 924 3x1/4 beat notes 0.14stars
// 924 3x1beat 0.28stars
// 912 3x1beat with 1 extra note 10 sec away 0.29stars
float sum = drainRate + circleSize + overallDifficulty; // typically 2~27
if (sum <= 5f)
difficultyMultiplier = 2;
else if (sum <= 12f)
difficultyMultiplier = 3;
else if (sum <= 17f)
difficultyMultiplier = 4;
else if (sum <= 24f)
difficultyMultiplier = 5;
else //if (sum <= 30f)
difficultyMultiplier = 6;
//float multiplier = ((circleSize + overallDifficulty + drainRate) / 6) + 1.5f;
//difficultyMultiplier = (int) multiplier;
}
/**
* Handles a hit result and performs all associated calculations.
* @param time the object start time
* @param result the base 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 hitObject the hit object
* @param hitResultType the type of hit object for the result
* @param repeat the current repeat number (for sliders, or 0 otherwise)
* @param noIncrementCombo if the combo should not be incremented by this result
* @return the actual hit result (HIT_* constants)
*/
private int handleHitResult(int time, int result, float x, float y, Color color, boolean end,
HitObject hitObject, HitObjectType hitResultType, int repeat, boolean noIncrementCombo) {
// update health, score, and combo streak based on hit result
int hitValue = 0;
switch (result) {
case HIT_300:
hitValue = 300;
changeHealth(5f);
break;
case HIT_100:
hitValue = 100;
changeHealth(2f);
comboEnd |= 1;
break;
case HIT_50:
hitValue = 50;
comboEnd |= 2;
break;
case HIT_MISS:
hitValue = 0;
changeHealth(-10f);
comboEnd |= 2;
resetComboStreak();
break;
default:
return HIT_MISS;
}
if (hitValue > 0) {
SoundController.playHitSound(
hitObject.getEdgeHitSoundType(repeat),
hitObject.getSampleSet(repeat),
hitObject.getAdditionSampleSet(repeat));
// calculate score and increment combo streak
changeScore(getScoreForHit(hitValue, hitObject));
if (!noIncrementCombo)
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;
}
return result;
}
/**
* 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 hitObject the hit object
* @param hitResultType the type of hit object for the result
* @param expand whether or not the hit result animation should expand (if applicable)
* @param repeat the current repeat number (for sliders, or 0 otherwise)
* @param curve the slider curve (or null if not applicable)
* @param sliderHeldToEnd whether or not the slider was held to the end (if applicable)
*/
public void sendHitResult(int time, int result, float x, float y, Color color,
boolean end, HitObject hitObject, HitObjectType hitResultType,
boolean expand, int repeat, Curve curve, boolean sliderHeldToEnd) {
sendHitResult(time, result, x, y, color, end, hitObject, hitResultType, expand, repeat, curve, sliderHeldToEnd, true);
}
/**
* 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 hitObject the hit object
* @param hitResultType the type of hit object for the result
* @param expand whether or not the hit result animation should expand (if applicable)
* @param repeat the current repeat number (for sliders, or 0 otherwise)
* @param curve the slider curve (or null if not applicable)
* @param sliderHeldToEnd whether or not the slider was held to the end (if applicable)
* @param handleResult whether or not to send a score result
*/
public void sendHitResult(int time, int result, float x, float y, Color color,
boolean end, HitObject hitObject, HitObjectType hitResultType,
boolean expand, int repeat, Curve curve, boolean sliderHeldToEnd, boolean handleResult) {
int hitResult;
if (handleResult) {
hitResult = handleHitResult(time, result, x, y, color, end, hitObject,
hitResultType, repeat, (curve != null && !sliderHeldToEnd));
} else {
hitResult = HIT_300;
}
if (hitResult == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive()))
return; // "relax" and "autopilot" mods: hide misses
boolean hideResult = (hitResult == HIT_300 || hitResult == HIT_300G || hitResult == HIT_300K) && !Options.isPerfectHitBurstEnabled();
hitResultList.add(new HitObjectResult(time, hitResult, x, y, color, hitResultType, curve, expand, hideResult));
}
/**
* Returns a ScoreData object encapsulating all game data.
* If score data already exists, the existing object will be returned
* (i.e. this will not overwrite existing data).
* @param beatmap the beatmap
* @return the ScoreData object
* @see #getCurrentScoreData(Beatmap, boolean)
*/
public ScoreData getScoreData(Beatmap beatmap) {
if (scoreData == null)
scoreData = getCurrentScoreData(beatmap, false);
return scoreData;
}
/**
* Returns a ScoreData object encapsulating all current game data.
* @param beatmap the beatmap
* @param slidingScore if true, use the display score (might not be actual score)
* @return the ScoreData object
* @see #getScoreData(Beatmap)
*/
public ScoreData getCurrentScoreData(Beatmap beatmap, boolean slidingScore) {
ScoreData sd = new ScoreData();
sd.timestamp = System.currentTimeMillis() / 1000L;
sd.MID = beatmap.beatmapID;
sd.MSID = beatmap.beatmapSetID;
sd.title = beatmap.title;
sd.artist = beatmap.artist;
sd.creator = beatmap.creator;
sd.version = beatmap.version;
sd.hit300 = hitResultCount[HIT_300];
sd.hit100 = hitResultCount[HIT_100];
sd.hit50 = hitResultCount[HIT_50];
sd.geki = hitResultCount[HIT_300G];
sd.katu = hitResultCount[HIT_300K] + hitResultCount[HIT_100K];
sd.miss = hitResultCount[HIT_MISS];
sd.score = slidingScore ? scoreDisplay : score;
sd.combo = comboMax;
sd.perfect = (comboMax == fullObjectCount);
sd.mods = GameMod.getModState();
sd.replayString = (replay == null) ? null : replay.getReplayFilename();
sd.playerName = null; // TODO
return sd;
}
/**
* Returns a Replay object encapsulating all game data.
* If a replay already exists and frames is null, the existing object will be returned.
* @param frames the replay frames
* @param beatmap the associated beatmap
* @return the Replay object, or null if none exists and frames is null
*/
public Replay getReplay(ReplayFrame[] frames, Beatmap beatmap) {
if (replay != null && frames == null)
return replay;
if (frames == null)
return null;
replay = new Replay();
replay.mode = Beatmap.MODE_OSU;
replay.version = Updater.get().getBuildDate();
replay.beatmapHash = (beatmap == null) ? "" : beatmap.md5Hash;
replay.playerName = ""; // TODO
replay.replayHash = Long.toString(System.currentTimeMillis()); // TODO
replay.hit300 = (short) hitResultCount[HIT_300];
replay.hit100 = (short) hitResultCount[HIT_100];
replay.hit50 = (short) hitResultCount[HIT_50];
replay.geki = (short) hitResultCount[HIT_300G];
replay.katu = (short) (hitResultCount[HIT_300K] + hitResultCount[HIT_100K]);
replay.miss = (short) hitResultCount[HIT_MISS];
replay.score = (int) score;
replay.combo = (short) comboMax;
replay.perfect = (comboMax == fullObjectCount);
replay.mods = GameMod.getModState();
replay.lifeFrames = null; // TODO
replay.timestamp = new Date();
replay.frames = frames;
replay.seed = 0; // TODO
replay.loaded = true;
return replay;
}
/**
* Sets the replay object.
* @param replay the replay
*/
public void setReplay(Replay replay) { this.replay = replay; }
/**
* Returns whether or not this object is used for gameplay.
* @return true if gameplay, false if score viewing
*/
public boolean isGameplay() { return isGameplay; }
/**
* Sets whether or not this object is used for gameplay.
* @param gameplay true if gameplay, false if score viewing
*/
public void setGameplay(boolean gameplay) { this.isGameplay = gameplay; }
/**
* Adds the hit into the list of hit error information.
* @param time the correct hit time
* @param x the x coordinate of the hit
* @param y the y coordinate of the hit
* @param timeDiff the difference between the correct and actual hit times
*/
public void addHitError(int time, int x, int y, int timeDiff) {
hitErrorList.addFirst(new HitErrorInfo(time, x, y, timeDiff));
}
}