87667c5dab
Match osu! behaviour. Fadein time is static and doesn't scale with approach rate. Circles, numbers, arrows and ticks alpha is the same.
1298 lines
41 KiB
Java
1298 lines
41 KiB
Java
/*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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.downloads.Updater;
|
|
import itdelatrisu.opsu.replay.Replay;
|
|
import itdelatrisu.opsu.replay.ReplayFrame;
|
|
|
|
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;
|
|
|
|
/**
|
|
* 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 fade. */
|
|
public static final int HITRESULT_FADE_TIME = 500;
|
|
|
|
/** 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;
|
|
|
|
/** 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 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;
|
|
}
|
|
|
|
/**
|
|
* 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.hasSkinImage()) // 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 result-related images (indexed by HIT_* constants). */
|
|
private Image[] hitResults;
|
|
|
|
/** Counts of each hit result so far. */
|
|
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.
|
|
* <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 float comboBurstX;
|
|
|
|
/** Time offsets for obtaining each hit result (indexed by HIT_* constants). */
|
|
private int[] hitResultOffset;
|
|
|
|
/** List of hit result objects associated with hit objects. */
|
|
private LinkedBlockingDeque<OsuHitObjectResult> hitResultList;
|
|
|
|
/**
|
|
* Class to store hit error information.
|
|
* @author fluddokt
|
|
*/
|
|
private class HitErrorInfo {
|
|
/** The correct hit time. */
|
|
private int time;
|
|
|
|
/** The coordinates of the hit. */
|
|
@SuppressWarnings("unused")
|
|
private int x, y;
|
|
|
|
/** The difference between the correct and actual hit times. */
|
|
private 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<HitErrorInfo> hitErrorList;
|
|
|
|
/**
|
|
* Hit result helper class.
|
|
*/
|
|
private class OsuHitObjectResult {
|
|
/** Object start time. */
|
|
public int time;
|
|
|
|
/** Hit result. */
|
|
public int result;
|
|
|
|
/** Object coordinates. */
|
|
public float x, y;
|
|
|
|
/** Combo color. */
|
|
public Color color;
|
|
|
|
/** Whether the hit object was a spinner. */
|
|
public boolean isSpinner;
|
|
|
|
/** 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 isSpinner whether the hit object was a spinner
|
|
*/
|
|
public OsuHitObjectResult(int time, int result, float x, float y, Color color, boolean isSpinner) {
|
|
this.time = time;
|
|
this.result = result;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.color = color;
|
|
this.isSpinner = isSpinner;
|
|
}
|
|
}
|
|
|
|
/** 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;
|
|
|
|
/** Beatmap HPDrainRate value. (0:easy ~ 10:hard) */
|
|
private float drainRate = 5f;
|
|
|
|
/** Beatmap OverallDifficulty value. (0:easy ~ 10:hard) */
|
|
private float difficulty = 5f;
|
|
|
|
/** Default text symbol images. */
|
|
private Image[] defaultSymbols;
|
|
|
|
/** Score text symbol images. */
|
|
private HashMap<Character, Image> 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 gameplay;
|
|
|
|
/** 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.gameplay = true;
|
|
|
|
clear();
|
|
}
|
|
|
|
/**
|
|
* Constructor for score viewing.
|
|
* This will initialize all parameters and images needed for the
|
|
* {@link #drawRankingElements(Graphics, OsuFile)} 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.gameplay = 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];
|
|
hitResultList = new LinkedBlockingDeque<OsuHitObjectResult>();
|
|
hitErrorList = new LinkedBlockingDeque<HitErrorInfo>();
|
|
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.hasSkinImages() ||
|
|
(!GameImage.COMBO_BURST.hasSkinImage() && 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<Character, Image>(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/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; }
|
|
|
|
/**
|
|
* Sets the array of hit result offsets.
|
|
*/
|
|
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
|
|
*/
|
|
public void drawSymbolNumber(int n, float x, float y, float scale, Color color) {
|
|
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, color);
|
|
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();
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 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 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.draw(cx + (fixedsize - digit.getWidth()) / 2, 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 + (fixedsize - digit.getWidth()) / 2, y);
|
|
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
|
|
*/
|
|
@SuppressWarnings("deprecation")
|
|
public void drawGameElements(Graphics g, boolean breakPeriod, boolean firstObject) {
|
|
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, 1.0f, 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, 1f, true);
|
|
|
|
// map progress circle
|
|
OsuFile osu = MusicController.getOsuFile();
|
|
int firstObjectTime = osu.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) {
|
|
g.setAntiAlias(true);
|
|
g.setLineWidth(2f);
|
|
g.setColor(Color.white);
|
|
g.drawOval(circleX, symbolHeight, circleDiameter, circleDiameter);
|
|
if (trackPosition > firstObjectTime) {
|
|
// map progress (white)
|
|
g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter,
|
|
-90, -90 + (int) (360f * (trackPosition - firstObjectTime) / (osu.endTime - firstObjectTime))
|
|
);
|
|
} else {
|
|
// lead-in time (yellow)
|
|
g.setColor(Utils.COLOR_YELLOW_ALPHA);
|
|
g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter,
|
|
-90 + (int) (360f * trackPosition / firstObjectTime), -90
|
|
);
|
|
}
|
|
}
|
|
|
|
// 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().draw(
|
|
modX - (modCount * (modWidth / 2f)),
|
|
symbolHeight + circleDiameter + 10
|
|
);
|
|
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 = Utils.COLOR_BLACK_ALPHA.a;
|
|
Utils.COLOR_BLACK_ALPHA.a = hitErrorAlpha;
|
|
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
|
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_50]) * uiScale, tickY,
|
|
(hitResultOffset[HIT_50] * 2) * uiScale, tickHeight);
|
|
Utils.COLOR_BLACK_ALPHA.a = oldAlphaBlack;
|
|
Utils.COLOR_LIGHT_ORANGE.a = hitErrorAlpha;
|
|
g.setColor(Utils.COLOR_LIGHT_ORANGE);
|
|
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_50]) * uiScale, barY,
|
|
(hitResultOffset[HIT_50] * 2) * uiScale, barHeight);
|
|
Utils.COLOR_LIGHT_ORANGE.a = 1f;
|
|
Utils.COLOR_LIGHT_GREEN.a = hitErrorAlpha;
|
|
g.setColor(Utils.COLOR_LIGHT_GREEN);
|
|
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_100]) * uiScale, barY,
|
|
(hitResultOffset[HIT_100] * 2) * uiScale, barHeight);
|
|
Utils.COLOR_LIGHT_GREEN.a = 1f;
|
|
Utils.COLOR_LIGHT_BLUE.a = hitErrorAlpha;
|
|
g.setColor(Utils.COLOR_LIGHT_BLUE);
|
|
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_300]) * uiScale, barY,
|
|
(hitResultOffset[HIT_300] * 2) * uiScale, barHeight);
|
|
Utils.COLOR_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 alpha = 1 - ((float) (trackPosition - time) / HIT_ERROR_FADE_TIME);
|
|
white.a = alpha * 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(1f);
|
|
scorebar.draw(0, 0);
|
|
colourCropped.draw(colourX, colourY);
|
|
|
|
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.drawCentered(colourX + colourCropped.getWidth(), colourY);
|
|
|
|
// 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, false);
|
|
drawSymbolString(comboString, margin, height - margin - (symbolHeight * comboPopFront), comboPopFront, 1f, false);
|
|
}
|
|
} else if (!relaxAutoPilot) {
|
|
// grade
|
|
Grade grade = getGrade();
|
|
if (grade != Grade.NULL) {
|
|
Image gradeImage = grade.getSmallImage();
|
|
float gradeScale = symbolHeight * 0.75f / gradeImage.getHeight();
|
|
gradeImage.getScaledCopy(gradeScale).draw(
|
|
circleX - gradeImage.getWidth(), symbolHeight
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draws ranking elements: score, results, ranking, game mods.
|
|
* @param g the graphics context
|
|
* @param osu the OsuFile
|
|
*/
|
|
public void drawRankingElements(Graphics g, OsuFile osu) {
|
|
// 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, 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(Utils.COLOR_BLACK_ALPHA);
|
|
g.fillRect(0, 0, width, 100 * uiScale);
|
|
rankingTitle.draw((width * 0.97f) - rankingTitle.getWidth(), 0);
|
|
float c = width * 0.01f;
|
|
Utils.FONT_LARGE.drawString(c, c,
|
|
String.format("%s - %s [%s]", osu.getArtist(), osu.getTitle(), osu.version), Color.white);
|
|
Utils.FONT_MEDIUM.drawString(c, c + Utils.FONT_LARGE.getLineHeight() - 6,
|
|
String.format("Beatmap by %s", osu.creator), Color.white);
|
|
Utils.FONT_MEDIUM.drawString(
|
|
c, c + Utils.FONT_LARGE.getLineHeight() + Utils.FONT_MEDIUM.getLineHeight() - 10,
|
|
String.format("Played on %s.", 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
|
|
*/
|
|
public void drawHitResults(int trackPosition) {
|
|
Iterator<OsuHitObjectResult> iter = hitResultList.iterator();
|
|
while (iter.hasNext()) {
|
|
OsuHitObjectResult hitResult = iter.next();
|
|
if (hitResult.time + HITRESULT_FADE_TIME > trackPosition) {
|
|
// hit result
|
|
hitResults[hitResult.result].setAlpha(hitResult.alpha);
|
|
hitResults[hitResult.result].drawCentered(hitResult.x, hitResult.y);
|
|
hitResults[hitResult.result].setAlpha(1f);
|
|
|
|
// spinner
|
|
if (hitResult.isSpinner && hitResult.result != HIT_MISS) {
|
|
Image spinnerOsu = GameImage.SPINNER_OSU.getImage();
|
|
spinnerOsu.setAlpha(hitResult.alpha);
|
|
spinnerOsu.drawCentered(width / 2, height / 4);
|
|
spinnerOsu.setAlpha(1f);
|
|
}
|
|
|
|
// hit lighting
|
|
else if (Options.isHitLightingEnabled() && hitResult.result != HIT_MISS &&
|
|
hitResult.result != HIT_SLIDER30 && hitResult.result != HIT_SLIDER10) {
|
|
float scale = 1f + ((trackPosition - hitResult.time) / (float) HITRESULT_FADE_TIME);
|
|
Image scaledLighting = GameImage.LIGHTING.getImage().getScaledCopy(scale);
|
|
Image scaledLighting1 = GameImage.LIGHTING1.getImage().getScaledCopy(scale);
|
|
scaledLighting.setAlpha(hitResult.alpha);
|
|
scaledLighting1.setAlpha(hitResult.alpha);
|
|
|
|
scaledLighting.draw(hitResult.x - (scaledLighting.getWidth() / 2f),
|
|
hitResult.y - (scaledLighting.getHeight() / 2f), hitResult.color);
|
|
scaledLighting1.draw(hitResult.x - (scaledLighting1.getWidth() / 2f),
|
|
hitResult.y - (scaledLighting1.getHeight() / 2f), hitResult.color);
|
|
}
|
|
|
|
hitResult.alpha = 1 - ((float) (trackPosition - hitResult.time) / HITRESULT_FADE_TIME);
|
|
} else
|
|
iter.remove();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Changes health by a given percentage, modified by drainRate.
|
|
*/
|
|
public void changeHealth(float percent) {
|
|
// TODO: drainRate formula
|
|
health += percent;
|
|
if (health > 100f)
|
|
health = 100f;
|
|
else 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 || GameMod.NO_FAIL.isActive() || GameMod.AUTO.isActive() ||
|
|
GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive());
|
|
}
|
|
|
|
/**
|
|
* Changes score by a raw value (not affected by other modifiers).
|
|
*/
|
|
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 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 Grade.NULL if no objects have been processed.
|
|
*/
|
|
private Grade getGrade() {
|
|
return getGrade(
|
|
hitResultCount[HIT_300], hitResultCount[HIT_100],
|
|
hitResultCount[HIT_50], hitResultCount[HIT_MISS],
|
|
(GameMod.HIDDEN.isActive() || GameMod.FLASHLIGHT.isActive())
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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<HitErrorInfo> 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 (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 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 sliderTickResult(int time, int result, float x, float y, OsuHitObject hitObject, int repeat) {
|
|
int hitValue = 0;
|
|
switch (result) {
|
|
case HIT_SLIDER30:
|
|
hitValue = 30;
|
|
incrementComboStreak();
|
|
changeHealth(1f);
|
|
SoundController.playHitSound(
|
|
hitObject.getEdgeHitSoundType(repeat),
|
|
hitObject.getSampleSet(repeat),
|
|
hitObject.getAdditionSampleSet(repeat));
|
|
break;
|
|
case HIT_SLIDER10:
|
|
hitValue = 10;
|
|
incrementComboStreak();
|
|
SoundController.playHitSound(HitSound.SLIDERTICK);
|
|
break;
|
|
case HIT_MISS:
|
|
resetComboStreak();
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
fullObjectCount++;
|
|
|
|
if (hitValue > 0) {
|
|
score += hitValue;
|
|
if (!Options.isPerfectHitBurstEnabled())
|
|
; // hide perfect hit results
|
|
else
|
|
hitResultList.add(new OsuHitObjectResult(time, result, x, y, null, false));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 repeat the current repeat number (for sliders, or 0 otherwise)
|
|
*/
|
|
public void hitResult(int time, int result, float x, float y, Color color,
|
|
boolean end, OsuHitObject hitObject, int repeat) {
|
|
int hitValue = 0;
|
|
boolean perfectHit = false;
|
|
switch (result) {
|
|
case HIT_300:
|
|
perfectHit = true;
|
|
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;
|
|
}
|
|
if (hitValue > 0) {
|
|
SoundController.playHitSound(
|
|
hitObject.getEdgeHitSoundType(repeat),
|
|
hitObject.getSampleSet(repeat),
|
|
hitObject.getAdditionSampleSet(repeat));
|
|
/**
|
|
* [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 * GameMod.getScoreMultiplier()) / 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;
|
|
}
|
|
|
|
if (perfectHit && !Options.isPerfectHitBurstEnabled())
|
|
; // hide perfect hit results
|
|
else if (result == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive()))
|
|
; // "relax" and "autopilot" mods: hide misses
|
|
else
|
|
hitResultList.add(new OsuHitObjectResult(time, result, x, y, color, hitObject.isSpinner()));
|
|
}
|
|
|
|
/**
|
|
* 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 osu the OsuFile
|
|
* @return the ScoreData object
|
|
*/
|
|
public ScoreData getScoreData(OsuFile osu) {
|
|
if (scoreData != null)
|
|
return scoreData;
|
|
|
|
scoreData = new ScoreData();
|
|
scoreData.timestamp = System.currentTimeMillis() / 1000L;
|
|
scoreData.MID = osu.beatmapID;
|
|
scoreData.MSID = osu.beatmapSetID;
|
|
scoreData.title = osu.title;
|
|
scoreData.artist = osu.artist;
|
|
scoreData.creator = osu.creator;
|
|
scoreData.version = osu.version;
|
|
scoreData.hit300 = hitResultCount[HIT_300];
|
|
scoreData.hit100 = hitResultCount[HIT_100];
|
|
scoreData.hit50 = hitResultCount[HIT_50];
|
|
scoreData.geki = hitResultCount[HIT_300G];
|
|
scoreData.katu = hitResultCount[HIT_300K] + hitResultCount[HIT_100K];
|
|
scoreData.miss = hitResultCount[HIT_MISS];
|
|
scoreData.score = score;
|
|
scoreData.combo = comboMax;
|
|
scoreData.perfect = (comboMax == fullObjectCount);
|
|
scoreData.mods = GameMod.getModState();
|
|
scoreData.replayString = (replay == null) ? null : replay.getReplayFilename();
|
|
return scoreData;
|
|
}
|
|
|
|
/**
|
|
* 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 osu the associated OsuFile
|
|
* @return the Replay object, or null if none exists and frames is null
|
|
*/
|
|
public Replay getReplay(ReplayFrame[] frames, OsuFile osu) {
|
|
if (replay != null && frames == null)
|
|
return replay;
|
|
|
|
if (frames == null)
|
|
return null;
|
|
|
|
replay = new Replay();
|
|
replay.mode = OsuFile.MODE_OSU;
|
|
replay.version = Updater.get().getBuildDate();
|
|
replay.beatmapHash = (osu == null) ? "" : Utils.getMD5(osu.getFile());
|
|
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.
|
|
*/
|
|
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 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));
|
|
}
|
|
}
|