Initial commit.
This commit is contained in:
148
src/itdelatrisu/opsu/GUIMenuButton.java
Normal file
148
src/itdelatrisu/opsu/GUIMenuButton.java
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu;
|
||||
|
||||
import org.newdawn.slick.Animation;
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.Image;
|
||||
|
||||
/**
|
||||
* A convenience class for menu buttons.
|
||||
* Consists of an image or animation and coordinates.
|
||||
*/
|
||||
public class GUIMenuButton {
|
||||
/**
|
||||
* The image associated with the button.
|
||||
*/
|
||||
private Image img;
|
||||
|
||||
/**
|
||||
* The left and right parts of the button (optional).
|
||||
*/
|
||||
private Image imgL, imgR;
|
||||
|
||||
/**
|
||||
* The animation associated with the button.
|
||||
*/
|
||||
private Animation anim;
|
||||
|
||||
/**
|
||||
* The center coordinates.
|
||||
*/
|
||||
private float x, y;
|
||||
|
||||
/**
|
||||
* The x and y radius of the button.
|
||||
*/
|
||||
private float xRadius, yRadius;
|
||||
|
||||
/**
|
||||
* Creates a new button from an Image.
|
||||
*/
|
||||
public GUIMenuButton(Image img, float x, float y) {
|
||||
this.img = img;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
xRadius = img.getWidth() / 2f;
|
||||
yRadius = img.getHeight() / 2f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new button from a 3-part Image.
|
||||
*/
|
||||
public GUIMenuButton(Image imgCenter, Image imgLeft, Image imgRight,
|
||||
float x, float y) {
|
||||
this.img = imgCenter;
|
||||
this.imgL = imgLeft;
|
||||
this.imgR = imgRight;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
xRadius = (img.getWidth() + imgL.getWidth() + imgR.getWidth()) / 2f;
|
||||
yRadius = img.getHeight() / 2f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new button from an Animation.
|
||||
*/
|
||||
public GUIMenuButton(Animation anim, float x, float y) {
|
||||
this.anim = anim;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
xRadius = anim.getWidth() / 2f;
|
||||
yRadius = anim.getHeight() / 2f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets/returns new center coordinates.
|
||||
*/
|
||||
public void setX(float x) { this.x = x; }
|
||||
public void setY(float y) { this.y = y; }
|
||||
public float getX() { return x; }
|
||||
public float getY() { return y; }
|
||||
|
||||
/**
|
||||
* Returns the associated image or animation.
|
||||
*/
|
||||
public Image getImage() { return img; }
|
||||
public Animation getAnimation() { return anim; }
|
||||
|
||||
/**
|
||||
* Draws the button.
|
||||
*/
|
||||
public void draw() {
|
||||
if (img != null) {
|
||||
if (imgL == null)
|
||||
img.draw(x - xRadius, y - yRadius);
|
||||
else {
|
||||
img.draw(x - xRadius + imgL.getWidth(), y - yRadius);
|
||||
imgL.draw(x - xRadius, y - yRadius);
|
||||
imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius);
|
||||
}
|
||||
} else
|
||||
anim.draw(x - xRadius, y - yRadius);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the button with a color filter.
|
||||
* @param filter the color to filter with when drawing
|
||||
*/
|
||||
public void draw(Color filter) {
|
||||
if (img != null) {
|
||||
if (imgL == null)
|
||||
img.draw(x - xRadius, y - yRadius, filter);
|
||||
else {
|
||||
img.draw(x - xRadius + imgL.getWidth(), y - yRadius, filter);
|
||||
imgL.draw(x - xRadius, y - yRadius, filter);
|
||||
imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter);
|
||||
}
|
||||
} else
|
||||
anim.draw(x - xRadius, y - yRadius, filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the coordinates are within the button bounds.
|
||||
*/
|
||||
public boolean contains(float cx, float cy) {
|
||||
return ((cx > x - xRadius && cx < x + xRadius) &&
|
||||
(cy > y - yRadius && cy < y + yRadius));
|
||||
}
|
||||
}
|
||||
844
src/itdelatrisu/opsu/GameScore.java
Normal file
844
src/itdelatrisu/opsu/GameScore.java
Normal file
@@ -0,0 +1,844 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu;
|
||||
|
||||
import itdelatrisu.opsu.states.Game;
|
||||
import itdelatrisu.opsu.states.Options;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.Graphics;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.util.Log;
|
||||
|
||||
/**
|
||||
* Holds score data and renders all score-related elements.
|
||||
*/
|
||||
public class GameScore {
|
||||
/**
|
||||
* Letter grades.
|
||||
*/
|
||||
public static final int
|
||||
GRADE_SS = 0,
|
||||
GRADE_SSH = 1, // silver
|
||||
GRADE_S = 2,
|
||||
GRADE_SH = 3, // silver
|
||||
GRADE_A = 4,
|
||||
GRADE_B = 5,
|
||||
GRADE_C = 6,
|
||||
GRADE_D = 7,
|
||||
GRADE_MAX = 8; // not a grade
|
||||
|
||||
/**
|
||||
* Hit result types.
|
||||
*/
|
||||
public static final int
|
||||
HIT_MISS = 0,
|
||||
HIT_50 = 1,
|
||||
HIT_100 = 2,
|
||||
HIT_300 = 3,
|
||||
HIT_100K = 4, // 100-Katu
|
||||
HIT_300K = 5, // 300-Katu
|
||||
HIT_300G = 6, // Geki
|
||||
HIT_SLIDER10 = 7,
|
||||
HIT_SLIDER30 = 8,
|
||||
HIT_MAX = 9; // not a hit result
|
||||
|
||||
/**
|
||||
* Hit result-related images (indexed by HIT_* constants).
|
||||
*/
|
||||
private Image[] hitResults;
|
||||
|
||||
/**
|
||||
* Counts of each hit result so far.
|
||||
*/
|
||||
private int[] hitResultCount;
|
||||
|
||||
/**
|
||||
* Total number of hit objects so far, not including Katu/Geki (for calculating grade).
|
||||
*/
|
||||
private int objectCount;
|
||||
|
||||
/**
|
||||
* Total objects including slider hits/ticks (for determining Full Combo status).
|
||||
*/
|
||||
private int fullObjectCount;
|
||||
|
||||
/**
|
||||
* The current combo streak.
|
||||
*/
|
||||
private int combo;
|
||||
|
||||
/**
|
||||
* The max combo streak obtained.
|
||||
*/
|
||||
private int comboMax;
|
||||
|
||||
/**
|
||||
* Hit result types accumulated this streak (bitmask), for Katu/Geki status.
|
||||
* <ul>
|
||||
* <li>&1: 100
|
||||
* <li>&2: 50/Miss
|
||||
* </ul>
|
||||
*/
|
||||
private byte comboEnd;
|
||||
|
||||
/**
|
||||
* Combo burst images.
|
||||
*/
|
||||
private Image[] comboBurstImages;
|
||||
|
||||
/**
|
||||
* Index of the current combo burst image.
|
||||
*/
|
||||
private int comboBurstIndex;
|
||||
|
||||
/**
|
||||
* Alpha level of the current combo burst image (for fade out).
|
||||
*/
|
||||
private float comboBurstAlpha;
|
||||
|
||||
/**
|
||||
* Current x coordinate of the combo burst image (for sliding animation).
|
||||
*/
|
||||
private int comboBurstX;
|
||||
|
||||
/**
|
||||
* List of hit result objects associated with hit objects.
|
||||
*/
|
||||
private LinkedList<OsuHitObjectResult> hitResultList;
|
||||
|
||||
/**
|
||||
* Hit result helper class.
|
||||
*/
|
||||
private class OsuHitObjectResult {
|
||||
public int time; // object start time
|
||||
public int result; // hit result
|
||||
public float x, y; // object coordinates
|
||||
public Color color; // combo color
|
||||
public float alpha = 1f; // alpha level (for fade out)
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param time the result's starting track position
|
||||
* @param result the hit result (HIT_* constants)
|
||||
* @param x the center x coordinate
|
||||
* @param y the center y coordinate
|
||||
* @param color the color of the hit object
|
||||
*/
|
||||
public OsuHitObjectResult(int time, int result, float x, float y, Color color) {
|
||||
this.time = time;
|
||||
this.result = result;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Current game score.
|
||||
*/
|
||||
private long score;
|
||||
|
||||
/**
|
||||
* Current health bar percentage.
|
||||
*/
|
||||
private float health;
|
||||
|
||||
/**
|
||||
* Beatmap HPDrainRate value. (0:easy ~ 10:hard)
|
||||
*/
|
||||
private byte drainRate = 5;
|
||||
|
||||
/**
|
||||
* Beatmap OverallDifficulty value. (0:easy ~ 10:hard)
|
||||
*/
|
||||
private byte difficulty = 5;
|
||||
|
||||
/**
|
||||
* Scorebar-related images.
|
||||
*/
|
||||
private Image
|
||||
bgImage, // background (always rendered)
|
||||
colourImage, // health bar (cropped)
|
||||
kiImage, // end image (50~100% health)
|
||||
kiDangerImage, // end image (25~50% health)
|
||||
kiDanger2Image; // end image (0~25% health)
|
||||
|
||||
/**
|
||||
* Ranking screen images.
|
||||
*/
|
||||
private Image
|
||||
rankingPanel, // panel to display text in
|
||||
perfectImage, // display if full combo
|
||||
rankingImage, // styled text "Ranking"
|
||||
comboImage, // styled text "Combo"
|
||||
accuracyImage; // styled text "Accuracy"
|
||||
|
||||
/**
|
||||
* Default text symbol images.
|
||||
*/
|
||||
private Image[] defaultSymbols;
|
||||
|
||||
/**
|
||||
* Score text symbol images.
|
||||
*/
|
||||
private HashMap<Character, Image> scoreSymbols;
|
||||
|
||||
/**
|
||||
* Letter grade images (large and small sizes).
|
||||
*/
|
||||
private Image[] gradesLarge, gradesSmall;
|
||||
|
||||
/**
|
||||
* Lighting effects, displayed behind hit object results (optional).
|
||||
*/
|
||||
private Image lighting, lighting1;
|
||||
|
||||
/**
|
||||
* Container dimensions.
|
||||
*/
|
||||
private int width, height;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param width container width
|
||||
* @param height container height
|
||||
*/
|
||||
public GameScore(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
hitResults = new Image[HIT_MAX];
|
||||
defaultSymbols = new Image[10];
|
||||
scoreSymbols = new HashMap<Character, Image>(14);
|
||||
gradesLarge = new Image[GRADE_MAX];
|
||||
gradesSmall = new Image[GRADE_MAX];
|
||||
comboBurstImages = new Image[4];
|
||||
|
||||
clear();
|
||||
|
||||
try {
|
||||
initializeImages();
|
||||
} catch (Exception e) {
|
||||
Log.error("Failed to initialize images.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all data and re-initializes object.
|
||||
*/
|
||||
public void clear() {
|
||||
score = 0;
|
||||
health = 100f;
|
||||
hitResultCount = new int[HIT_MAX];
|
||||
hitResultList = new LinkedList<OsuHitObjectResult>();
|
||||
objectCount = 0;
|
||||
fullObjectCount = 0;
|
||||
combo = 0;
|
||||
comboMax = 0;
|
||||
comboEnd = 0;
|
||||
comboBurstIndex = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all images tied to this object.
|
||||
* @throws SlickException
|
||||
*/
|
||||
private void initializeImages() throws SlickException {
|
||||
// scorebar
|
||||
setScorebarImage(
|
||||
new Image("scorebar-bg.png"),
|
||||
new Image("scorebar-colour.png"),
|
||||
new Image("scorebar-ki.png"),
|
||||
new Image("scorebar-kidanger.png"),
|
||||
new Image("scorebar-kidanger2.png")
|
||||
);
|
||||
|
||||
// text symbol images
|
||||
for (int i = 0; i <= 9; i++) {
|
||||
defaultSymbols[i] = new Image(String.format("default-%d.png", i));
|
||||
scoreSymbols.put(Character.forDigit(i, 10), new Image(String.format("score-%d.png", i)));
|
||||
}
|
||||
scoreSymbols.put(',', new Image("score-comma.png"));
|
||||
scoreSymbols.put('.', new Image("score-dot.png"));
|
||||
scoreSymbols.put('%', new Image("score-percent.png"));
|
||||
scoreSymbols.put('x', new Image("score-x.png"));
|
||||
|
||||
// hit result images
|
||||
hitResults[HIT_MISS] = new Image("hit0.png");
|
||||
hitResults[HIT_50] = new Image("hit50.png");
|
||||
hitResults[HIT_100] = new Image("hit100.png");
|
||||
hitResults[HIT_300] = new Image("hit300.png");
|
||||
hitResults[HIT_100K] = new Image("hit100k.png");
|
||||
hitResults[HIT_300K] = new Image("hit300k.png");
|
||||
hitResults[HIT_300G] = new Image("hit300g.png");
|
||||
hitResults[HIT_SLIDER10] = new Image("sliderpoint10.png");
|
||||
hitResults[HIT_SLIDER30] = new Image("sliderpoint30.png");
|
||||
|
||||
// combo burst images
|
||||
for (int i = 0; i <= 3; i++)
|
||||
comboBurstImages[i] = new Image(String.format("comboburst-%d.png", i));
|
||||
|
||||
// lighting image
|
||||
try {
|
||||
lighting = new Image("lighting.png");
|
||||
lighting1 = new Image("lighting1.png");
|
||||
} catch (Exception e) {
|
||||
// optional
|
||||
}
|
||||
|
||||
// letter grade images
|
||||
String[] grades = { "X", "XH", "S", "SH", "A", "B", "C", "D" };
|
||||
for (int i = 0; i < grades.length; i++) {
|
||||
gradesLarge[i] = new Image(String.format("ranking-%s.png", grades[i]));
|
||||
gradesSmall[i] = new Image(String.format("ranking-%s-small.png", grades[i]));
|
||||
}
|
||||
|
||||
// ranking screen elements
|
||||
setRankingImage(
|
||||
new Image("ranking-panel.png"),
|
||||
new Image("ranking-perfect.png"),
|
||||
new Image("ranking-title.png"),
|
||||
new Image("ranking-maxcombo.png"),
|
||||
new Image("ranking-accuracy.png")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a background, health bar, and end image.
|
||||
* @param bgImage background image
|
||||
* @param colourImage health bar image
|
||||
* @param kiImage end image
|
||||
*/
|
||||
public void setScorebarImage(Image bg, Image colour,
|
||||
Image ki, Image kiDanger, Image kiDanger2) {
|
||||
int bgWidth = width / 2;
|
||||
this.bgImage = bg.getScaledCopy(bgWidth, bg.getHeight());
|
||||
this.colourImage = colour.getScaledCopy(bgWidth, colour.getHeight());
|
||||
this.kiImage = ki;
|
||||
this.kiDangerImage = kiDanger;
|
||||
this.kiDanger2Image = kiDanger2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a ranking panel, full combo, and ranking/combo/accuracy text image.
|
||||
* @param rankingPanel ranking panel image
|
||||
* @param perfectImage full combo image
|
||||
* @param rankingImage styled text "Ranking"
|
||||
* @param comboImage styled text "Combo"
|
||||
* @param accuracyImage styled text "Accuracy"
|
||||
*/
|
||||
public void setRankingImage(Image rankingPanel, Image perfectImage,
|
||||
Image rankingImage, Image comboImage, Image accuracyImage) {
|
||||
this.rankingPanel = rankingPanel.getScaledCopy((height * 0.63f) / rankingPanel.getHeight());
|
||||
this.perfectImage = perfectImage.getScaledCopy((height * 0.16f) / perfectImage.getHeight());
|
||||
this.rankingImage = rankingImage.getScaledCopy((height * 0.15f) / rankingImage.getHeight());
|
||||
this.comboImage = comboImage.getScaledCopy((height * 0.05f) / comboImage.getHeight());
|
||||
this.accuracyImage = accuracyImage.getScaledCopy((height * 0.05f) / accuracyImage.getHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a default/score text symbol image for a character.
|
||||
*/
|
||||
public Image getDefaultSymbolImage(int i) { return defaultSymbols[i]; }
|
||||
public Image getScoreSymbolImage(char c) { return scoreSymbols.get(c); }
|
||||
|
||||
/**
|
||||
* Sets or returns the health drain rate.
|
||||
*/
|
||||
public void setDrainRate(byte drainRate) { this.drainRate = drainRate; }
|
||||
public byte getDrainRate() { return drainRate; }
|
||||
|
||||
/**
|
||||
* Sets or returns the difficulty.
|
||||
*/
|
||||
public void setDifficulty(byte difficulty) { this.difficulty = difficulty; }
|
||||
public byte getDifficulty() { return difficulty; }
|
||||
|
||||
/**
|
||||
* Draws a number with defaultSymbols.
|
||||
* @param n the number to draw
|
||||
* @param x the center x coordinate
|
||||
* @param y the center y coordinate
|
||||
* @param scale the scale to apply
|
||||
*/
|
||||
public void drawSymbolNumber(int n, float x, float y, float scale) {
|
||||
int length = (int) (Math.log10(n) + 1);
|
||||
float digitWidth = getDefaultSymbolImage(0).getWidth() * scale;
|
||||
float cx = x + ((length - 1) * (digitWidth / 2));
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
getDefaultSymbolImage(n % 10).getScaledCopy(scale).drawCentered(cx, y);
|
||||
cx -= digitWidth;
|
||||
n /= 10;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Draws a string of scoreSymbols.
|
||||
* @param str the string to draw
|
||||
* @param x the starting x coordinate
|
||||
* @param y the y coordinate
|
||||
* @param scale the scale to apply
|
||||
* @param rightAlign align right (true) or left (false)
|
||||
*/
|
||||
private void drawSymbolString(String str, int x, int y, float scale, boolean rightAlign) {
|
||||
char[] c = str.toCharArray();
|
||||
int cx = x;
|
||||
if (rightAlign) {
|
||||
for (int i = c.length - 1; i >= 0; i--) {
|
||||
Image digit = getScoreSymbolImage(c[i]);
|
||||
if (scale != 1.0f)
|
||||
digit = digit.getScaledCopy(scale);
|
||||
cx -= digit.getWidth();
|
||||
digit.draw(cx, y);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < c.length; i++) {
|
||||
Image digit = getScoreSymbolImage(c[i]);
|
||||
if (scale != 1.0f)
|
||||
digit = digit.getScaledCopy(scale);
|
||||
digit.draw(cx, y);
|
||||
cx += digit.getWidth();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws game elements: scorebar, score, score percentage, combo count, and combo burst.
|
||||
* @param g the graphics context
|
||||
* @param mapLength the length of the beatmap (in ms)
|
||||
* @param breakPeriod if true, will not draw scorebar and combo elements, and will draw grade
|
||||
* @param firstObject true if the first hit object's start time has not yet passed
|
||||
*/
|
||||
public void drawGameElements(Graphics g, int mapLength, boolean breakPeriod, boolean firstObject) {
|
||||
// score
|
||||
drawSymbolString(String.format("%08d", score),
|
||||
width - 2, 0, 1.0f, true);
|
||||
|
||||
// score percentage
|
||||
String scorePercentage = String.format("%02.2f%%", getScorePercent());
|
||||
drawSymbolString(scorePercentage, width - 2, getScoreSymbolImage('0').getHeight(), 0.75f, true);
|
||||
|
||||
// map progress circle
|
||||
g.setAntiAlias(true);
|
||||
g.setLineWidth(2f);
|
||||
g.setColor(Color.white);
|
||||
int circleX = width - (getScoreSymbolImage('0').getWidth() * scorePercentage.length());
|
||||
int circleY = getScoreSymbolImage('0').getHeight();
|
||||
float circleDiameter = getScoreSymbolImage('0').getHeight() * 0.75f;
|
||||
g.drawOval(circleX, circleY, circleDiameter, circleDiameter);
|
||||
|
||||
int firstObjectTime = Game.getOsuFile().objects[0].time;
|
||||
int trackPosition = MusicController.getPosition();
|
||||
if (trackPosition > firstObjectTime) {
|
||||
g.fillArc(circleX, circleY, circleDiameter, circleDiameter,
|
||||
-90, -90 + (int) (360f * (trackPosition - firstObjectTime) / mapLength)
|
||||
);
|
||||
}
|
||||
|
||||
if (!breakPeriod) {
|
||||
// scorebar
|
||||
float healthRatio = health / 100f;
|
||||
if (firstObject) { // gradually move ki before map begins
|
||||
if (firstObjectTime >= 1500 && trackPosition < firstObjectTime - 500)
|
||||
healthRatio = (float) trackPosition / (firstObjectTime - 500);
|
||||
}
|
||||
bgImage.draw(0, 0);
|
||||
Image colourCropped = colourImage.getSubImage(0, 0, (int) (colourImage.getWidth() * healthRatio), colourImage.getHeight());
|
||||
colourCropped.draw(0, bgImage.getHeight() / 4f);
|
||||
if (health >= 50f)
|
||||
kiImage.drawCentered(colourCropped.getWidth(), kiImage.getHeight() / 2f);
|
||||
else if (health >= 25f)
|
||||
kiDangerImage.drawCentered(colourCropped.getWidth(), kiDangerImage.getHeight() / 2f);
|
||||
else
|
||||
kiDanger2Image.drawCentered(colourCropped.getWidth(), kiDanger2Image.getHeight() / 2f);
|
||||
|
||||
// combo burst
|
||||
if (comboBurstIndex != -1 && comboBurstAlpha > 0f) {
|
||||
Image comboBurst = comboBurstImages[comboBurstIndex];
|
||||
comboBurst.setAlpha(comboBurstAlpha);
|
||||
comboBurstImages[comboBurstIndex].draw(comboBurstX, height - comboBurst.getHeight());
|
||||
}
|
||||
|
||||
// combo count
|
||||
if (combo > 0) // 0 isn't a combo
|
||||
drawSymbolString(String.format("%dx", combo), 10, height - 10 - getScoreSymbolImage('0').getHeight(), 1.0f, false);
|
||||
} else {
|
||||
// grade
|
||||
Image grade = gradesSmall[getGrade()];
|
||||
float gradeScale = circleY * 0.75f / grade.getHeight();
|
||||
gradesSmall[getGrade()].getScaledCopy(gradeScale).draw(
|
||||
circleX - grade.getWidth(), circleY
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws ranking elements: score, results, ranking.
|
||||
* @param g the graphics context
|
||||
* @param width the width of the container
|
||||
* @param height the height of the container
|
||||
*/
|
||||
public void drawRankingElements(Graphics g, int width, int height) {
|
||||
// grade
|
||||
Image grade = gradesLarge[getGrade()];
|
||||
float gradeScale = (height * 0.5f) / grade.getHeight();
|
||||
grade = grade.getScaledCopy(gradeScale);
|
||||
grade.draw(width - grade.getWidth(), height * 0.09f);
|
||||
|
||||
// header & "Ranking" text
|
||||
float rankingHeight = (rankingImage.getHeight() * 0.75f) + 3;
|
||||
g.setColor(Options.COLOR_BLACK_ALPHA);
|
||||
g.fillRect(0, 0, width, rankingHeight);
|
||||
rankingImage.draw((width * 0.97f) - rankingImage.getWidth(), 0);
|
||||
|
||||
// ranking panel
|
||||
int rankingPanelWidth = rankingPanel.getWidth();
|
||||
int rankingPanelHeight = rankingPanel.getHeight();
|
||||
rankingPanel.draw(0, rankingHeight - (rankingHeight / 10f));
|
||||
|
||||
float symbolTextScale = (height / 15f) / getScoreSymbolImage('0').getHeight();
|
||||
float rankResultScale = (height * 0.03f) / hitResults[HIT_300].getHeight();
|
||||
|
||||
// score
|
||||
drawSymbolString((score / 100000000 == 0) ? String.format("%08d", score) : Long.toString(score),
|
||||
(int) (width * 0.18f), height / 6, symbolTextScale, false);
|
||||
|
||||
// result counts
|
||||
float resultInitialX = rankingPanelWidth * 0.20f;
|
||||
float resultInitialY = rankingHeight + (rankingPanelHeight * 0.27f) + (rankingHeight / 10f);
|
||||
float resultHitInitialX = rankingPanelWidth * 0.05f;
|
||||
float resultHitInitialY = resultInitialY + (getScoreSymbolImage('0').getHeight() * symbolTextScale);
|
||||
float resultOffsetX = rankingPanelWidth / 2f;
|
||||
float resultOffsetY = rankingPanelHeight * 0.2f;
|
||||
|
||||
int[] rankDrawOrder = { HIT_300, HIT_300G, HIT_100, HIT_100K, HIT_50, HIT_MISS };
|
||||
int[] rankResultOrder = {
|
||||
hitResultCount[HIT_300], hitResultCount[HIT_300G],
|
||||
hitResultCount[HIT_100], hitResultCount[HIT_100K] + hitResultCount[HIT_300K],
|
||||
hitResultCount[HIT_50], hitResultCount[HIT_MISS]
|
||||
};
|
||||
|
||||
for (int i = 0; i < rankDrawOrder.length; i += 2) {
|
||||
hitResults[rankDrawOrder[i]].getScaledCopy(rankResultScale).draw(
|
||||
resultHitInitialX, resultHitInitialY - (hitResults[rankDrawOrder[i]].getHeight() * rankResultScale) + (resultOffsetY * (i / 2)));
|
||||
hitResults[rankDrawOrder[i+1]].getScaledCopy(rankResultScale).draw(
|
||||
resultHitInitialX + resultOffsetX, resultHitInitialY - (hitResults[rankDrawOrder[i]].getHeight() * rankResultScale) + (resultOffsetY * (i / 2)));
|
||||
drawSymbolString(String.format("%dx", rankResultOrder[i]),
|
||||
(int) resultInitialX, (int) (resultInitialY + (resultOffsetY * (i / 2))), symbolTextScale, false);
|
||||
drawSymbolString(String.format("%dx", rankResultOrder[i+1]),
|
||||
(int) (resultInitialX + resultOffsetX), (int) (resultInitialY + (resultOffsetY * (i / 2))), symbolTextScale, false);
|
||||
}
|
||||
|
||||
// combo and accuracy
|
||||
float textY = rankingHeight + (rankingPanelHeight * 0.87f) - (rankingHeight / 10f);
|
||||
float numbersX = comboImage.getWidth() * .07f;
|
||||
float numbersY = textY + comboImage.getHeight() * 0.7f;
|
||||
comboImage.draw(width * 0.01f, textY);
|
||||
accuracyImage.draw(rankingPanelWidth / 2f, textY);
|
||||
drawSymbolString(String.format("%dx", comboMax),
|
||||
(int) (width * 0.01f + numbersX), (int) numbersY, symbolTextScale, false);
|
||||
drawSymbolString(String.format("%02.2f%%", getScorePercent()),
|
||||
(int) (rankingPanelWidth / 2f + numbersX), (int) numbersY, symbolTextScale, false);
|
||||
|
||||
// full combo
|
||||
if (combo == fullObjectCount)
|
||||
perfectImage.draw(width * 0.08f, (height * 0.99f) - perfectImage.getHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws stored hit results and removes them from the list as necessary.
|
||||
* @param trackPosition the current track position
|
||||
*/
|
||||
public void drawHitResults(int trackPosition) {
|
||||
int fadeDelay = 500;
|
||||
|
||||
Iterator<OsuHitObjectResult> iter = hitResultList.iterator();
|
||||
while (iter.hasNext()) {
|
||||
OsuHitObjectResult hitResult = iter.next();
|
||||
if (hitResult.time + fadeDelay > trackPosition) {
|
||||
hitResults[hitResult.result].setAlpha(hitResult.alpha);
|
||||
hitResult.alpha = 1 - ((float) (trackPosition - hitResult.time) / fadeDelay);
|
||||
hitResults[hitResult.result].drawCentered(hitResult.x, hitResult.y);
|
||||
|
||||
// hit lighting
|
||||
if (Options.isHitLightingEnabled() && lighting != null &&
|
||||
hitResult.result != HIT_MISS && hitResult.result != HIT_SLIDER30 && hitResult.result != HIT_SLIDER10) {
|
||||
float scale = 1f + ((trackPosition - hitResult.time) / (float) fadeDelay);
|
||||
Image scaledLighting = lighting.getScaledCopy(scale);
|
||||
scaledLighting.draw(hitResult.x - (scaledLighting.getWidth() / 2f),
|
||||
hitResult.y - (scaledLighting.getHeight() / 2f),
|
||||
hitResult.color);
|
||||
if (lighting1 != null) {
|
||||
Image scaledLighting1 = lighting1.getScaledCopy(scale);
|
||||
scaledLighting1.draw(hitResult.x - (scaledLighting1.getWidth() / 2f),
|
||||
hitResult.y - (scaledLighting1.getHeight() / 2f),
|
||||
hitResult.color);
|
||||
}
|
||||
}
|
||||
} else
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes health by a given percentage, modified by drainRate.
|
||||
*/
|
||||
public void changeHealth(float percent) {
|
||||
// TODO: drainRate formula
|
||||
health += percent;
|
||||
if (health > 100f)
|
||||
health = 100f;
|
||||
if (health < 0f)
|
||||
health = 0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns health percentage.
|
||||
*/
|
||||
public float getHealth() { return health; }
|
||||
|
||||
/**
|
||||
* Returns false if health is zero.
|
||||
* If "No Fail" or "Auto" mods are active, this will always return true.
|
||||
*/
|
||||
public boolean isAlive() {
|
||||
return (health > 0f ||
|
||||
Options.isModActive(Options.MOD_NO_FAIL) ||
|
||||
Options.isModActive(Options.MOD_AUTO));
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes score by a raw value (not affected by other modifiers).
|
||||
*/
|
||||
public void changeScore(int value) { score += value; }
|
||||
|
||||
/**
|
||||
* Returns score percentage (raw score only).
|
||||
*/
|
||||
private float getScorePercent() {
|
||||
float percent = 0;
|
||||
if (objectCount > 0)
|
||||
percent = ((hitResultCount[HIT_50] * 50) + (hitResultCount[HIT_100] * 100)
|
||||
+ (hitResultCount[HIT_300] * 300)) / (objectCount * 300f) * 100f;
|
||||
return percent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns (current) letter grade.
|
||||
*/
|
||||
private int getGrade() {
|
||||
if (objectCount < 1) // avoid division by zero
|
||||
return GRADE_D;
|
||||
|
||||
// TODO: silvers
|
||||
float percent = getScorePercent();
|
||||
float hit300ratio = hitResultCount[HIT_300] * 100f / objectCount;
|
||||
float hit50ratio = hitResultCount[HIT_50] * 100f / objectCount;
|
||||
boolean noMiss = (hitResultCount[HIT_MISS] == 0);
|
||||
if (percent >= 100f)
|
||||
return GRADE_SS;
|
||||
else if (hit300ratio >= 90f && hit50ratio < 1.0f && noMiss)
|
||||
return GRADE_S;
|
||||
else if ((hit300ratio >= 80f && noMiss) || hit300ratio >= 90f)
|
||||
return GRADE_A;
|
||||
else if ((hit300ratio >= 70f && noMiss) || hit300ratio >= 80f)
|
||||
return GRADE_B;
|
||||
else if (hit300ratio >= 60f)
|
||||
return GRADE_C;
|
||||
else
|
||||
return GRADE_D;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates combo burst data based on a delta value.
|
||||
*/
|
||||
public void updateComboBurst(int delta) {
|
||||
if (comboBurstIndex > -1 && Options.isComboBurstEnabled()) {
|
||||
int leftX = 0;
|
||||
int rightX = width - comboBurstImages[comboBurstIndex].getWidth();
|
||||
if (comboBurstX < leftX) {
|
||||
comboBurstX += (delta / 2f);
|
||||
if (comboBurstX > leftX)
|
||||
comboBurstX = leftX;
|
||||
} else if (comboBurstX > rightX) {
|
||||
comboBurstX -= (delta / 2f);
|
||||
if (comboBurstX < rightX)
|
||||
comboBurstX = rightX;
|
||||
} else if (comboBurstAlpha > 0f) {
|
||||
comboBurstAlpha -= (delta / 1200f);
|
||||
if (comboBurstAlpha < 0f)
|
||||
comboBurstAlpha = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the combo streak by one.
|
||||
*/
|
||||
private void incrementComboStreak() {
|
||||
combo++;
|
||||
if (combo > comboMax)
|
||||
comboMax = combo;
|
||||
|
||||
// combo bursts (at 30, 60, 100+50x)
|
||||
if (Options.isComboBurstEnabled() &&
|
||||
(combo == 30 || combo == 60 || (combo >= 100 && combo % 50 == 0))) {
|
||||
if (combo == 30)
|
||||
comboBurstIndex = 0;
|
||||
else
|
||||
comboBurstIndex = (comboBurstIndex + 1) % comboBurstImages.length;
|
||||
comboBurstAlpha = 0.8f;
|
||||
if ((comboBurstIndex % 2) == 0)
|
||||
comboBurstX = width;
|
||||
else
|
||||
comboBurstX = comboBurstImages[0].getWidth() * -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public void sliderTickResult(int time, int result, float x, float y) {
|
||||
int hitValue = 0;
|
||||
switch (result) {
|
||||
case HIT_SLIDER30:
|
||||
hitValue = 30;
|
||||
incrementComboStreak();
|
||||
changeHealth(1f);
|
||||
break;
|
||||
case HIT_SLIDER10:
|
||||
hitValue = 10;
|
||||
incrementComboStreak();
|
||||
break;
|
||||
case HIT_MISS:
|
||||
combo = 0;
|
||||
if (Options.isModActive(Options.MOD_SUDDEN_DEATH))
|
||||
health = 0f;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
fullObjectCount++;
|
||||
|
||||
if (hitValue > 0) {
|
||||
score += hitValue;
|
||||
hitResultList.add(new OsuHitObjectResult(time, result, x, y, null));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a hit result.
|
||||
* @param time the object start time
|
||||
* @param result the hit result (HIT_* constants)
|
||||
* @param x the x coordinate
|
||||
* @param y the y coordinate
|
||||
* @param color the combo color
|
||||
* @param end true if this is the last hit object in the combo
|
||||
*/
|
||||
public void hitResult(int time, int result, float x, float y, Color color, boolean end) {
|
||||
int hitValue = 0;
|
||||
switch (result) {
|
||||
case HIT_300:
|
||||
hitValue = 300;
|
||||
changeHealth(5f);
|
||||
objectCount++;
|
||||
break;
|
||||
case HIT_100:
|
||||
hitValue = 100;
|
||||
changeHealth(2f);
|
||||
comboEnd |= 1;
|
||||
objectCount++;
|
||||
break;
|
||||
case HIT_50:
|
||||
hitValue = 50;
|
||||
comboEnd |= 2;
|
||||
objectCount++;
|
||||
break;
|
||||
case HIT_MISS:
|
||||
hitValue = 0;
|
||||
changeHealth(-10f);
|
||||
comboEnd |= 2;
|
||||
combo = 0;
|
||||
if (Options.isModActive(Options.MOD_SUDDEN_DEATH))
|
||||
health = 0f;
|
||||
objectCount++;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (hitValue > 0) {
|
||||
// game mod score multipliers
|
||||
float modMultiplier = 1f;
|
||||
if (Options.isModActive(Options.MOD_NO_FAIL))
|
||||
modMultiplier *= 0.5f;
|
||||
if (Options.isModActive(Options.MOD_HARD_ROCK))
|
||||
modMultiplier *= 1.06f;
|
||||
if (Options.isModActive(Options.MOD_SPUN_OUT))
|
||||
modMultiplier *= 0.9f;
|
||||
// not implemented:
|
||||
// EASY (0.5x), HALF_TIME (0.3x),
|
||||
// DOUBLE_TIME (1.12x), HIDDEN (1.06x), FLASHLIGHT (1.12x)
|
||||
|
||||
/**
|
||||
* [SCORE FORMULA]
|
||||
* Score = Hit Value + Hit Value * (Combo * Difficulty * Mod) / 25
|
||||
* - Hit Value: hit result (50, 100, 300), slider ticks, spinner bonus
|
||||
* - Combo: combo before this hit - 1 (minimum 0)
|
||||
* - Difficulty: the beatmap difficulty
|
||||
* - Mod: mod multipliers
|
||||
*/
|
||||
score += (hitValue + (hitValue * (Math.max(combo - 1, 0) * difficulty * modMultiplier) / 25));
|
||||
incrementComboStreak();
|
||||
}
|
||||
hitResultCount[result]++;
|
||||
fullObjectCount++;
|
||||
|
||||
// last element in combo: check for Geki/Katu
|
||||
if (end) {
|
||||
if (comboEnd == 0) {
|
||||
result = HIT_300G;
|
||||
changeHealth(15f);
|
||||
hitResultCount[result]++;
|
||||
} else if ((comboEnd & 2) == 0) {
|
||||
if (result == HIT_100) {
|
||||
result = HIT_100K;
|
||||
changeHealth(10f);
|
||||
hitResultCount[result]++;
|
||||
} else if (result == HIT_300) {
|
||||
result = HIT_300K;
|
||||
changeHealth(10f);
|
||||
hitResultCount[result]++;
|
||||
}
|
||||
}
|
||||
comboEnd = 0;
|
||||
}
|
||||
|
||||
hitResultList.add(new OsuHitObjectResult(time, result, x, y, color));
|
||||
}
|
||||
}
|
||||
346
src/itdelatrisu/opsu/MusicController.java
Normal file
346
src/itdelatrisu/opsu/MusicController.java
Normal file
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu;
|
||||
|
||||
import itdelatrisu.opsu.states.Options;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javazoom.jl.converter.Converter;
|
||||
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.openal.AL;
|
||||
import org.lwjgl.openal.AL10;
|
||||
import org.newdawn.slick.Music;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.openal.Audio;
|
||||
import org.newdawn.slick.openal.SoundStore;
|
||||
import org.newdawn.slick.util.Log;
|
||||
|
||||
/**
|
||||
* Controller for all music.
|
||||
*/
|
||||
public class MusicController {
|
||||
/**
|
||||
* The current music track.
|
||||
*/
|
||||
private static Music player;
|
||||
|
||||
/**
|
||||
* The last OsuFile passed to play().
|
||||
*/
|
||||
private static OsuFile lastOsu;
|
||||
|
||||
/**
|
||||
* Temporary WAV file for file conversions (to be deleted).
|
||||
*/
|
||||
private static File wavFile;
|
||||
|
||||
/**
|
||||
* Thread for MP3 conversions.
|
||||
*/
|
||||
private static Thread conversion;
|
||||
|
||||
// This class should not be instantiated.
|
||||
private MusicController() {}
|
||||
|
||||
/**
|
||||
* Plays an audio file at the preview position.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public static void play(final OsuFile osu, final boolean loop) {
|
||||
if (lastOsu == null || !osu.audioFilename.equals(lastOsu.audioFilename)) {
|
||||
lastOsu = osu;
|
||||
|
||||
// TODO: properly interrupt instead of using deprecated conversion.stop();
|
||||
// interrupt the conversion
|
||||
if (isConverting())
|
||||
// conversion.interrupt();
|
||||
conversion.stop();
|
||||
|
||||
if (wavFile != null)
|
||||
wavFile.delete();
|
||||
|
||||
// releases all sources from previous tracks
|
||||
destroyOpenAL();
|
||||
|
||||
switch (OsuParser.getExtension(osu.audioFilename.getName())) {
|
||||
case "ogg":
|
||||
loadTrack(osu.audioFilename, osu.previewTime, loop);
|
||||
break;
|
||||
case "mp3":
|
||||
// convert MP3 to WAV in a new conversion
|
||||
conversion = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
convertMp3(osu.audioFilename);
|
||||
// if (!Thread.currentThread().isInterrupted())
|
||||
loadTrack(wavFile, osu.previewTime, loop);
|
||||
}
|
||||
};
|
||||
conversion.start();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a track and plays it.
|
||||
*/
|
||||
private static void loadTrack(File file, int previewTime, boolean loop) {
|
||||
try { // create a new player
|
||||
player = new Music(file.getPath());
|
||||
playAt((previewTime > 0) ? previewTime : 0, loop);
|
||||
} catch (Exception e) {
|
||||
Log.error(String.format("Could not play track '%s'.", file.getName()), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the current track at the given position.
|
||||
*/
|
||||
public static void playAt(final int position, final boolean loop) {
|
||||
if (trackExists()) {
|
||||
SoundStore.get().setMusicVolume(Options.getMusicVolume());
|
||||
player.setPosition(position / 1000f);
|
||||
if (loop)
|
||||
player.loop();
|
||||
else
|
||||
player.play();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an MP3 file to a temporary WAV file.
|
||||
*/
|
||||
private static File convertMp3(File file) {
|
||||
try {
|
||||
wavFile = File.createTempFile(".osu", ".wav", Options.TMP_DIR);
|
||||
wavFile.deleteOnExit();
|
||||
|
||||
Converter converter = new Converter();
|
||||
converter.convert(file.getPath(), wavFile.getPath());
|
||||
return wavFile;
|
||||
} catch (Exception e) {
|
||||
Log.error(String.format("Failed to play file '%s'.", file.getAbsolutePath()), e);
|
||||
}
|
||||
return wavFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a conversion is running.
|
||||
*/
|
||||
public static boolean isConverting() {
|
||||
return (conversion != null && conversion.isAlive());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a track is loaded.
|
||||
*/
|
||||
public static boolean trackExists() {
|
||||
return (player != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the current track.
|
||||
*/
|
||||
public static String getTrackName() {
|
||||
if (!trackExists() || lastOsu == null)
|
||||
return null;
|
||||
return lastOsu.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the artist of the current track.
|
||||
*/
|
||||
public static String getArtistName() {
|
||||
if (!trackExists() || lastOsu == null)
|
||||
return null;
|
||||
return lastOsu.artist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current track is playing.
|
||||
*/
|
||||
public static boolean isPlaying() {
|
||||
return (trackExists() && player.playing());
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the current track.
|
||||
*/
|
||||
public static void pause() {
|
||||
if (isPlaying())
|
||||
player.pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the current track.
|
||||
*/
|
||||
public static void resume() {
|
||||
if (trackExists()) {
|
||||
player.resume();
|
||||
player.setVolume(1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the current track.
|
||||
*/
|
||||
public static void stop() {
|
||||
if (isPlaying())
|
||||
player.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fades out the track.
|
||||
*/
|
||||
public static void fadeOut(int duration) {
|
||||
if (isPlaying())
|
||||
player.fade(duration, 0f, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position in the current track.
|
||||
* If no track is playing, 0f will be returned.
|
||||
*/
|
||||
public static int getPosition() {
|
||||
if (isPlaying())
|
||||
return Math.max((int) (player.getPosition() * 1000 + Options.getMusicOffset()), 0);
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks to a position in the current track.
|
||||
*/
|
||||
public static boolean setPosition(int position) {
|
||||
return (trackExists() && player.setPosition(position / 1000f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the length of the track, in milliseconds.
|
||||
* Returns 0f if no file is loaded or an MP3 is currently being converted.
|
||||
* @author bdk (http://slick.ninjacave.com/forum/viewtopic.php?t=2699)
|
||||
*/
|
||||
public static int getTrackLength() {
|
||||
if (!trackExists() || isConverting())
|
||||
return 0;
|
||||
|
||||
float duration = 0f;
|
||||
try {
|
||||
// get Music object's (private) Audio object reference
|
||||
Field sound = player.getClass().getDeclaredField("sound");
|
||||
sound.setAccessible(true);
|
||||
Audio audio = (Audio) (sound.get(player));
|
||||
|
||||
// access Audio object's (private)'length' field
|
||||
Field length = audio.getClass().getDeclaredField("length");
|
||||
length.setAccessible(true);
|
||||
duration = (float) (length.get(audio));
|
||||
} catch (Exception e) {
|
||||
Log.error("Could not get track length.", e);
|
||||
}
|
||||
return (int) (duration * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the length of the track as a formatted string (M:SS).
|
||||
* Returns "--" if an MP3 is currently being converted.
|
||||
*/
|
||||
public static String getTrackLengthString() {
|
||||
if (isConverting())
|
||||
return "...";
|
||||
|
||||
int duration = getTrackLength();
|
||||
return String.format("%d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(duration),
|
||||
TimeUnit.MILLISECONDS.toSeconds(duration) -
|
||||
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(duration)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops and releases all sources, clears each of the specified Audio
|
||||
* buffers, destroys the OpenAL context, and resets SoundStore for future use.
|
||||
*
|
||||
* Calling SoundStore.get().init() will re-initialize the OpenAL context
|
||||
* after a call to destroyOpenAL (Note: AudioLoader.getXXX calls init for you).
|
||||
*
|
||||
* @author davedes (http://slick.ninjacave.com/forum/viewtopic.php?t=3920)
|
||||
*/
|
||||
private static void destroyOpenAL() {
|
||||
if (!trackExists())
|
||||
return;
|
||||
stop();
|
||||
|
||||
try {
|
||||
// get Music object's (private) Audio object reference
|
||||
Field sound = player.getClass().getDeclaredField("sound");
|
||||
sound.setAccessible(true);
|
||||
Audio audio = (Audio) (sound.get(player));
|
||||
|
||||
// first clear the sources allocated by SoundStore
|
||||
int max = SoundStore.get().getSourceCount();
|
||||
IntBuffer buf = BufferUtils.createIntBuffer(max);
|
||||
for (int i = 0; i < max; i++) {
|
||||
int source = SoundStore.get().getSource(i);
|
||||
buf.put(source);
|
||||
|
||||
// stop and detach any buffers at this source
|
||||
AL10.alSourceStop(source);
|
||||
AL10.alSourcei(source, AL10.AL_BUFFER, 0);
|
||||
}
|
||||
buf.flip();
|
||||
AL10.alDeleteSources(buf);
|
||||
int exc = AL10.alGetError();
|
||||
if (exc != AL10.AL_NO_ERROR) {
|
||||
throw new SlickException(
|
||||
"Could not clear SoundStore sources, err: " + exc);
|
||||
}
|
||||
|
||||
// delete any buffer data stored in memory, too...
|
||||
if (audio != null && audio.getBufferID() != 0) {
|
||||
buf = BufferUtils.createIntBuffer(1).put(audio.getBufferID());
|
||||
buf.flip();
|
||||
AL10.alDeleteBuffers(buf);
|
||||
exc = AL10.alGetError();
|
||||
if (exc != AL10.AL_NO_ERROR) {
|
||||
throw new SlickException("Could not clear buffer "
|
||||
+ audio.getBufferID()
|
||||
+ ", err: "+exc);
|
||||
}
|
||||
}
|
||||
|
||||
// clear OpenAL
|
||||
AL.destroy();
|
||||
|
||||
// reset SoundStore so that next time we create a Sound/Music, it will reinit
|
||||
SoundStore.get().clear();
|
||||
|
||||
player = null;
|
||||
} catch (Exception e) {
|
||||
Log.error("Failed to destroy OpenAL.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
152
src/itdelatrisu/opsu/Opsu.java
Normal file
152
src/itdelatrisu/opsu/Opsu.java
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu;
|
||||
|
||||
import itdelatrisu.opsu.states.Game;
|
||||
import itdelatrisu.opsu.states.GamePauseMenu;
|
||||
import itdelatrisu.opsu.states.GameRanking;
|
||||
import itdelatrisu.opsu.states.MainMenu;
|
||||
import itdelatrisu.opsu.states.MainMenuExit;
|
||||
import itdelatrisu.opsu.states.Options;
|
||||
import itdelatrisu.opsu.states.SongMenu;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.PrintStream;
|
||||
|
||||
import org.newdawn.slick.AppGameContainer;
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.state.StateBasedGame;
|
||||
import org.newdawn.slick.state.transition.FadeInTransition;
|
||||
import org.newdawn.slick.state.transition.FadeOutTransition;
|
||||
import org.newdawn.slick.util.DefaultLogSystem;
|
||||
import org.newdawn.slick.util.FileSystemLocation;
|
||||
import org.newdawn.slick.util.Log;
|
||||
import org.newdawn.slick.util.ResourceLoader;
|
||||
|
||||
/**
|
||||
* Main class.
|
||||
* <p>
|
||||
* Creates game container, adds all other states, and initializes song data.
|
||||
*/
|
||||
public class Opsu extends StateBasedGame {
|
||||
/**
|
||||
* Song group structure (each group contains of an ArrayList of OsuFiles).
|
||||
*/
|
||||
public static OsuGroupList groups = new OsuGroupList();
|
||||
|
||||
/**
|
||||
* Game states.
|
||||
*/
|
||||
public static final int
|
||||
STATE_OPTIONS = 1,
|
||||
STATE_MAINMENU = 2,
|
||||
STATE_MAINMENUEXIT = 3,
|
||||
STATE_SONGMENU = 4,
|
||||
STATE_GAME = 5,
|
||||
STATE_GAMEPAUSEMENU = 6,
|
||||
STATE_GAMERANKING = 7;
|
||||
|
||||
public Opsu(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initStatesList(GameContainer container) throws SlickException {
|
||||
addState(new Options(STATE_OPTIONS));
|
||||
addState(new MainMenu(STATE_MAINMENU));
|
||||
addState(new MainMenuExit(STATE_MAINMENUEXIT));
|
||||
addState(new SongMenu(STATE_SONGMENU));
|
||||
addState(new Game(STATE_GAME));
|
||||
addState(new GamePauseMenu(STATE_GAMEPAUSEMENU));
|
||||
addState(new GameRanking(STATE_GAMERANKING));
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
// set path for lwjgl natives - NOT NEEDED if using JarSplice
|
||||
// System.setProperty("org.lwjgl.librarypath", new File("native").getAbsolutePath());
|
||||
|
||||
// set the resource path
|
||||
ResourceLoader.addResourceLocation(new FileSystemLocation(new File("./res/")));
|
||||
|
||||
// log all errors to a file
|
||||
Log.setVerbose(false);
|
||||
try {
|
||||
DefaultLogSystem.out = new PrintStream(new FileOutputStream(Options.LOG_FILE, true));
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.error(e);
|
||||
}
|
||||
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
Log.error("** Uncaught Exception! **", e);
|
||||
}
|
||||
});
|
||||
|
||||
// start the game
|
||||
Opsu osuGame = new Opsu("opsu!");
|
||||
try {
|
||||
AppGameContainer app = new AppGameContainer(osuGame);
|
||||
|
||||
// basic game settings
|
||||
Options.parseOptions();
|
||||
int[] containerSize = Options.getContainerSize();
|
||||
app.setDisplayMode(containerSize[0], containerSize[1], false);
|
||||
String[] icons = { "icon16.png", "icon32.png" };
|
||||
app.setIcons(icons);
|
||||
|
||||
// parse song directory
|
||||
OsuParser.parseAllFiles(Options.getBeatmapDir(),
|
||||
app.getWidth(), app.getHeight()
|
||||
);
|
||||
|
||||
// clear the cache
|
||||
if (!Options.TMP_DIR.mkdir()) {
|
||||
for (File tmp : Options.TMP_DIR.listFiles())
|
||||
tmp.delete();
|
||||
}
|
||||
Options.TMP_DIR.deleteOnExit();
|
||||
|
||||
app.start();
|
||||
} catch (SlickException e) {
|
||||
Log.error("Error while creating game container.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean closeRequested() {
|
||||
int id = this.getCurrentStateID();
|
||||
|
||||
// intercept close requests in game-related states and return to song menu
|
||||
if (id == STATE_GAME || id == STATE_GAMEPAUSEMENU || id == STATE_GAMERANKING) {
|
||||
// start playing track at preview position
|
||||
MusicController.pause();
|
||||
MusicController.playAt(Game.getOsuFile().previewTime, true);
|
||||
this.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
return false;
|
||||
}
|
||||
|
||||
Options.saveOptions();
|
||||
((AppGameContainer) this.getContainer()).destroy();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
158
src/itdelatrisu/opsu/OsuFile.java
Normal file
158
src/itdelatrisu/opsu/OsuFile.java
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.util.Log;
|
||||
|
||||
/**
|
||||
* Data type storing parsed data from OSU files.
|
||||
*/
|
||||
public class OsuFile implements Comparable<OsuFile> {
|
||||
/**
|
||||
* The OSU File object associated with this OsuFile.
|
||||
*/
|
||||
private File file;
|
||||
|
||||
/* [General] */
|
||||
public File audioFilename; // audio file object
|
||||
public int audioLeadIn = 0; // delay before music starts (in ms)
|
||||
// public String audioHash = ""; // audio hash (deprecated)
|
||||
public int previewTime = -1; // start position of music preview (in ms)
|
||||
public byte countdown = 0; // countdown type (0:disabled, 1:normal, 2:half, 3:double)
|
||||
public String sampleSet = ""; // ? ("Normal", "Soft")
|
||||
public float stackLeniency = 0.7f; // how often closely placed hit objects will be stacked together
|
||||
public byte mode = 0; // game mode (0:osu!, 1:taiko, 2:catch the beat, 3:osu!mania)
|
||||
public boolean letterboxInBreaks = false; // whether the letterbox (top/bottom black bars) appears during breaks
|
||||
public boolean widescreenStoryboard = false;// whether the storyboard should be widescreen
|
||||
public boolean epilepsyWarning = false; // whether to show an epilepsy warning
|
||||
|
||||
/* [Editor] */
|
||||
/* Not implemented. */
|
||||
// public int[] bookmarks; // list of editor bookmarks (in ms)
|
||||
// public float distanceSpacing = 0f; // multiplier for "Distance Snap"
|
||||
// public byte beatDivisor = 0; // beat division
|
||||
// public int gridSize = 0; // size of grid for "Grid Snap"
|
||||
// public int timelineZoom = 0; // zoom in the editor timeline
|
||||
|
||||
/* [Metadata] */
|
||||
public String title = ""; // song title
|
||||
public String titleUnicode = ""; // song title (unicode)
|
||||
public String artist = ""; // song artist
|
||||
public String artistUnicode = ""; // song artist (unicode)
|
||||
public String creator = ""; // beatmap creator
|
||||
public String version = ""; // beatmap difficulty
|
||||
public String source = ""; // song source
|
||||
// public String[] tags; // song tags, for searching -> different structure
|
||||
public int beatmapID = 0; // beatmap ID
|
||||
public int beatmapSetID = 0; // beatmap set ID
|
||||
|
||||
/* [Difficulty] */
|
||||
public byte HPDrainRate = 5; // HP drain (0:easy ~ 10:hard)
|
||||
public byte circleSize = 4; // size of circles
|
||||
public byte overallDifficulty = 5; // affects timing window, spinners, and approach speed (0:easy ~ 10:hard)
|
||||
public byte approachRate = -1; // how long circles stay on the screen (0:long ~ 10:short) **not in old format**
|
||||
public float sliderMultiplier = 1f; // slider movement speed multiplier
|
||||
public float sliderTickRate = 1f; // rate at which slider ticks are placed (x per beat)
|
||||
|
||||
/* [Events] */
|
||||
//Background and Video events (0)
|
||||
public String bg; // background image path
|
||||
private Image bgImage; // background image (created when needed)
|
||||
// public Video bgVideo; // background video (not implemented)
|
||||
//Break Periods (2)
|
||||
public ArrayList<Integer> breaks; // break periods (start time, end time, ...)
|
||||
//Storyboard elements (not implemented)
|
||||
|
||||
/* [TimingPoints] */
|
||||
public ArrayList<OsuTimingPoint> timingPoints; // timing points
|
||||
int bpmMin = 0, bpmMax = 0; // min and max BPM
|
||||
|
||||
/* [Colours] */
|
||||
public Color[] combo; // combo colors (R,G,B), max 5
|
||||
|
||||
/* [HitObjects] */
|
||||
public OsuHitObject[] objects; // hit objects
|
||||
public int hitObjectCircle = 0; // number of circles
|
||||
public int hitObjectSlider = 0; // number of sliders
|
||||
public int hitObjectSpinner = 0; // number of spinners
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param file the file associated with this OsuFile
|
||||
*/
|
||||
public OsuFile(File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the associated file object.
|
||||
*/
|
||||
public File getFile() { return file; }
|
||||
|
||||
/**
|
||||
* Draws the background associated with the OsuFile.
|
||||
* @param width the container width
|
||||
* @param height the container height
|
||||
* @param alpha the alpha value
|
||||
* @return true if successful, false if any errors were produced
|
||||
*/
|
||||
public boolean drawBG(int width, int height, float alpha) {
|
||||
if (bg == null)
|
||||
return false;
|
||||
try {
|
||||
if (bgImage == null)
|
||||
bgImage = new Image(bg).getScaledCopy(width, height);
|
||||
bgImage.setAlpha(alpha);
|
||||
bgImage.draw();
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to get background image '%s'.", bg), e);
|
||||
bg = null; // don't try to load the file again until a restart
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two OsuFile objects first by overall difficulty, then by total objects.
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(OsuFile that) {
|
||||
int cmp = Byte.compare(this.overallDifficulty, that.overallDifficulty);
|
||||
if (cmp == 0)
|
||||
cmp = Integer.compare(
|
||||
this.hitObjectCircle + this.hitObjectSlider + this.hitObjectSpinner,
|
||||
that.hitObjectCircle + that.hitObjectSlider + that.hitObjectSpinner
|
||||
);
|
||||
return cmp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted string: "Artist - Title [Version]"
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s - %s [%s]", artist, title, version);
|
||||
}
|
||||
}
|
||||
313
src/itdelatrisu/opsu/OsuGroupList.java
Normal file
313
src/itdelatrisu/opsu/OsuGroupList.java
Normal file
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* Indexed, expanding, doubly-linked list data type for song groups.
|
||||
*/
|
||||
public class OsuGroupList {
|
||||
/**
|
||||
* Sorting orders.
|
||||
*/
|
||||
public static final byte
|
||||
SORT_TITLE = 0,
|
||||
SORT_ARTIST = 1,
|
||||
SORT_CREATOR = 2,
|
||||
SORT_BPM = 3,
|
||||
SORT_MAX = 4; // not a sort
|
||||
|
||||
/**
|
||||
* Sorting order names (indexed by SORT_* constants).
|
||||
*/
|
||||
public static final String[] SORT_NAMES = {
|
||||
"Title", "Artist", "Creator", "BPM"
|
||||
};
|
||||
|
||||
/**
|
||||
* List containing all parsed nodes.
|
||||
*/
|
||||
private ArrayList<OsuGroupNode> parsedNodes;
|
||||
|
||||
/**
|
||||
* Total number of maps (i.e. OsuFile objects).
|
||||
*/
|
||||
private int mapCount = 0;
|
||||
|
||||
/**
|
||||
* Song tags.
|
||||
* Each tag value is a HashSet which points song group ArrayLists.
|
||||
*/
|
||||
private HashMap<String, HashSet<ArrayList<OsuFile>>> tags;
|
||||
|
||||
/**
|
||||
* Current list of nodes.
|
||||
* (For searches; otherwise, a pointer to parsedNodes.)
|
||||
*/
|
||||
private ArrayList<OsuGroupNode> nodes;
|
||||
|
||||
/**
|
||||
* Index of current expanded node.
|
||||
* If no node is expanded, the value will be -1.
|
||||
*/
|
||||
private int expandedIndex = -1;
|
||||
|
||||
/**
|
||||
* The last search query.
|
||||
*/
|
||||
private String lastQuery = "";
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public OsuGroupList() {
|
||||
parsedNodes = new ArrayList<OsuGroupNode>();
|
||||
nodes = parsedNodes;
|
||||
tags = new HashMap<String, HashSet<ArrayList<OsuFile>>>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of elements.
|
||||
*/
|
||||
public int size() { return nodes.size(); }
|
||||
|
||||
/**
|
||||
* Adds a song group.
|
||||
* @param osuFiles the list of OsuFile objects in the group
|
||||
*/
|
||||
public void addSongGroup(ArrayList<OsuFile> osuFiles) {
|
||||
parsedNodes.add(new OsuGroupNode(osuFiles));
|
||||
mapCount += osuFiles.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of parsed maps (i.e. OsuFile objects).
|
||||
*/
|
||||
public int getMapCount() { return mapCount; }
|
||||
|
||||
/**
|
||||
* Adds a tag.
|
||||
* @param tag the tag string (key)
|
||||
* @param osuFiles the song group associated with the tag (value)
|
||||
*/
|
||||
public void addTag(String tag, ArrayList<OsuFile> osuFiles) {
|
||||
tag = tag.toLowerCase();
|
||||
HashSet<ArrayList<OsuFile>> tagSet = tags.get(tag);
|
||||
if (tagSet == null) {
|
||||
tagSet = new HashSet<ArrayList<OsuFile>>();
|
||||
tags.put(tag, tagSet);
|
||||
}
|
||||
tagSet.add(osuFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OsuGroupNode at an index, disregarding expansions.
|
||||
*/
|
||||
public OsuGroupNode getBaseNode(int index) {
|
||||
if (index < 0 || index >= size())
|
||||
return null;
|
||||
|
||||
return nodes.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random base node.
|
||||
*/
|
||||
public OsuGroupNode getRandomNode() {
|
||||
return getBaseNode((int) (Math.random() * size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OsuGroupNode a given number of positions forward or backwards.
|
||||
* @param node the starting node
|
||||
* @param shift the number of nodes to shift forward (+) or backward (-).
|
||||
*/
|
||||
public OsuGroupNode getNode(OsuGroupNode node, int shift) {
|
||||
OsuGroupNode startNode = node;
|
||||
if (shift > 0) {
|
||||
for (int i = 0; i < shift && startNode != null; i++)
|
||||
startNode = startNode.next;
|
||||
} else {
|
||||
for (int i = 0; i < shift && startNode != null; i++)
|
||||
startNode = startNode.prev;
|
||||
}
|
||||
return startNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the expanded node (or -1 if nothing is expanded).
|
||||
*/
|
||||
public int getExpandedIndex() { return expandedIndex; }
|
||||
|
||||
/**
|
||||
* Expands the node at an index by inserting a new node for each OsuFile in that node.
|
||||
*/
|
||||
public void expand(int index) {
|
||||
// undo the previous expansion
|
||||
if (unexpand() == index)
|
||||
return; // don't re-expand the same node
|
||||
|
||||
OsuGroupNode node = getBaseNode(index);
|
||||
if (node == null)
|
||||
return;
|
||||
|
||||
ArrayList<OsuFile> osuFiles = node.osuFiles;
|
||||
OsuGroupNode nextNode = node.next;
|
||||
for (int i = 0; i < node.osuFiles.size(); i++) {
|
||||
OsuGroupNode newNode = new OsuGroupNode(osuFiles);
|
||||
newNode.index = index;
|
||||
newNode.osuFileIndex = i;
|
||||
newNode.prev = node;
|
||||
|
||||
node.next = newNode;
|
||||
node = node.next;
|
||||
}
|
||||
if (nextNode != null) {
|
||||
node.next = nextNode;
|
||||
nextNode.prev = node;
|
||||
}
|
||||
|
||||
expandedIndex = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Undoes the current expansion, if any.
|
||||
* @return the index of the previously expanded node, or -1 if no expansions
|
||||
*/
|
||||
private int unexpand() {
|
||||
if (expandedIndex < 0 || expandedIndex >= size())
|
||||
return -1;
|
||||
|
||||
// reset [base].next and [base+1].prev
|
||||
OsuGroupNode eCur = getBaseNode(expandedIndex);
|
||||
OsuGroupNode eNext = getBaseNode(expandedIndex + 1);
|
||||
eCur.next = eNext;
|
||||
if (eNext != null)
|
||||
eNext.prev = eCur;
|
||||
|
||||
int oldIndex = expandedIndex;
|
||||
expandedIndex = -1;
|
||||
return oldIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the links in the list, given a sorting order (SORT_* constants).
|
||||
*/
|
||||
public void init(byte order) {
|
||||
if (size() < 1)
|
||||
return;
|
||||
|
||||
// sort the list
|
||||
switch (order) {
|
||||
case SORT_TITLE:
|
||||
Collections.sort(nodes);
|
||||
break;
|
||||
case SORT_ARTIST:
|
||||
Collections.sort(nodes, new OsuGroupNode.ArtistOrder());
|
||||
break;
|
||||
case SORT_CREATOR:
|
||||
Collections.sort(nodes, new OsuGroupNode.CreatorOrder());
|
||||
break;
|
||||
case SORT_BPM:
|
||||
Collections.sort(nodes, new OsuGroupNode.BPMOrder());
|
||||
break;
|
||||
}
|
||||
expandedIndex = -1;
|
||||
|
||||
// create links
|
||||
OsuGroupNode lastNode = nodes.get(0);
|
||||
lastNode.index = 0;
|
||||
lastNode.prev = null;
|
||||
for (int i = 1, size = size(); i < size; i++) {
|
||||
OsuGroupNode node = nodes.get(i);
|
||||
lastNode.next = node;
|
||||
node.index = i;
|
||||
node.prev = lastNode;
|
||||
|
||||
lastNode = node;
|
||||
}
|
||||
lastNode.next = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new list of song groups in which each group contains a match to a search query.
|
||||
* @param query the search query (tag terms separated by spaces)
|
||||
* @return false if query is the same as the previous one, true otherwise
|
||||
*/
|
||||
public boolean search(String query) {
|
||||
if (query == null)
|
||||
return false;
|
||||
|
||||
// don't redo the same search
|
||||
query = query.trim().toLowerCase();
|
||||
if (lastQuery != null && query.equals(lastQuery))
|
||||
return false;
|
||||
lastQuery = query;
|
||||
|
||||
// if empty query, reset to original list
|
||||
if (query.isEmpty()) {
|
||||
nodes = parsedNodes;
|
||||
return true;
|
||||
}
|
||||
|
||||
// tag search: check if each word is contained in global tag structure
|
||||
HashSet<ArrayList<OsuFile>> taggedGroups = new HashSet<ArrayList<OsuFile>>();
|
||||
String[] terms = query.split("\\s+");
|
||||
for (String term : terms) {
|
||||
if (tags.containsKey(term))
|
||||
taggedGroups.addAll(tags.get(term)); // add all matches
|
||||
}
|
||||
|
||||
// traverse parsedNodes, comparing each element with the query
|
||||
nodes = new ArrayList<OsuGroupNode>();
|
||||
for (OsuGroupNode node : parsedNodes) {
|
||||
// search: tags
|
||||
if (taggedGroups.contains(node.osuFiles)) {
|
||||
nodes.add(node);
|
||||
continue;
|
||||
}
|
||||
|
||||
OsuFile osu = node.osuFiles.get(0);
|
||||
|
||||
// search: title, artist, creator, source, version (first OsuFile)
|
||||
if (osu.title.toLowerCase().contains(query) ||
|
||||
osu.artist.toLowerCase().contains(query) ||
|
||||
osu.creator.toLowerCase().contains(query) ||
|
||||
osu.source.toLowerCase().contains(query) ||
|
||||
osu.version.toLowerCase().contains(query)) {
|
||||
nodes.add(node);
|
||||
continue;
|
||||
}
|
||||
|
||||
// search: versions (all OsuFiles)
|
||||
for (int i = 1; i < node.osuFiles.size(); i++) {
|
||||
if (node.osuFiles.get(i).version.toLowerCase().contains(query)) {
|
||||
nodes.add(node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
191
src/itdelatrisu/opsu/OsuGroupNode.java
Normal file
191
src/itdelatrisu/opsu/OsuGroupNode.java
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu;
|
||||
|
||||
import itdelatrisu.opsu.states.Options;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.Image;
|
||||
|
||||
/**
|
||||
* Node in an OsuGroupList representing a group of OsuFile objects.
|
||||
*/
|
||||
public class OsuGroupNode implements Comparable<OsuGroupNode> {
|
||||
/**
|
||||
* Menu background image.
|
||||
*/
|
||||
private static Image bg;
|
||||
|
||||
/**
|
||||
* List of associated OsuFile objects.
|
||||
*/
|
||||
public ArrayList<OsuFile> osuFiles;
|
||||
|
||||
/**
|
||||
* Index of this OsuGroupNode.
|
||||
*/
|
||||
public int index = 0;
|
||||
|
||||
/**
|
||||
* Index of selected osuFile.
|
||||
* If not focused, the value will be -1.
|
||||
*/
|
||||
public int osuFileIndex = -1;
|
||||
|
||||
/**
|
||||
* Links to other OsuGroupNode objects.
|
||||
*/
|
||||
public OsuGroupNode prev, next;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param osuFiles the OsuFile objects in this group
|
||||
*/
|
||||
public OsuGroupNode(ArrayList<OsuFile> osuFiles) {
|
||||
this.osuFiles = osuFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two OsuGroupNode objects by title.
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(OsuGroupNode that) {
|
||||
return this.osuFiles.get(0).title.compareToIgnoreCase(that.osuFiles.get(0).title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two OsuGroupNode objects by artist.
|
||||
*/
|
||||
public static class ArtistOrder implements Comparator<OsuGroupNode> {
|
||||
@Override
|
||||
public int compare(OsuGroupNode v, OsuGroupNode w) {
|
||||
return v.osuFiles.get(0).artist.compareToIgnoreCase(w.osuFiles.get(0).artist);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two OsuGroupNode objects by creator.
|
||||
*/
|
||||
public static class CreatorOrder implements Comparator<OsuGroupNode> {
|
||||
@Override
|
||||
public int compare(OsuGroupNode v, OsuGroupNode w) {
|
||||
return v.osuFiles.get(0).creator.compareToIgnoreCase(w.osuFiles.get(0).creator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two OsuGroupNode objects by BPM.
|
||||
*/
|
||||
public static class BPMOrder implements Comparator<OsuGroupNode> {
|
||||
@Override
|
||||
public int compare(OsuGroupNode v, OsuGroupNode w) {
|
||||
return Integer.compare(v.osuFiles.get(0).bpmMax, w.osuFiles.get(0).bpmMax);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a button background image.
|
||||
*/
|
||||
public static void setBackground(Image background) {
|
||||
bg = background;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the button.
|
||||
* @param x the x coordinate
|
||||
* @param y the y coordinate
|
||||
* @param focus true if this is the focused node
|
||||
*/
|
||||
public void draw(float x, float y, boolean focus) {
|
||||
boolean expanded = (osuFileIndex > -1);
|
||||
float xOffset = 0f;
|
||||
OsuFile osu;
|
||||
Color textColor = Color.lightGray;
|
||||
|
||||
if (expanded) { // expanded
|
||||
if (focus) {
|
||||
xOffset = bg.getWidth() * -0.05f;
|
||||
bg.draw(x + xOffset, y, Color.white);
|
||||
textColor = Color.white;
|
||||
} else {
|
||||
xOffset = bg.getWidth() * 0.05f;
|
||||
bg.draw(x + xOffset, y, Options.COLOR_BLUE_BUTTON);
|
||||
}
|
||||
osu = osuFiles.get(osuFileIndex);
|
||||
} else {
|
||||
bg.draw(x, y, Options.COLOR_ORANGE_BUTTON);
|
||||
osu = osuFiles.get(0);
|
||||
}
|
||||
|
||||
float cx = x + (bg.getWidth() * 0.05f) + xOffset;
|
||||
float cy = y + (bg.getHeight() * 0.2f) - 3;
|
||||
|
||||
Options.FONT_MEDIUM.drawString(cx, cy, osu.title, textColor);
|
||||
Options.FONT_DEFAULT.drawString(cx, cy + Options.FONT_MEDIUM.getLineHeight() - 4, String.format("%s // %s", osu.artist, osu.creator), textColor);
|
||||
if (expanded)
|
||||
Options.FONT_BOLD.drawString(cx, cy + Options.FONT_MEDIUM.getLineHeight() + Options.FONT_DEFAULT.getLineHeight() - 8, osu.version, textColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of strings containing song information.
|
||||
* <ul>
|
||||
* <li>0: {Artist} - {Title} [{Version}]
|
||||
* <li>1: Mapped by {Creator}
|
||||
* <li>2: Length: {} BPM: {} Objects: {}
|
||||
* <li>3: Circles: {} Sliders: {} Spinners: {}
|
||||
* <li>4: CS:{} HP:{} AR:{} OD:{}
|
||||
* </ul>
|
||||
*/
|
||||
public String[] getInfo() {
|
||||
if (osuFileIndex < 0)
|
||||
return null;
|
||||
|
||||
OsuFile osu = osuFiles.get(osuFileIndex);
|
||||
String[] info = new String[5];
|
||||
info[0] = osu.toString();
|
||||
info[1] = String.format("Mapped by %s",
|
||||
osu.creator);
|
||||
info[2] = String.format("Length: %s BPM: %s Objects: %d",
|
||||
MusicController.getTrackLengthString(),
|
||||
(osu.bpmMax <= 0) ? "--" :
|
||||
((osu.bpmMin == osu.bpmMax) ? osu.bpmMin : String.format("%d-%d", osu.bpmMin, osu.bpmMax)),
|
||||
(osu.hitObjectCircle + osu.hitObjectSlider + osu.hitObjectSpinner));
|
||||
info[3] = String.format("Circles: %d Sliders: %d Spinners: %d",
|
||||
osu.hitObjectCircle, osu.hitObjectSlider, osu.hitObjectSpinner);
|
||||
info[4] = String.format("CS:%d HP:%d AR:%d OD:%d",
|
||||
osu.circleSize, osu.HPDrainRate, osu.approachRate, osu.overallDifficulty);
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted string for the OsuFile at osuFileIndex:
|
||||
* "Artist - Title [Version]" (version omitted if osuFileIndex is invalid)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
if (osuFileIndex == -1)
|
||||
return String.format("%s - %s", osuFiles.get(0).artist, osuFiles.get(0).title);
|
||||
else
|
||||
return osuFiles.get(osuFileIndex).toString();
|
||||
}
|
||||
}
|
||||
92
src/itdelatrisu/opsu/OsuHitObject.java
Normal file
92
src/itdelatrisu/opsu/OsuHitObject.java
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu;
|
||||
|
||||
/**
|
||||
* Data type representing a hit object.
|
||||
*/
|
||||
public class OsuHitObject {
|
||||
/**
|
||||
* Hit object types (bits).
|
||||
*/
|
||||
public static final int
|
||||
TYPE_CIRCLE = 1,
|
||||
TYPE_SLIDER = 2,
|
||||
TYPE_NEWCOMBO = 4, // not an object
|
||||
TYPE_SPINNER = 8;
|
||||
|
||||
/**
|
||||
* Hit sound types.
|
||||
*/
|
||||
public static final byte
|
||||
SOUND_NORMAL = 0,
|
||||
SOUND_WHISTLE = 2,
|
||||
SOUND_FINISH = 4,
|
||||
SOUND_WHISTLEFINISH = 6,
|
||||
SOUND_CLAP = 8;
|
||||
|
||||
/**
|
||||
* Slider curve types.
|
||||
* (Deprecated: only Beziers are currently used.)
|
||||
*/
|
||||
public static final char
|
||||
SLIDER_CATMULL = 'C',
|
||||
SLIDER_BEZIER = 'B',
|
||||
SLIDER_LINEAR = 'L',
|
||||
SLIDER_PASSTHROUGH = 'P';
|
||||
|
||||
/**
|
||||
* Max hit object coordinates.
|
||||
*/
|
||||
public static final int
|
||||
MAX_X = 512,
|
||||
MAX_Y = 384;
|
||||
|
||||
// parsed fields (coordinates are scaled)
|
||||
public float x, y; // start coordinates
|
||||
public int time; // start time, in ms
|
||||
public int type; // hit object type
|
||||
public byte hitSound; // hit sound type
|
||||
public char sliderType; // slider curve type (sliders only)
|
||||
public float[] sliderX; // slider x coordinate list (sliders only)
|
||||
public float[] sliderY; // slider y coordinate list (sliders only)
|
||||
public int repeat; // slider repeat count (sliders only)
|
||||
public float pixelLength; // slider pixel length (sliders only)
|
||||
public int endTime; // end time, in ms (spinners only)
|
||||
|
||||
// additional v10+ parameters not implemented...
|
||||
// addition -> sampl:add:cust:vol:hitsound
|
||||
// edge_hitsound, edge_addition (sliders only)
|
||||
|
||||
// extra fields
|
||||
public int comboIndex; // current index in Color array
|
||||
public int comboNumber; // number to display in hit object
|
||||
|
||||
/**
|
||||
* Constructor with all required fields.
|
||||
*/
|
||||
public OsuHitObject(float x, float y, int time, int type, byte hitSound) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.time = time;
|
||||
this.type = type;
|
||||
this.hitSound = hitSound;
|
||||
}
|
||||
|
||||
}
|
||||
553
src/itdelatrisu/opsu/OsuParser.java
Normal file
553
src/itdelatrisu/opsu/OsuParser.java
Normal file
@@ -0,0 +1,553 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu;
|
||||
|
||||
import itdelatrisu.opsu.states.Options;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FilenameFilter;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.util.Log;
|
||||
|
||||
/**
|
||||
* Parser for OSU files.
|
||||
*/
|
||||
public class OsuParser {
|
||||
/**
|
||||
* The x and y multipliers for hit object coordinates.
|
||||
*/
|
||||
private static float xMultiplier, yMultiplier;
|
||||
|
||||
/**
|
||||
* The x and y offsets for hit object coordinates.
|
||||
*/
|
||||
private static int
|
||||
xOffset, // offset right of border
|
||||
yOffset; // offset below health bar
|
||||
|
||||
// This class should not be instantiated.
|
||||
private OsuParser() {}
|
||||
|
||||
/**
|
||||
* Invokes parser for each OSU file in a root directory.
|
||||
* @param root the root directory (search has depth 1)
|
||||
* @param width the container width
|
||||
* @param height the container height
|
||||
*/
|
||||
public static void parseAllFiles(File root, int width, int height) {
|
||||
// set coordinate modifiers
|
||||
xMultiplier = (width * 0.6f) / OsuHitObject.MAX_X;
|
||||
yMultiplier = (height * 0.6f) / OsuHitObject.MAX_Y;
|
||||
xOffset = width / 5;
|
||||
yOffset = height / 5;
|
||||
|
||||
for (File folder : root.listFiles()) {
|
||||
if (!folder.isDirectory())
|
||||
continue;
|
||||
File[] files = folder.listFiles(new FilenameFilter() {
|
||||
@Override
|
||||
public boolean accept(File dir, String name) {
|
||||
return name.toLowerCase().endsWith(".osu");
|
||||
}
|
||||
});
|
||||
if (files.length < 1)
|
||||
continue;
|
||||
|
||||
// create a new group entry
|
||||
ArrayList<OsuFile> osuFiles = new ArrayList<OsuFile>();
|
||||
for (File file : files) {
|
||||
// Parse hit objects only when needed to save time/memory.
|
||||
// Change boolean to 'true' to parse them immediately.
|
||||
parseFile(file, osuFiles, false);
|
||||
}
|
||||
if (!osuFiles.isEmpty()) { // add entry if non-empty
|
||||
Collections.sort(osuFiles);
|
||||
Opsu.groups.addSongGroup(osuFiles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an OSU file.
|
||||
* @param file the file to parse
|
||||
* @param osuFiles the song group
|
||||
* @param parseObjects if true, hit objects will be fully parsed now
|
||||
* @return the new OsuFile object
|
||||
*/
|
||||
private static OsuFile parseFile(File file, ArrayList<OsuFile> osuFiles, boolean parseObjects) {
|
||||
OsuFile osu = new OsuFile(file);
|
||||
String tags = ""; // parse at end, only if valid OsuFile
|
||||
|
||||
try (BufferedReader in = new BufferedReader(new FileReader(file))) {
|
||||
|
||||
// copy the default combo colors
|
||||
osu.combo = Arrays.copyOf(Options.DEFAULT_COMBO, Options.DEFAULT_COMBO.length);
|
||||
// initialize timing point list
|
||||
osu.timingPoints = new ArrayList<OsuTimingPoint>();
|
||||
|
||||
String line = in.readLine();
|
||||
String tokens[];
|
||||
while (line != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line)) {
|
||||
line = in.readLine();
|
||||
continue;
|
||||
}
|
||||
switch (line) {
|
||||
case "[General]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
tokens = tokenize(line);
|
||||
switch (tokens[0]) {
|
||||
case "AudioFilename":
|
||||
osu.audioFilename = new File(file.getParent() + File.separator + tokens[1]);
|
||||
break;
|
||||
case "AudioLeadIn":
|
||||
osu.audioLeadIn = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
// case "AudioHash": // deprecated
|
||||
// osu.audioHash = tokens[1];
|
||||
// break;
|
||||
case "PreviewTime":
|
||||
osu.previewTime = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
case "Countdown":
|
||||
osu.countdown = Byte.parseByte(tokens[1]);
|
||||
break;
|
||||
case "SampleSet":
|
||||
osu.sampleSet = tokens[1];
|
||||
break;
|
||||
case "StackLeniency":
|
||||
osu.stackLeniency = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
case "Mode":
|
||||
osu.mode = Byte.parseByte(tokens[1]);
|
||||
|
||||
/* Non-Opsu! standard files not implemented (obviously). */
|
||||
if (osu.mode != 0)
|
||||
return null;
|
||||
|
||||
break;
|
||||
case "LetterboxInBreaks":
|
||||
osu.letterboxInBreaks = (Integer.parseInt(tokens[1]) == 1);
|
||||
break;
|
||||
case "WidescreenStoryboard":
|
||||
osu.widescreenStoryboard = (Integer.parseInt(tokens[1]) == 1);
|
||||
break;
|
||||
case "EpilepsyWarning":
|
||||
osu.epilepsyWarning = (Integer.parseInt(tokens[1]) == 1);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "[Editor]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
/* Not implemented. */
|
||||
// tokens = tokenize(line);
|
||||
// switch (tokens[0]) {
|
||||
// case "Bookmarks":
|
||||
// String[] bookmarks = tokens[1].split(",");
|
||||
// osu.bookmarks = new int[bookmarks.length];
|
||||
// for (int i = 0; i < bookmarks.length; i++)
|
||||
// osu.bookmarks[i] = Integer.parseInt(bookmarks[i]);
|
||||
// break;
|
||||
// case "DistanceSpacing":
|
||||
// osu.distanceSpacing = Float.parseFloat(tokens[1]);
|
||||
// break;
|
||||
// case "BeatDivisor":
|
||||
// osu.beatDivisor = Byte.parseByte(tokens[1]);
|
||||
// break;
|
||||
// case "GridSize":
|
||||
// osu.gridSize = Integer.parseInt(tokens[1]);
|
||||
// break;
|
||||
// case "TimelineZoom":
|
||||
// osu.timelineZoom = Integer.parseInt(tokens[1]);
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
}
|
||||
break;
|
||||
case "[Metadata]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
tokens = tokenize(line);
|
||||
switch (tokens[0]) {
|
||||
case "Title":
|
||||
osu.title = tokens[1];
|
||||
break;
|
||||
case "TitleUnicode":
|
||||
osu.titleUnicode = tokens[1];
|
||||
break;
|
||||
case "Artist":
|
||||
osu.artist = tokens[1];
|
||||
break;
|
||||
case "ArtistUnicode":
|
||||
osu.artistUnicode = tokens[1];
|
||||
break;
|
||||
case "Creator":
|
||||
osu.creator = tokens[1];
|
||||
break;
|
||||
case "Version":
|
||||
osu.version = tokens[1];
|
||||
break;
|
||||
case "Source":
|
||||
osu.source = tokens[1];
|
||||
break;
|
||||
case "Tags":
|
||||
tags = tokens[1];
|
||||
break;
|
||||
case "BeatmapID":
|
||||
osu.beatmapID = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
case "BeatmapSetID":
|
||||
osu.beatmapSetID = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "[Difficulty]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
tokens = tokenize(line);
|
||||
switch (tokens[0]) {
|
||||
case "HPDrainRate":
|
||||
osu.HPDrainRate = Byte.parseByte(tokens[1]);
|
||||
break;
|
||||
case "CircleSize":
|
||||
osu.circleSize = Byte.parseByte(tokens[1]);
|
||||
break;
|
||||
case "OverallDifficulty":
|
||||
osu.overallDifficulty = Byte.parseByte(tokens[1]);
|
||||
break;
|
||||
case "ApproachRate":
|
||||
osu.approachRate = Byte.parseByte(tokens[1]);
|
||||
break;
|
||||
case "SliderMultiplier":
|
||||
osu.sliderMultiplier = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
case "SliderTickRate":
|
||||
osu.sliderTickRate = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (osu.approachRate == -1) // not in old format
|
||||
osu.approachRate = osu.overallDifficulty;
|
||||
break;
|
||||
case "[Events]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
tokens = line.split(",");
|
||||
switch (tokens[0]) {
|
||||
case "0": // background
|
||||
tokens[2] = tokens[2].replaceAll("^\"|\"$", "");
|
||||
String ext = OsuParser.getExtension(tokens[2]);
|
||||
if (ext.equals("jpg") || ext.equals("png"))
|
||||
osu.bg = file.getParent() + File.separator + tokens[2];
|
||||
break;
|
||||
case "2": // break periods
|
||||
if (osu.breaks == null) // optional, create if needed
|
||||
osu.breaks = new ArrayList<Integer>();
|
||||
osu.breaks.add(Integer.parseInt(tokens[1]));
|
||||
osu.breaks.add(Integer.parseInt(tokens[2]));
|
||||
break;
|
||||
default:
|
||||
/* Not implemented. */
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "[TimingPoints]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
tokens = line.split(",");
|
||||
|
||||
OsuTimingPoint timingPoint = new OsuTimingPoint();
|
||||
try { // newer file versions have many new fields
|
||||
timingPoint.time = (int) Float.parseFloat(tokens[0]); //rare float
|
||||
timingPoint.meter = Integer.parseInt(tokens[2]);
|
||||
timingPoint.sampleType = Byte.parseByte(tokens[3]);
|
||||
timingPoint.sampleTypeCustom = Byte.parseByte(tokens[4]);
|
||||
timingPoint.sampleVolume = Integer.parseInt(tokens[5]);
|
||||
timingPoint.inherited = (Integer.parseInt(tokens[6]) == 1);
|
||||
timingPoint.kiai = (Integer.parseInt(tokens[7]) == 1);
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
// TODO: better support for old formats
|
||||
// Log.error(String.format("Error while parsing TimingPoints, line: '%s'.", line), e);
|
||||
}
|
||||
|
||||
// tokens[1] is either beatLength (positive) or velocity (negative)
|
||||
float beatLength = Float.parseFloat(tokens[1]);
|
||||
if (beatLength > 0) {
|
||||
timingPoint.beatLength = beatLength;
|
||||
int bpm = Math.round(60000 / beatLength);
|
||||
if (osu.bpmMin == 0)
|
||||
osu.bpmMin = osu.bpmMax = bpm;
|
||||
else if (bpm < osu.bpmMin)
|
||||
osu.bpmMin = bpm;
|
||||
else if (bpm > osu.bpmMax)
|
||||
osu.bpmMax = bpm;
|
||||
} else
|
||||
timingPoint.velocity = (int) beatLength;
|
||||
|
||||
osu.timingPoints.add(timingPoint);
|
||||
}
|
||||
break;
|
||||
case "[Colours]":
|
||||
LinkedList<Color> colors = new LinkedList<Color>();
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
tokens = tokenize(line);
|
||||
switch (tokens[0]) {
|
||||
case "Combo1":
|
||||
case "Combo2":
|
||||
case "Combo3":
|
||||
case "Combo4":
|
||||
case "Combo5":
|
||||
case "Combo6":
|
||||
case "Combo7":
|
||||
case "Combo8":
|
||||
String[] rgb = tokens[1].split(",");
|
||||
colors.add(new Color(
|
||||
Integer.parseInt(rgb[0]),
|
||||
Integer.parseInt(rgb[1]),
|
||||
Integer.parseInt(rgb[2])
|
||||
));
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!colors.isEmpty())
|
||||
osu.combo = colors.toArray(new Color[colors.size()]);
|
||||
break;
|
||||
case "[HitObjects]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
/* Only type counts parsed at this time. */
|
||||
tokens = line.split(",");
|
||||
int type = Integer.parseInt(tokens[3]);
|
||||
if ((type & OsuHitObject.TYPE_CIRCLE) > 0)
|
||||
osu.hitObjectCircle++;
|
||||
else if ((type & OsuHitObject.TYPE_SLIDER) > 0)
|
||||
osu.hitObjectSlider++;
|
||||
else //if ((type & OsuHitObject.TYPE_SPINNER) > 0)
|
||||
osu.hitObjectSpinner++;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
line = in.readLine().trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.error(String.format("Failed to read file '%s'.", file.getAbsolutePath()), e);
|
||||
}
|
||||
|
||||
// if no custom colors, use the default color scheme
|
||||
if (osu.combo == null)
|
||||
osu.combo = Options.DEFAULT_COMBO;
|
||||
|
||||
// add tags
|
||||
if (!tags.isEmpty()) {
|
||||
for (String tag : tags.split(" "))
|
||||
Opsu.groups.addTag(tag, osuFiles);
|
||||
}
|
||||
|
||||
// parse hit objects now?
|
||||
if (parseObjects)
|
||||
parseHitObjects(osu);
|
||||
|
||||
// add OsuFile to song group
|
||||
osuFiles.add(osu);
|
||||
return osu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses all hit objects in an OSU file.
|
||||
*
|
||||
* Object formats:
|
||||
* - Circles [1]:
|
||||
* x,y,time,type,hitSound,addition
|
||||
* 256,148,9466,1,2,0:0:0:0:
|
||||
*
|
||||
* - Sliders [2]:
|
||||
* x,y,time,type,hitSound,sliderType|curveX:curveY|...,repeat,pixelLength,edgeHitsound,edgeAddition,addition
|
||||
* 300,68,4591,2,0,B|372:100|332:172|420:192,2,180,2|2|2,0:0|0:0|0:0,0:0:0:0:
|
||||
*
|
||||
* - Spinners [8]:
|
||||
* x,y,time,type,hitSound,endTime,addition
|
||||
* 256,192,654,12,0,4029,0:0:0:0:
|
||||
*
|
||||
* Notes:
|
||||
* - 'addition' is optional, and defaults to "0:0:0:0:".
|
||||
* - Field descriptions are located in OsuHitObject.java.
|
||||
*/
|
||||
public static void parseHitObjects(OsuFile osu) {
|
||||
if (osu.objects != null) // already parsed
|
||||
return;
|
||||
|
||||
osu.objects = new OsuHitObject[(osu.hitObjectCircle
|
||||
+ osu.hitObjectSlider + osu.hitObjectSpinner)];
|
||||
|
||||
try (BufferedReader in = new BufferedReader(new FileReader(osu.getFile()))) {
|
||||
String line = in.readLine();
|
||||
while (line != null) {
|
||||
line = line.trim();
|
||||
if (!line.equals("[HitObjects]")) {
|
||||
line = in.readLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
int i = 0; // object index
|
||||
int comboIndex = 0; // color index
|
||||
int comboNumber = 1; // combo number
|
||||
String tokens[], sliderTokens[], sliderXY[];
|
||||
|
||||
while ((line = in.readLine()) != null && i < osu.objects.length) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
// create a new OsuHitObject for each line
|
||||
tokens = line.split(",");
|
||||
float scaledX = Integer.parseInt(tokens[0]) * xMultiplier + xOffset;
|
||||
float scaledY = Integer.parseInt(tokens[1]) * yMultiplier + yOffset;
|
||||
int type = Integer.parseInt(tokens[3]);
|
||||
osu.objects[i] = new OsuHitObject(
|
||||
scaledX, scaledY, Integer.parseInt(tokens[2]),
|
||||
type, Byte.parseByte(tokens[4])
|
||||
);
|
||||
if ((type & OsuHitObject.TYPE_CIRCLE) > 0) {
|
||||
/* 'addition' not implemented. */
|
||||
} else if ((type & OsuHitObject.TYPE_SLIDER) > 0) {
|
||||
// slider curve type and coordinates
|
||||
sliderTokens = tokens[5].split("\\|");
|
||||
osu.objects[i].sliderType = sliderTokens[0].charAt(0);
|
||||
osu.objects[i].sliderX = new float[sliderTokens.length-1];
|
||||
osu.objects[i].sliderY = new float[sliderTokens.length-1];
|
||||
for (int j = 1; j < sliderTokens.length; j++) {
|
||||
sliderXY = sliderTokens[j].split(":");
|
||||
osu.objects[i].sliderX[j-1] = Integer.parseInt(sliderXY[0]) * xMultiplier + xOffset;
|
||||
osu.objects[i].sliderY[j-1] = Integer.parseInt(sliderXY[1]) * yMultiplier + yOffset;
|
||||
}
|
||||
|
||||
osu.objects[i].repeat = Integer.parseInt(tokens[6]);
|
||||
osu.objects[i].pixelLength = Float.parseFloat(tokens[7]);
|
||||
/* edge fields and 'addition' not implemented. */
|
||||
} else { //if ((type & OsuHitObject.TYPE_SPINNER) > 0) {
|
||||
// some 'endTime' fields contain a ':' character (?)
|
||||
int index = tokens[5].indexOf(':');
|
||||
if (index != -1)
|
||||
tokens[5] = tokens[5].substring(0, index);
|
||||
osu.objects[i].endTime = Integer.parseInt(tokens[5]);
|
||||
/* 'addition' not implemented. */
|
||||
}
|
||||
|
||||
// set combo info
|
||||
// - new combo: get next combo index, reset combo number
|
||||
// - else: maintain combo index, increase combo number
|
||||
if ((osu.objects[i].type & OsuHitObject.TYPE_NEWCOMBO) > 0) {
|
||||
comboIndex = (comboIndex + 1) % osu.combo.length;
|
||||
comboNumber = 1;
|
||||
}
|
||||
osu.objects[i].comboIndex = comboIndex;
|
||||
osu.objects[i].comboNumber = comboNumber++;
|
||||
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.error(String.format("Failed to read file '%s'.", osu.getFile().getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false if the line is too short or commented.
|
||||
*/
|
||||
private static boolean isValidLine(String line) {
|
||||
return (line.length() > 1 && !line.startsWith("//"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits line into two strings: tag, value.
|
||||
*/
|
||||
private static String[] tokenize(String line) {
|
||||
String[] tokens = new String[2];
|
||||
try {
|
||||
int index = line.indexOf(':');
|
||||
tokens[0] = line.substring(0, index).trim();
|
||||
tokens[1] = line.substring(index + 1).trim();
|
||||
} catch (java.lang.StringIndexOutOfBoundsException e) {
|
||||
Log.error(String.format("Failed to tokenize line: '%s'.", line), e);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file extension of a file.
|
||||
*/
|
||||
public static String getExtension(String file) {
|
||||
int i = file.lastIndexOf('.');
|
||||
return (i > -1) ? file.substring(i + 1).toLowerCase() : "";
|
||||
}
|
||||
}
|
||||
39
src/itdelatrisu/opsu/OsuTimingPoint.java
Normal file
39
src/itdelatrisu/opsu/OsuTimingPoint.java
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu;
|
||||
|
||||
/**
|
||||
* Data type representing a timing point.
|
||||
*/
|
||||
public class OsuTimingPoint {
|
||||
public int time; // start time/offset (in ms)
|
||||
public float beatLength; // (non-inherited) ms per beat
|
||||
public int velocity = 0; // (inherited) slider multiplier = -100 / value
|
||||
public int meter; // beats per measure
|
||||
public byte sampleType; // sound samples (0:none, 1:normal, 2:soft)
|
||||
public byte sampleTypeCustom; // custom samples (0:default, 1:custom1, 2:custom2
|
||||
public int sampleVolume; // volume of samples (0~100)
|
||||
public boolean inherited; // is this timing point inherited?
|
||||
public boolean kiai; // is Kiai Mode active?
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public OsuTimingPoint() {}
|
||||
}
|
||||
214
src/itdelatrisu/opsu/objects/Circle.java
Normal file
214
src/itdelatrisu/opsu/objects/Circle.java
Normal file
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.objects;
|
||||
|
||||
import itdelatrisu.opsu.GameScore;
|
||||
import itdelatrisu.opsu.MusicController;
|
||||
import itdelatrisu.opsu.OsuHitObject;
|
||||
import itdelatrisu.opsu.states.Game;
|
||||
import itdelatrisu.opsu.states.Options;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.SlickException;
|
||||
|
||||
/**
|
||||
* Data type representing a circle object.
|
||||
*/
|
||||
public class Circle {
|
||||
/**
|
||||
* Images related to hit circles.
|
||||
*/
|
||||
private static Image
|
||||
hitCircle, // hit circle
|
||||
hitCircleOverlay, // hit circle overlay
|
||||
approachCircle; // approach circle
|
||||
|
||||
/**
|
||||
* The associated OsuHitObject.
|
||||
*/
|
||||
private OsuHitObject hitObject;
|
||||
|
||||
/**
|
||||
* The associated Game object.
|
||||
*/
|
||||
private Game game;
|
||||
|
||||
/**
|
||||
* The associated GameScore object.
|
||||
*/
|
||||
private GameScore score;
|
||||
|
||||
/**
|
||||
* The color of this circle.
|
||||
*/
|
||||
private Color color;
|
||||
|
||||
/**
|
||||
* Whether or not the circle result ends the combo streak.
|
||||
*/
|
||||
private boolean comboEnd;
|
||||
|
||||
/**
|
||||
* Initializes the Circle data type with map modifiers, images, and dimensions.
|
||||
* @param container the game container
|
||||
* @param circleSize the map's circleSize value
|
||||
* @throws SlickException
|
||||
*/
|
||||
public static void init(GameContainer container, byte circleSize) throws SlickException {
|
||||
int diameter = 96 - (circleSize * 8);
|
||||
diameter = diameter * container.getWidth() / 640; // convert from Osupixels (640x480)
|
||||
hitCircle = new Image("hitcircle.png").getScaledCopy(diameter, diameter);
|
||||
hitCircleOverlay = new Image("hitcircleoverlay.png").getScaledCopy(diameter, diameter);
|
||||
approachCircle = new Image("approachcircle.png").getScaledCopy(diameter, diameter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hit circle image.
|
||||
*/
|
||||
public static Image getHitCircle() { return hitCircle; }
|
||||
|
||||
/**
|
||||
* Returns the hit circle overlay image.
|
||||
*/
|
||||
public static Image getHitCircleOverlay() { return hitCircleOverlay; }
|
||||
|
||||
/**
|
||||
* Returns the approach circle image.
|
||||
*/
|
||||
public static Image getApproachCircle() { return approachCircle; }
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param hitObject the associated OsuHitObject
|
||||
* @param game the associated Game object
|
||||
* @param score the associated GameScore object
|
||||
* @param color the color of this circle
|
||||
* @param comboEnd true if this is the last hit object in the combo
|
||||
*/
|
||||
public Circle(OsuHitObject hitObject, Game game, GameScore score, Color color, boolean comboEnd) {
|
||||
this.hitObject = hitObject;
|
||||
this.game = game;
|
||||
this.score = score;
|
||||
this.color = color;
|
||||
this.comboEnd = comboEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the circle to the graphics context.
|
||||
* @param trackPosition the current track position
|
||||
*/
|
||||
public void draw(int trackPosition) {
|
||||
int timeDiff = hitObject.time - trackPosition;
|
||||
|
||||
if (timeDiff >= 0) {
|
||||
float approachScale = 1 + (timeDiff * 2f / game.getApproachTime());
|
||||
drawCentered(approachCircle.getScaledCopy(approachScale), hitObject.x, hitObject.y, color);
|
||||
drawCentered(hitCircleOverlay, hitObject.x, hitObject.y, Color.white);
|
||||
drawCentered(hitCircle, hitObject.x, hitObject.y, color);
|
||||
score.drawSymbolNumber(hitObject.comboNumber, hitObject.x, hitObject.y,
|
||||
hitCircle.getWidth() * 0.40f / score.getDefaultSymbolImage(0).getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an image based on its center with a color filter.
|
||||
*/
|
||||
private void drawCentered(Image img, float x, float y, Color color) {
|
||||
img.draw(x - (img.getWidth() / 2f), y - (img.getHeight() / 2f), color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the circle hit result.
|
||||
* @param time the hit object time (difference between track time)
|
||||
* @return the hit result (GameScore.HIT_* constants)
|
||||
*/
|
||||
public int hitResult(int time) {
|
||||
int trackPosition = MusicController.getPosition();
|
||||
int timeDiff = Math.abs(trackPosition - time);
|
||||
|
||||
int[] hitResultOffset = game.getHitResultOffsets();
|
||||
int result = -1;
|
||||
if (timeDiff < hitResultOffset[GameScore.HIT_300])
|
||||
result = GameScore.HIT_300;
|
||||
else if (timeDiff < hitResultOffset[GameScore.HIT_100])
|
||||
result = GameScore.HIT_100;
|
||||
else if (timeDiff < hitResultOffset[GameScore.HIT_50])
|
||||
result = GameScore.HIT_50;
|
||||
else if (timeDiff < hitResultOffset[GameScore.HIT_MISS])
|
||||
result = GameScore.HIT_MISS;
|
||||
//else not a hit
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a mouse click.
|
||||
* @param x the x coordinate of the mouse
|
||||
* @param y the y coordinate of the mouse
|
||||
* @param comboEnd if this is the last object in the combo
|
||||
* @return true if a hit result was processed
|
||||
*/
|
||||
public boolean mousePressed(int x, int y) {
|
||||
double distance = Math.hypot(hitObject.x - x, hitObject.y - y);
|
||||
int circleRadius = hitCircle.getWidth() / 2;
|
||||
if (distance < circleRadius) {
|
||||
int result = hitResult(hitObject.time);
|
||||
if (result > -1) {
|
||||
score.hitResult(hitObject.time, result, hitObject.x, hitObject.y, color, comboEnd);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the circle object.
|
||||
* @param overlap true if the next object's start time has already passed
|
||||
* @return true if a hit result (miss) was processed
|
||||
*/
|
||||
public boolean update(boolean overlap) {
|
||||
int trackPosition = MusicController.getPosition();
|
||||
int[] hitResultOffset = game.getHitResultOffsets();
|
||||
boolean isAutoMod = Options.isModActive(Options.MOD_AUTO);
|
||||
|
||||
if (overlap || trackPosition > hitObject.time + hitResultOffset[GameScore.HIT_50]) {
|
||||
if (isAutoMod) // "auto" mod: catch any missed notes due to lag
|
||||
score.hitResult(hitObject.time, GameScore.HIT_300,
|
||||
hitObject.x, hitObject.y, color, comboEnd);
|
||||
|
||||
else // no more points can be scored, so send a miss
|
||||
score.hitResult(hitObject.time, GameScore.HIT_MISS,
|
||||
hitObject.x, hitObject.y, null, comboEnd);
|
||||
return true;
|
||||
}
|
||||
|
||||
// "auto" mod: send a perfect hit result
|
||||
else if (isAutoMod) {
|
||||
if (Math.abs(trackPosition - hitObject.time) < hitResultOffset[GameScore.HIT_300]) {
|
||||
score.hitResult(hitObject.time, GameScore.HIT_300,
|
||||
hitObject.x, hitObject.y, color, comboEnd);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
581
src/itdelatrisu/opsu/objects/Slider.java
Normal file
581
src/itdelatrisu/opsu/objects/Slider.java
Normal file
@@ -0,0 +1,581 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.objects;
|
||||
|
||||
import itdelatrisu.opsu.GameScore;
|
||||
import itdelatrisu.opsu.MusicController;
|
||||
import itdelatrisu.opsu.OsuFile;
|
||||
import itdelatrisu.opsu.OsuHitObject;
|
||||
import itdelatrisu.opsu.states.Game;
|
||||
import itdelatrisu.opsu.states.Options;
|
||||
|
||||
import org.newdawn.slick.Animation;
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.SlickException;
|
||||
|
||||
/**
|
||||
* Data type representing a slider object.
|
||||
*/
|
||||
public class Slider {
|
||||
/**
|
||||
* Images related to sliders.
|
||||
*/
|
||||
private static Image
|
||||
sliderFollowCircle, // slider follow circle
|
||||
reverseArrow, // reverse arrow (for repeats)
|
||||
sliderTick; // slider tick
|
||||
|
||||
/**
|
||||
* Slider ball animation.
|
||||
*/
|
||||
private static Animation sliderBall;
|
||||
|
||||
/**
|
||||
* Slider movement speed multiplier.
|
||||
*/
|
||||
private static float sliderMultiplier = 1.0f;
|
||||
|
||||
/**
|
||||
* Rate at which slider ticks are placed.
|
||||
*/
|
||||
private static float sliderTickRate = 1.0f;
|
||||
|
||||
/**
|
||||
* The associated OsuHitObject.
|
||||
*/
|
||||
private OsuHitObject hitObject;
|
||||
|
||||
/**
|
||||
* The associated Game object.
|
||||
*/
|
||||
private Game game;
|
||||
|
||||
/**
|
||||
* The associated GameScore object.
|
||||
*/
|
||||
private GameScore score;
|
||||
|
||||
/**
|
||||
* The color of this slider.
|
||||
*/
|
||||
private Color color;
|
||||
|
||||
/**
|
||||
* The underlying Bezier object.
|
||||
*/
|
||||
private Bezier bezier;
|
||||
|
||||
/**
|
||||
* The time duration of the slider, in milliseconds.
|
||||
*/
|
||||
private float sliderTime = 0f;
|
||||
|
||||
/**
|
||||
* The time duration of the slider including repeats, in milliseconds.
|
||||
*/
|
||||
private float sliderTimeTotal = 0f;
|
||||
|
||||
/**
|
||||
* Whether or not the result of the initial hit circle has been processed.
|
||||
*/
|
||||
private boolean sliderClicked = false;
|
||||
|
||||
/**
|
||||
* Whether or not to show the follow circle.
|
||||
*/
|
||||
private boolean followCircleActive = false;
|
||||
|
||||
/**
|
||||
* Whether or not the slider result ends the combo streak.
|
||||
*/
|
||||
private boolean comboEnd;
|
||||
|
||||
/**
|
||||
* The number of repeats that have passed so far.
|
||||
*/
|
||||
private int currentRepeats = 0;
|
||||
|
||||
/**
|
||||
* The t values of the slider ticks.
|
||||
*/
|
||||
private float[] ticksT;
|
||||
|
||||
/**
|
||||
* The tick index in the ticksT[] array.
|
||||
*/
|
||||
private int tickIndex = 0;
|
||||
|
||||
/**
|
||||
* Number of ticks hit and tick intervals so far.
|
||||
*/
|
||||
private int ticksHit = 0, tickIntervals = 1;
|
||||
|
||||
/**
|
||||
* Representation of a Bezier curve, the main component of a slider.
|
||||
*
|
||||
* @author Alex Gheorghiu (http://html5tutorial.com/how-to-draw-n-grade-bezier-curve-with-canvas-api/)
|
||||
* @author pictuga (https://github.com/pictuga/osu-web)
|
||||
*/
|
||||
private class Bezier {
|
||||
/**
|
||||
* The order of the Bezier curve.
|
||||
*/
|
||||
private int order;
|
||||
|
||||
/**
|
||||
* The step size (used for drawing),
|
||||
*/
|
||||
private float step;
|
||||
|
||||
/**
|
||||
* The curve points for drawing with step size given by 'step'.
|
||||
*/
|
||||
private float[] curveX, curveY;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public Bezier(float length) {
|
||||
this.order = hitObject.sliderX.length + 1;
|
||||
this.step = 5 / length;
|
||||
|
||||
// calculate curve points for drawing
|
||||
int N = (int) (1 / step);
|
||||
this.curveX = new float[N];
|
||||
this.curveY = new float[N];
|
||||
float t = 0f;
|
||||
for (int i = 0; i < N; i++, t += step) {
|
||||
float[] c = pointAt(t);
|
||||
curveX[i] = c[0];
|
||||
curveY[i] = c[1];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the x coordinate of the control point at index i.
|
||||
*/
|
||||
private float getX(int i) {
|
||||
return (i == 0) ? hitObject.x : hitObject.sliderX[i - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the y coordinate of the control point at index i.
|
||||
*/
|
||||
private float getY(int i) {
|
||||
return (i == 0) ? hitObject.y : hitObject.sliderY[i - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the factorial of a number.
|
||||
*/
|
||||
private long factorial(int n) {
|
||||
return (n <= 1 || n > 20) ? 1 : n * factorial(n - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the Bernstein polynomial.
|
||||
* @param i the index
|
||||
* @param n the degree of the polynomial (i.e. number of points)
|
||||
* @param t the t value [0, 1]
|
||||
*/
|
||||
private double bernstein(int i, int n, float t) {
|
||||
return factorial(n) / (factorial(i) * factorial(n-i)) *
|
||||
Math.pow(t, i) * Math.pow(1-t, n-i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the point on the Bezier curve at a value t.
|
||||
* For curves of order greater than 4, points will be generated along
|
||||
* a path of overlapping cubic (at most) Beziers.
|
||||
* @param t the t value [0, 1]
|
||||
* @return the point [x, y]
|
||||
*/
|
||||
public float[] pointAt(float t) {
|
||||
float[] c = { 0f, 0f };
|
||||
int n = order - 1;
|
||||
if (n < 4) { // normal curve
|
||||
for (int i = 0; i <= n; i++) {
|
||||
c[0] += getX(i) * bernstein(i, n, t);
|
||||
c[1] += getY(i) * bernstein(i, n, t);
|
||||
}
|
||||
} else { // split curve into path
|
||||
// TODO: this is probably wrong...
|
||||
int segmentCount = (n / 3) + 1;
|
||||
int segment = (int) Math.floor(t * segmentCount);
|
||||
int startIndex = 3 * segment;
|
||||
int segmentOrder = Math.min(startIndex + 3, n) - startIndex;
|
||||
float segmentT = (t * segmentCount) - segment;
|
||||
for (int i = 0; i <= segmentOrder; i++) {
|
||||
c[0] += getX(i + startIndex) * bernstein(i, segmentOrder, segmentT);
|
||||
c[1] += getY(i + startIndex) * bernstein(i, segmentOrder, segmentT);
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the full Bezier curve to the graphics context.
|
||||
*/
|
||||
public void draw() {
|
||||
Image hitCircle = Circle.getHitCircle();
|
||||
Image hitCircleOverlay = Circle.getHitCircleOverlay();
|
||||
|
||||
// draw overlay and hit circle
|
||||
for (int i = curveX.length - 1; i >= 0; i--)
|
||||
drawCentered(hitCircleOverlay, curveX[i], curveY[i], Color.white);
|
||||
for (int i = curveX.length - 1; i >= 0; i--)
|
||||
drawCentered(hitCircle, curveX[i], curveY[i], color);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Slider data type with images and dimensions.
|
||||
* @param container the game container
|
||||
* @param circleSize the map's circleSize value
|
||||
* @param osu the associated OsuFile object
|
||||
* @throws SlickException
|
||||
*/
|
||||
public static void init(GameContainer container, byte circleSize, OsuFile osu) throws SlickException {
|
||||
int diameter = 96 - (circleSize * 8);
|
||||
diameter = diameter * container.getWidth() / 640; // convert from Osupixels (640x480)
|
||||
|
||||
sliderBall = new Animation();
|
||||
for (int i = 0; i <= 9; i++)
|
||||
sliderBall.addFrame(new Image(String.format("sliderb%d.png", i)).getScaledCopy(diameter * 118 / 128, diameter * 118 / 128), 60);
|
||||
sliderFollowCircle = new Image("sliderfollowcircle.png").getScaledCopy(diameter * 259 / 128, diameter * 259 / 128);
|
||||
reverseArrow = new Image("reversearrow.png").getScaledCopy(diameter, diameter);
|
||||
sliderTick = new Image("sliderscorepoint.png").getScaledCopy(diameter / 4, diameter / 4);
|
||||
|
||||
sliderMultiplier = osu.sliderMultiplier;
|
||||
sliderTickRate = osu.sliderTickRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param hitObject the associated OsuHitObject
|
||||
* @param game the associated Game object
|
||||
* @param score the associated GameScore object
|
||||
* @param color the color of this circle
|
||||
* @param comboEnd true if this is the last hit object in the combo
|
||||
*/
|
||||
public Slider(OsuHitObject hitObject, Game game, GameScore score, Color color, boolean comboEnd) {
|
||||
this.hitObject = hitObject;
|
||||
this.game = game;
|
||||
this.score = score;
|
||||
this.color = color;
|
||||
this.comboEnd = comboEnd;
|
||||
|
||||
this.bezier = new Bezier(hitObject.pixelLength);
|
||||
|
||||
// calculate slider time and ticks upon first update call
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the slider to the graphics context.
|
||||
* @param trackPosition the current track position
|
||||
* @param currentObject true if this is the current hit object
|
||||
*/
|
||||
public void draw(int trackPosition, boolean currentObject) {
|
||||
int timeDiff = hitObject.time - trackPosition;
|
||||
|
||||
Image hitCircleOverlay = Circle.getHitCircleOverlay();
|
||||
Image hitCircle = Circle.getHitCircle();
|
||||
|
||||
// bezier
|
||||
bezier.draw();
|
||||
|
||||
// ticks
|
||||
if (currentObject && ticksT != null) {
|
||||
for (int i = 0; i < ticksT.length; i++) {
|
||||
float[] c = bezier.pointAt(ticksT[i]);
|
||||
sliderTick.drawCentered(c[0], c[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// end circle
|
||||
int lastIndex = hitObject.sliderX.length - 1;
|
||||
drawCentered(hitCircleOverlay, hitObject.sliderX[lastIndex], hitObject.sliderY[lastIndex], Color.white);
|
||||
drawCentered(hitCircle, hitObject.sliderX[lastIndex], hitObject.sliderY[lastIndex], color);
|
||||
|
||||
// start circle
|
||||
drawCentered(hitCircleOverlay, hitObject.x, hitObject.y, Color.white);
|
||||
drawCentered(hitCircle, hitObject.x, hitObject.y, color);
|
||||
if (sliderClicked)
|
||||
; // don't draw current combo number if already clicked
|
||||
else
|
||||
score.drawSymbolNumber(hitObject.comboNumber, hitObject.x, hitObject.y,
|
||||
hitCircle.getWidth() * 0.40f / score.getDefaultSymbolImage(0).getHeight());
|
||||
|
||||
// repeats
|
||||
if (hitObject.repeat - 1 > currentRepeats) {
|
||||
if (currentRepeats % 2 == 0) // last circle
|
||||
reverseArrow.drawCentered(hitObject.sliderX[lastIndex], hitObject.sliderY[lastIndex]);
|
||||
else // first circle
|
||||
reverseArrow.drawCentered(hitObject.x, hitObject.y);
|
||||
}
|
||||
|
||||
if (timeDiff >= 0) {
|
||||
// approach circle
|
||||
float approachScale = 1 + (timeDiff * 2f / game.getApproachTime());
|
||||
drawCentered(Circle.getApproachCircle().getScaledCopy(approachScale), hitObject.x, hitObject.y, color);
|
||||
} else {
|
||||
float[] c = bezier.pointAt(getT(trackPosition, false));
|
||||
|
||||
// slider ball
|
||||
drawCentered(sliderBall, c[0], c[1]);
|
||||
|
||||
// follow circle
|
||||
if (followCircleActive)
|
||||
sliderFollowCircle.drawCentered(c[0], c[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an image based on its center with a color filter.
|
||||
*/
|
||||
private void drawCentered(Image img, float x, float y, Color color) {
|
||||
img.draw(x - (img.getWidth() / 2f), y - (img.getHeight() / 2f), color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an animation based on its center with a color filter.
|
||||
*/
|
||||
private static void drawCentered(Animation anim, float x, float y) {
|
||||
anim.draw(x - (anim.getWidth() / 2f), y - (anim.getHeight() / 2f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the slider hit result.
|
||||
* @param time the hit object time (difference between track time)
|
||||
* @param lastCircleHit true if the cursor was held within the last circle
|
||||
* @return the hit result (GameScore.HIT_* constants)
|
||||
*/
|
||||
public int hitResult() {
|
||||
int lastIndex = hitObject.sliderX.length - 1;
|
||||
float tickRatio = (float) ticksHit / tickIntervals;
|
||||
|
||||
int result;
|
||||
if (tickRatio >= 1.0f)
|
||||
result = GameScore.HIT_300;
|
||||
else if (tickRatio >= 0.5f)
|
||||
result = GameScore.HIT_100;
|
||||
else if (tickRatio > 0f)
|
||||
result = GameScore.HIT_50;
|
||||
else
|
||||
result = GameScore.HIT_MISS;
|
||||
|
||||
if (currentRepeats % 2 == 0) // last circle
|
||||
score.hitResult(hitObject.time + (int) sliderTimeTotal, result,
|
||||
hitObject.sliderX[lastIndex], hitObject.sliderY[lastIndex], color, comboEnd);
|
||||
else // first circle
|
||||
score.hitResult(hitObject.time + (int) sliderTimeTotal, result,
|
||||
hitObject.x, hitObject.y, color, comboEnd);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a mouse click.
|
||||
* @param x the x coordinate of the mouse
|
||||
* @param y the y coordinate of the mouse
|
||||
* @param comboEnd if this is the last object in the combo
|
||||
* @return true if a hit result was processed
|
||||
*/
|
||||
public boolean mousePressed(int x, int y) {
|
||||
if (sliderClicked) // first circle already processed
|
||||
return false;
|
||||
|
||||
double distance = Math.hypot(hitObject.x - x, hitObject.y - y);
|
||||
int circleRadius = Circle.getHitCircle().getWidth() / 2;
|
||||
if (distance < circleRadius) {
|
||||
int trackPosition = MusicController.getPosition();
|
||||
int timeDiff = Math.abs(trackPosition - hitObject.time);
|
||||
int[] hitResultOffset = game.getHitResultOffsets();
|
||||
|
||||
int result = -1;
|
||||
if (timeDiff < hitResultOffset[GameScore.HIT_50]) {
|
||||
result = GameScore.HIT_SLIDER30;
|
||||
ticksHit++;
|
||||
} else if (timeDiff < hitResultOffset[GameScore.HIT_MISS])
|
||||
result = GameScore.HIT_MISS;
|
||||
//else not a hit
|
||||
|
||||
if (result > -1) {
|
||||
sliderClicked = true;
|
||||
score.sliderTickResult(hitObject.time, result, hitObject.x, hitObject.y);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the slider object.
|
||||
* @param overlap true if the next object's start time has already passed
|
||||
* @param delta the delta interval since the last call
|
||||
* @param mouseX the x coordinate of the mouse
|
||||
* @param mouseY the y coordinate of the mouse
|
||||
* @return true if slider ended
|
||||
*/
|
||||
public boolean update(boolean overlap, int delta, int mouseX, int mouseY) {
|
||||
// slider time and tick calculations
|
||||
if (sliderTimeTotal == 0f) {
|
||||
// slider time
|
||||
this.sliderTime = game.getBeatLength() * (hitObject.pixelLength / sliderMultiplier) / 100f;
|
||||
this.sliderTimeTotal = sliderTime * hitObject.repeat;
|
||||
|
||||
// ticks
|
||||
float tickLengthDiv = 100f * sliderMultiplier / sliderTickRate / game.getTimingPointMultiplier();
|
||||
int tickCount = (int) Math.ceil(hitObject.pixelLength / tickLengthDiv) - 1;
|
||||
if (tickCount > 0) {
|
||||
this.ticksT = new float[tickCount];
|
||||
float tickTOffset = 1f / (tickCount + 1);
|
||||
float t = tickTOffset;
|
||||
for (int i = 0; i < tickCount; i++, t += tickTOffset)
|
||||
ticksT[i] = t;
|
||||
}
|
||||
}
|
||||
|
||||
int trackPosition = MusicController.getPosition();
|
||||
int[] hitResultOffset = game.getHitResultOffsets();
|
||||
int lastIndex = hitObject.sliderX.length - 1;
|
||||
boolean isAutoMod = Options.isModActive(Options.MOD_AUTO);
|
||||
|
||||
if (!sliderClicked) {
|
||||
// start circle time passed
|
||||
if (trackPosition > hitObject.time + hitResultOffset[GameScore.HIT_50]) {
|
||||
sliderClicked = true;
|
||||
if (isAutoMod) { // "auto" mod: catch any missed notes due to lag
|
||||
ticksHit++;
|
||||
score.sliderTickResult(hitObject.time, GameScore.HIT_SLIDER30, hitObject.x, hitObject.y);
|
||||
} else
|
||||
score.sliderTickResult(hitObject.time, GameScore.HIT_MISS, hitObject.x, hitObject.y);
|
||||
}
|
||||
|
||||
// "auto" mod: send a perfect hit result
|
||||
else if (isAutoMod) {
|
||||
if (Math.abs(trackPosition - hitObject.time) < hitResultOffset[GameScore.HIT_300]) {
|
||||
ticksHit++;
|
||||
sliderClicked = true;
|
||||
score.sliderTickResult(hitObject.time, GameScore.HIT_SLIDER30, hitObject.x, hitObject.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// end of slider
|
||||
if (overlap || trackPosition > hitObject.time + sliderTimeTotal) {
|
||||
tickIntervals++;
|
||||
|
||||
// "auto" mod: send a perfect hit result
|
||||
if (isAutoMod)
|
||||
ticksHit++;
|
||||
|
||||
// check if cursor pressed and within end circle
|
||||
else if (game.isInputKeyPressed()) {
|
||||
double distance = Math.hypot(hitObject.sliderX[lastIndex] - mouseX, hitObject.sliderY[lastIndex] - mouseY);
|
||||
int followCircleRadius = sliderFollowCircle.getWidth() / 2;
|
||||
if (distance < followCircleRadius)
|
||||
ticksHit++;
|
||||
}
|
||||
|
||||
// calculate and send slider result
|
||||
hitResult();
|
||||
return true;
|
||||
}
|
||||
|
||||
// repeats
|
||||
boolean isNewRepeat = false;
|
||||
if (hitObject.repeat - 1 > currentRepeats) {
|
||||
float t = getT(trackPosition, true);
|
||||
if (Math.floor(t) > currentRepeats) {
|
||||
currentRepeats++;
|
||||
tickIntervals++;
|
||||
isNewRepeat = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ticks
|
||||
boolean isNewTick = false;
|
||||
if (ticksT != null &&
|
||||
tickIntervals < (ticksT.length * (currentRepeats + 1)) + hitObject.repeat &&
|
||||
tickIntervals < (ticksT.length * hitObject.repeat) + hitObject.repeat) {
|
||||
float t = getT(trackPosition, true);
|
||||
if (t - Math.floor(t) >= ticksT[tickIndex]) {
|
||||
tickIntervals++;
|
||||
tickIndex = (tickIndex + 1) % ticksT.length;
|
||||
isNewTick = true;
|
||||
}
|
||||
}
|
||||
|
||||
// holding slider...
|
||||
float[] c = bezier.pointAt(getT(trackPosition, false));
|
||||
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
|
||||
int followCircleRadius = sliderFollowCircle.getWidth() / 2;
|
||||
if ((game.isInputKeyPressed() && distance < followCircleRadius) || isAutoMod) {
|
||||
// mouse pressed and within follow circle
|
||||
followCircleActive = true;
|
||||
score.changeHealth(delta / 200f);
|
||||
|
||||
// held during new repeat
|
||||
if (isNewRepeat) {
|
||||
ticksHit++;
|
||||
if (currentRepeats % 2 > 0) // last circle
|
||||
score.sliderTickResult(trackPosition, GameScore.HIT_SLIDER30,
|
||||
hitObject.sliderX[lastIndex], hitObject.sliderY[lastIndex]);
|
||||
else // first circle
|
||||
score.sliderTickResult(trackPosition, GameScore.HIT_SLIDER30,
|
||||
c[0], c[1]);
|
||||
}
|
||||
|
||||
// held during new tick
|
||||
if (isNewTick) {
|
||||
ticksHit++;
|
||||
score.sliderTickResult(trackPosition, GameScore.HIT_SLIDER10, c[0], c[1]);
|
||||
}
|
||||
} else {
|
||||
followCircleActive = false;
|
||||
|
||||
if (isNewRepeat)
|
||||
score.sliderTickResult(trackPosition, GameScore.HIT_MISS, 0, 0);
|
||||
if (isNewTick)
|
||||
score.sliderTickResult(trackPosition, GameScore.HIT_MISS, 0, 0);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the t value based on the given track position.
|
||||
* @param trackPosition the current track position
|
||||
* @param raw if false, ensures that the value lies within [0, 1] by looping repeats
|
||||
* @return the t value: raw [0, repeats] or looped [0, 1]
|
||||
*/
|
||||
public float getT(int trackPosition, boolean raw) {
|
||||
float t = (trackPosition - hitObject.time) / sliderTime;
|
||||
if (raw)
|
||||
return t;
|
||||
else {
|
||||
float floor = (float) Math.floor(t);
|
||||
return (floor % 2 == 0) ? t - floor : floor + 1 - t;
|
||||
}
|
||||
}
|
||||
}
|
||||
251
src/itdelatrisu/opsu/objects/Spinner.java
Normal file
251
src/itdelatrisu/opsu/objects/Spinner.java
Normal file
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.objects;
|
||||
|
||||
import itdelatrisu.opsu.GameScore;
|
||||
import itdelatrisu.opsu.MusicController;
|
||||
import itdelatrisu.opsu.OsuHitObject;
|
||||
import itdelatrisu.opsu.states.Game;
|
||||
import itdelatrisu.opsu.states.Options;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
import org.newdawn.slick.Graphics;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.SlickException;
|
||||
|
||||
/**
|
||||
* Data type representing a spinner object.
|
||||
*/
|
||||
public class Spinner {
|
||||
/**
|
||||
* Images related to spinners.
|
||||
*/
|
||||
private static Image
|
||||
spinnerCircle, // spinner
|
||||
spinnerApproachCircle, // spinner approach circle (for end time)
|
||||
spinnerMetre, // spinner meter (subimage based on completion ratio)
|
||||
// spinnerOsuImage, // spinner "OSU!" text (complete)
|
||||
spinnerSpinImage, // spinner "SPIN!" text (start)
|
||||
spinnerClearImage; // spinner "CLEAR" text (passed)
|
||||
|
||||
/**
|
||||
* Container dimensions.
|
||||
*/
|
||||
private static int width, height;
|
||||
|
||||
/**
|
||||
* The associated OsuHitObject.
|
||||
*/
|
||||
private OsuHitObject hitObject;
|
||||
|
||||
/**
|
||||
* The associated Game object.
|
||||
*/
|
||||
private Game game;
|
||||
|
||||
/**
|
||||
* The associated GameScore object.
|
||||
*/
|
||||
private GameScore score;
|
||||
|
||||
/**
|
||||
* The last rotation angle.
|
||||
*/
|
||||
private float lastAngle = -1f;
|
||||
|
||||
/**
|
||||
* The current number of rotations.
|
||||
*/
|
||||
private float rotations = 0f;
|
||||
|
||||
/**
|
||||
* The total number of rotations needed to clear the spinner.
|
||||
*/
|
||||
private float rotationsNeeded;
|
||||
|
||||
/**
|
||||
* Initializes the Spinner data type with images and dimensions.
|
||||
* @param container the game container
|
||||
* @throws SlickException
|
||||
*/
|
||||
public static void init(GameContainer container) throws SlickException {
|
||||
width = container.getWidth();
|
||||
height = container.getHeight();
|
||||
|
||||
spinnerCircle = new Image("spinner-circle.png").getScaledCopy(height * 9 / 10, height * 9 / 10);
|
||||
spinnerApproachCircle = new Image("spinner-approachcircle.png").getScaledCopy(spinnerCircle.getWidth(), spinnerCircle.getHeight());
|
||||
spinnerMetre = new Image("spinner-metre.png").getScaledCopy(width, height);
|
||||
spinnerSpinImage = new Image("spinner-spin.png");
|
||||
spinnerClearImage = new Image("spinner-clear.png");
|
||||
// spinnerOsuImage = new Image("spinner-osu.png");
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param hitObject the associated OsuHitObject
|
||||
* @param game the associated Game object
|
||||
* @param score the associated GameScore object
|
||||
*/
|
||||
public Spinner(OsuHitObject hitObject, Game game, GameScore score) {
|
||||
this.hitObject = hitObject;
|
||||
this.game = game;
|
||||
this.score = score;
|
||||
|
||||
// calculate rotations needed
|
||||
int spinsPerMinute = 100 + (score.getDifficulty() * 15);
|
||||
rotationsNeeded = (float) spinsPerMinute * (hitObject.endTime - hitObject.time) / 60000f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the spinner to the graphics context.
|
||||
* @param trackPosition the current track position
|
||||
* @param g the graphics context
|
||||
*/
|
||||
public void draw(int trackPosition, Graphics g) {
|
||||
int timeDiff = hitObject.time - trackPosition;
|
||||
boolean spinnerComplete = (rotations >= rotationsNeeded);
|
||||
|
||||
// TODO: draw "OSU!" image after spinner ends
|
||||
//spinnerOsuImage.drawCentered(width / 2, height / 4);
|
||||
|
||||
// darken screen
|
||||
g.setColor(Options.COLOR_BLACK_ALPHA);
|
||||
g.fillRect(0, 0, width, height);
|
||||
|
||||
if (timeDiff > 0)
|
||||
return;
|
||||
|
||||
// spinner meter (subimage)
|
||||
int spinnerMetreY = (spinnerComplete) ? 0 : (int) (spinnerMetre.getHeight() * (1 - (rotations / rotationsNeeded)));
|
||||
Image spinnerMetreSub = spinnerMetre.getSubImage(
|
||||
0, spinnerMetreY,
|
||||
spinnerMetre.getWidth(), spinnerMetre.getHeight() - spinnerMetreY
|
||||
);
|
||||
spinnerMetreSub.draw(0, height - spinnerMetreSub.getHeight());
|
||||
|
||||
// main spinner elements
|
||||
spinnerCircle.drawCentered(width / 2, height / 2);
|
||||
spinnerApproachCircle.getScaledCopy(1 - ((float) timeDiff / (hitObject.time - hitObject.endTime))).drawCentered(width / 2, height / 2);
|
||||
spinnerSpinImage.drawCentered(width / 2, height * 3 / 4);
|
||||
|
||||
if (spinnerComplete) {
|
||||
spinnerClearImage.drawCentered(width / 2, height / 4);
|
||||
int extraRotations = (int) (rotations - rotationsNeeded);
|
||||
if (extraRotations > 0)
|
||||
score.drawSymbolNumber(extraRotations * 1000, width / 2, height * 2 / 3, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates and sends the spinner hit result.
|
||||
* @return the hit result (GameScore.HIT_* constants)
|
||||
*/
|
||||
public int hitResult() {
|
||||
// TODO: verify ratios
|
||||
|
||||
int result;
|
||||
float ratio = rotations / rotationsNeeded;
|
||||
if (ratio >= 1.0f ||
|
||||
Options.isModActive(Options.MOD_AUTO) || Options.isModActive(Options.MOD_SPUN_OUT))
|
||||
result = GameScore.HIT_300;
|
||||
else if (ratio >= 0.8f)
|
||||
result = GameScore.HIT_100;
|
||||
else if (ratio >= 0.5f)
|
||||
result = GameScore.HIT_50;
|
||||
else
|
||||
result = GameScore.HIT_MISS;
|
||||
|
||||
score.hitResult(hitObject.endTime, result, width / 2, height / 2, Color.transparent, true);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the spinner by a delta interval.
|
||||
* @param overlap true if the next object's start time has already passed
|
||||
* @param delta the delta interval since the last call
|
||||
* @param mouseX the x coordinate of the mouse
|
||||
* @param mouseY the y coordinate of the mouse
|
||||
* @return true if spinner ended
|
||||
*/
|
||||
public boolean update(boolean overlap, int delta, int mouseX, int mouseY) {
|
||||
int trackPosition = MusicController.getPosition();
|
||||
if (overlap)
|
||||
return true;
|
||||
|
||||
// end of spinner
|
||||
if (trackPosition > hitObject.endTime) {
|
||||
hitResult();
|
||||
return true;
|
||||
}
|
||||
|
||||
// spin automatically (TODO: correct rotation angles)
|
||||
if (Options.isModActive(Options.MOD_AUTO)) {
|
||||
// "auto" mod (fast)
|
||||
score.changeHealth(delta / 200f); // maintain health (TODO)
|
||||
rotate(delta / 20f);
|
||||
return false;
|
||||
} else if (Options.isModActive(Options.MOD_SPUN_OUT)) {
|
||||
// "spun out" mod (slow)
|
||||
score.changeHealth(delta / 200f); // maintain health (TODO)
|
||||
rotate(delta / 32f);
|
||||
return false;
|
||||
}
|
||||
|
||||
// not spinning: nothing to do
|
||||
if (!game.isInputKeyPressed()) {
|
||||
lastAngle = -1f;
|
||||
return false;
|
||||
}
|
||||
|
||||
// scale angle from [-pi, +pi] to [0, +pi]
|
||||
float angle = (float) Math.atan2(mouseY - (height / 2), mouseX - (width / 2));
|
||||
if (angle < 0f)
|
||||
angle += Math.PI;
|
||||
|
||||
if (lastAngle >= 0f) { // skip initial clicks
|
||||
float angleDiff = Math.abs(lastAngle - angle);
|
||||
if (angleDiff < Math.PI / 2) { // skip huge angle changes...
|
||||
score.changeHealth(delta / 200f); // maintain health (TODO)
|
||||
rotate(angleDiff);
|
||||
}
|
||||
}
|
||||
|
||||
lastAngle = angle;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates the spinner by a number of degrees.
|
||||
* @param degrees the angle to rotate (in radians)
|
||||
*/
|
||||
private void rotate(float degrees) {
|
||||
float newRotations = rotations + (degrees / (float) (2 * Math.PI));
|
||||
|
||||
// added one whole rotation...
|
||||
if (Math.floor(newRotations) > rotations) {
|
||||
if (newRotations > rotationsNeeded) // extra rotations
|
||||
score.changeScore(1000);
|
||||
else
|
||||
score.changeScore(100);
|
||||
}
|
||||
|
||||
rotations = newRotations;
|
||||
}
|
||||
}
|
||||
774
src/itdelatrisu/opsu/states/Game.java
Normal file
774
src/itdelatrisu/opsu/states/Game.java
Normal file
@@ -0,0 +1,774 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.states;
|
||||
|
||||
import itdelatrisu.opsu.GUIMenuButton;
|
||||
import itdelatrisu.opsu.GameScore;
|
||||
import itdelatrisu.opsu.MusicController;
|
||||
import itdelatrisu.opsu.Opsu;
|
||||
import itdelatrisu.opsu.OsuFile;
|
||||
import itdelatrisu.opsu.OsuHitObject;
|
||||
import itdelatrisu.opsu.OsuTimingPoint;
|
||||
import itdelatrisu.opsu.objects.Circle;
|
||||
import itdelatrisu.opsu.objects.Slider;
|
||||
import itdelatrisu.opsu.objects.Spinner;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Stack;
|
||||
|
||||
import org.lwjgl.input.Keyboard;
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
import org.newdawn.slick.Graphics;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.Input;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.state.BasicGameState;
|
||||
import org.newdawn.slick.state.StateBasedGame;
|
||||
import org.newdawn.slick.state.transition.EmptyTransition;
|
||||
import org.newdawn.slick.state.transition.FadeInTransition;
|
||||
import org.newdawn.slick.state.transition.FadeOutTransition;
|
||||
import org.newdawn.slick.util.Log;
|
||||
|
||||
/**
|
||||
* "Game" state.
|
||||
*/
|
||||
public class Game extends BasicGameState {
|
||||
/**
|
||||
* Game restart states.
|
||||
*/
|
||||
public static final byte
|
||||
RESTART_FALSE = 0,
|
||||
RESTART_NEW = 1, // first time loading song
|
||||
RESTART_MANUAL = 2, // retry
|
||||
RESTART_LOSE = 3; // health is zero: no-continue/force restart
|
||||
|
||||
/**
|
||||
* Current restart state.
|
||||
*/
|
||||
private static byte restart;
|
||||
|
||||
/**
|
||||
* The associated OsuFile object.
|
||||
*/
|
||||
private static OsuFile osu;
|
||||
|
||||
/**
|
||||
* The associated GameScore object (holds all score data).
|
||||
*/
|
||||
private static GameScore score;
|
||||
|
||||
/**
|
||||
* Current hit object index in OsuHitObject[] array.
|
||||
*/
|
||||
private int objectIndex = 0;
|
||||
|
||||
/**
|
||||
* This map's hit circles objects, keyed by objectIndex.
|
||||
*/
|
||||
private HashMap<Integer, Circle> circles;
|
||||
|
||||
/**
|
||||
* This map's slider objects, keyed by objectIndex.
|
||||
*/
|
||||
private HashMap<Integer, Slider> sliders;
|
||||
|
||||
/**
|
||||
* This map's spinner objects, keyed by objectIndex.
|
||||
*/
|
||||
private HashMap<Integer, Spinner> spinners;
|
||||
|
||||
/**
|
||||
* Delay time, in milliseconds, before song starts.
|
||||
*/
|
||||
private static int leadInTime;
|
||||
|
||||
/**
|
||||
* Hit object approach time, in milliseconds.
|
||||
*/
|
||||
private int approachTime;
|
||||
|
||||
/**
|
||||
* Time offsets for obtaining each hit result (indexed by HIT_* constants).
|
||||
*/
|
||||
private int[] hitResultOffset;
|
||||
|
||||
/**
|
||||
* Time, in milliseconds, between the first and last hit object.
|
||||
*/
|
||||
private int mapLength;
|
||||
|
||||
/**
|
||||
* Current break index in breaks ArrayList.
|
||||
*/
|
||||
private int breakIndex;
|
||||
|
||||
/**
|
||||
* Warning arrows, pointing right and left.
|
||||
*/
|
||||
private Image warningArrowR, warningArrowL;
|
||||
|
||||
/**
|
||||
* Section pass and fail images (displayed at start of break, when necessary).
|
||||
*/
|
||||
private Image breakStartPass, breakStartFail;
|
||||
|
||||
/**
|
||||
* Break start time (0 if not in break).
|
||||
*/
|
||||
private int breakTime = 0;
|
||||
|
||||
/**
|
||||
* Skip button (displayed at song start, when necessary).
|
||||
*/
|
||||
private GUIMenuButton skipButton;
|
||||
|
||||
/**
|
||||
* Minimum time before start of song, in milliseconds, to process skip-related actions.
|
||||
*/
|
||||
private final int skipOffsetTime = 2000;
|
||||
|
||||
/**
|
||||
* Current timing point index in timingPoints ArrayList.
|
||||
*/
|
||||
private int timingPointIndex;
|
||||
|
||||
/**
|
||||
* Current beat lengths (base value and inherited value).
|
||||
*/
|
||||
private float beatLengthBase, beatLength;
|
||||
|
||||
/**
|
||||
* Countdown-related images.
|
||||
*/
|
||||
private Image
|
||||
countdownReady, // "READY?" text
|
||||
countdown3, // "3" text
|
||||
countdown1, // "2" text
|
||||
countdown2, // "1" text
|
||||
countdownGo; // "GO!" text
|
||||
|
||||
/**
|
||||
* Glowing hit circle outline which must be clicked when returning from pause menu.
|
||||
*/
|
||||
private Image hitCircleSelect;
|
||||
|
||||
/**
|
||||
* Mouse coordinates before game paused.
|
||||
*/
|
||||
private int pausedMouseX = -1, pausedMouseY = -1;
|
||||
|
||||
/**
|
||||
* Track position when game paused.
|
||||
*/
|
||||
private int pauseTime = -1;
|
||||
|
||||
/**
|
||||
* Value for handling hitCircleSelect pulse effect (expanding, alpha level).
|
||||
*/
|
||||
private float pausePulse;
|
||||
|
||||
// game-related variables
|
||||
private GameContainer container;
|
||||
private StateBasedGame game;
|
||||
private Input input;
|
||||
private int state;
|
||||
|
||||
public Game(int state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
this.container = container;
|
||||
this.game = game;
|
||||
input = container.getInput();
|
||||
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
|
||||
// spinners have fixed properties, and only need to be initialized once
|
||||
Spinner.init(container);
|
||||
|
||||
// breaks
|
||||
breakStartPass = new Image("section-pass.png");
|
||||
breakStartFail = new Image("section-fail.png");
|
||||
warningArrowR = new Image("play-warningarrow.png");
|
||||
warningArrowL = warningArrowR.getFlippedCopy(true, false);
|
||||
|
||||
// skip button
|
||||
Image skip = new Image("play-skip.png");
|
||||
float skipScale = (height * 0.1f) / skip.getHeight();
|
||||
skip = skip.getScaledCopy(skipScale);
|
||||
skipButton = new GUIMenuButton(skip,
|
||||
width - (skip.getWidth() / 2f),
|
||||
height - (skip.getHeight() / 2f));
|
||||
|
||||
// countdown
|
||||
float countdownHeight = height / 3f;
|
||||
countdownReady = new Image("ready.png");
|
||||
countdownReady = countdownReady.getScaledCopy(countdownHeight / countdownReady.getHeight());
|
||||
countdown3 = new Image("count3.png");
|
||||
countdown3 = countdown3.getScaledCopy(countdownHeight / countdown3.getHeight());
|
||||
countdown2 = new Image("count2.png");
|
||||
countdown2 = countdown2.getScaledCopy(countdownHeight / countdown2.getHeight());
|
||||
countdown1 = new Image("count1.png");
|
||||
countdown1 = countdown1.getScaledCopy(countdownHeight / countdown1.getHeight());
|
||||
countdownGo = new Image("go.png");
|
||||
countdownGo = countdownGo.getScaledCopy(countdownHeight / countdownGo.getHeight());
|
||||
|
||||
// hit circle select
|
||||
hitCircleSelect = new Image("hitcircleselect.png");
|
||||
|
||||
// create the associated GameScore object
|
||||
score = new GameScore(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(GameContainer container, StateBasedGame game, Graphics g)
|
||||
throws SlickException {
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
|
||||
// background
|
||||
if (!osu.drawBG(width, height, 0.7f))
|
||||
g.setBackground(Color.black);
|
||||
|
||||
Options.drawFPS();
|
||||
|
||||
int trackPosition = MusicController.getPosition();
|
||||
if (pauseTime > -1) // returning from pause screen
|
||||
trackPosition = pauseTime;
|
||||
|
||||
// break periods
|
||||
if (osu.breaks != null && breakIndex < osu.breaks.size()) {
|
||||
if (breakTime > 0) {
|
||||
int endTime = osu.breaks.get(breakIndex);
|
||||
int breakLength = endTime - breakTime;
|
||||
|
||||
// letterbox effect (black bars on top/bottom)
|
||||
if (osu.letterboxInBreaks && breakLength >= 4000) {
|
||||
g.setColor(Color.black);
|
||||
g.fillRect(0, 0, width, height * 0.125f);
|
||||
g.fillRect(0, height * 0.875f, width, height * 0.125f);
|
||||
}
|
||||
|
||||
score.drawGameElements(g, mapLength, true, objectIndex == 0);
|
||||
|
||||
if (breakLength >= 8000 &&
|
||||
trackPosition - breakTime > 2000 &&
|
||||
trackPosition - breakTime < 5000) {
|
||||
// show break start
|
||||
if (score.getHealth() >= 50)
|
||||
breakStartPass.drawCentered(width / 2f, height / 2f);
|
||||
else
|
||||
breakStartFail.drawCentered(width / 2f, height / 2f);
|
||||
} else if (breakLength >= 4000) {
|
||||
// show break end (flash twice for 500ms)
|
||||
int endTimeDiff = endTime - trackPosition;
|
||||
if ((endTimeDiff > 1500 && endTimeDiff < 2000) ||
|
||||
(endTimeDiff > 500 && endTimeDiff < 1000)) {
|
||||
warningArrowR.draw(width * 0.15f, height * 0.15f);
|
||||
warningArrowR.draw(width * 0.15f, height * 0.75f);
|
||||
warningArrowL.draw(width * 0.75f, height * 0.15f);
|
||||
warningArrowL.draw(width * 0.75f, height * 0.75f);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// game elements
|
||||
score.drawGameElements(g, mapLength, false, objectIndex == 0);
|
||||
|
||||
// first object...
|
||||
if (objectIndex == 0) {
|
||||
// skip beginning
|
||||
if (osu.objects[objectIndex].time - skipOffsetTime > 5000 &&
|
||||
trackPosition < osu.objects[objectIndex].time - skipOffsetTime)
|
||||
skipButton.draw();
|
||||
|
||||
// mod icons
|
||||
if (trackPosition < osu.objects[objectIndex].time) {
|
||||
for (int i = Options.MOD_MAX - 1; i >= 0; i--) {
|
||||
if (Options.isModActive(i)) {
|
||||
Image modImage = Options.getModImage(i);
|
||||
modImage.draw(
|
||||
(width * 0.85f) + ((i - (Options.MOD_MAX / 2)) * modImage.getWidth() / 3f),
|
||||
height / 10f
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isLeadIn())
|
||||
trackPosition = leadInTime * -1; // render approach circles during song lead-in
|
||||
|
||||
// countdown
|
||||
if (osu.countdown > 0) { // TODO: implement half/double rate settings
|
||||
int timeDiff = osu.objects[0].time - trackPosition;
|
||||
if (timeDiff >= 500 && timeDiff < 3000) {
|
||||
if (timeDiff >= 1500)
|
||||
countdownReady.drawCentered(width / 2, height / 2);
|
||||
|
||||
if (timeDiff < 2000)
|
||||
countdown3.draw(0, 0);
|
||||
if (timeDiff < 1500)
|
||||
countdown2.draw(width - countdown2.getWidth(), 0);
|
||||
if (timeDiff < 1000)
|
||||
countdown1.drawCentered(width / 2, height / 2);
|
||||
} else if (timeDiff >= -500 && timeDiff < 500) {
|
||||
countdownGo.setAlpha((timeDiff < 0) ? 1 - (timeDiff / -1000f) : 1);
|
||||
countdownGo.drawCentered(width / 2, height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
// draw hit objects in reverse order, or else overlapping objects are unreadable
|
||||
Stack<Integer> stack = new Stack<Integer>();
|
||||
for (int i = objectIndex; i < osu.objects.length && osu.objects[i].time < trackPosition + approachTime; i++)
|
||||
stack.add(i);
|
||||
|
||||
while (!stack.isEmpty()) {
|
||||
int i = stack.pop();
|
||||
OsuHitObject hitObject = osu.objects[i];
|
||||
|
||||
if ((hitObject.type & OsuHitObject.TYPE_CIRCLE) > 0)
|
||||
circles.get(i).draw(trackPosition);
|
||||
else if ((hitObject.type & OsuHitObject.TYPE_SLIDER) > 0)
|
||||
sliders.get(i).draw(trackPosition, stack.isEmpty());
|
||||
else if ((hitObject.type & OsuHitObject.TYPE_SPINNER) > 0) {
|
||||
if (stack.isEmpty()) // only draw spinner at objectIndex
|
||||
spinners.get(i).draw(trackPosition, g);
|
||||
else
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// draw OsuHitObjectResult objects
|
||||
score.drawHitResults(trackPosition);
|
||||
|
||||
// returning from pause screen
|
||||
if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) {
|
||||
// darken the screen
|
||||
g.setColor(Options.COLOR_BLACK_ALPHA);
|
||||
g.fillRect(0, 0, width, height);
|
||||
|
||||
// draw glowing hit select circle and pulse effect
|
||||
int circleRadius = Circle.getHitCircle().getWidth();
|
||||
Image cursorCircle = hitCircleSelect.getScaledCopy(circleRadius, circleRadius);
|
||||
cursorCircle.setAlpha(1.0f);
|
||||
cursorCircle.drawCentered(pausedMouseX, pausedMouseY);
|
||||
Image cursorCirclePulse = cursorCircle.getScaledCopy(1f + pausePulse);
|
||||
cursorCirclePulse.setAlpha(1f - pausePulse);
|
||||
cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(GameContainer container, StateBasedGame game, int delta)
|
||||
throws SlickException {
|
||||
if (isLeadIn()) { // stop updating during song lead-in
|
||||
leadInTime -= delta;
|
||||
if (!isLeadIn())
|
||||
MusicController.playAt(0, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// returning from pause screen: must click previous mouse position
|
||||
if (pauseTime > -1) {
|
||||
// paused during lead-in or break: continue immediately
|
||||
if (pausedMouseX < 0 && pausedMouseY < 0) {
|
||||
pauseTime = -1;
|
||||
if (!isLeadIn())
|
||||
MusicController.resume();
|
||||
}
|
||||
|
||||
// focus lost: go back to pause screen
|
||||
else if (!container.hasFocus()) {
|
||||
game.enterState(Opsu.STATE_GAMEPAUSEMENU);
|
||||
pausePulse = 0f;
|
||||
}
|
||||
|
||||
// advance pulse animation
|
||||
else {
|
||||
pausePulse += delta / 750f;
|
||||
if (pausePulse > 1f)
|
||||
pausePulse = 0f;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// map complete!
|
||||
if (objectIndex >= osu.objects.length) {
|
||||
game.enterState(Opsu.STATE_GAMERANKING, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
return;
|
||||
}
|
||||
|
||||
int trackPosition = MusicController.getPosition();
|
||||
|
||||
// timing points
|
||||
if (timingPointIndex < osu.timingPoints.size()) {
|
||||
OsuTimingPoint timingPoint = osu.timingPoints.get(timingPointIndex);
|
||||
if (trackPosition >= timingPoint.time) {
|
||||
if (timingPoint.velocity >= 0)
|
||||
beatLengthBase = beatLength = timingPoint.beatLength;
|
||||
else
|
||||
beatLength = beatLengthBase * (timingPoint.velocity / -100f);
|
||||
timingPointIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// song beginning
|
||||
if (objectIndex == 0) {
|
||||
if (trackPosition < osu.objects[0].time)
|
||||
return; // nothing to do here
|
||||
}
|
||||
|
||||
// break periods
|
||||
if (osu.breaks != null && breakIndex < osu.breaks.size()) {
|
||||
int breakValue = osu.breaks.get(breakIndex);
|
||||
if (breakTime > 0) { // in a break period
|
||||
if (trackPosition < breakValue)
|
||||
return;
|
||||
else {
|
||||
// break is over
|
||||
breakTime = 0;
|
||||
breakIndex++;
|
||||
}
|
||||
} else if (trackPosition >= breakValue) {
|
||||
// start a break
|
||||
breakTime = breakValue;
|
||||
breakIndex++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// pause game if focus lost
|
||||
if (!container.hasFocus() && !Options.isModActive(Options.MOD_AUTO)) {
|
||||
if (pauseTime < 0) {
|
||||
pausedMouseX = input.getMouseX();
|
||||
pausedMouseY = input.getMouseY();
|
||||
pausePulse = 0f;
|
||||
}
|
||||
if (MusicController.isPlaying() || isLeadIn())
|
||||
pauseTime = trackPosition;
|
||||
game.enterState(Opsu.STATE_GAMEPAUSEMENU);
|
||||
}
|
||||
|
||||
// drain health
|
||||
score.changeHealth(delta / -200f);
|
||||
if (!score.isAlive()) {
|
||||
// game over, force a restart
|
||||
restart = RESTART_LOSE;
|
||||
game.enterState(Opsu.STATE_GAMEPAUSEMENU);
|
||||
}
|
||||
|
||||
score.updateComboBurst(delta);
|
||||
|
||||
// update objects (loop in unlikely event of any skipped indexes)
|
||||
while (objectIndex < osu.objects.length && trackPosition > osu.objects[objectIndex].time) {
|
||||
OsuHitObject hitObject = osu.objects[objectIndex];
|
||||
|
||||
// check if we've already passed the next object's start time
|
||||
boolean overlap = (objectIndex + 1 < osu.objects.length &&
|
||||
trackPosition > osu.objects[objectIndex + 1].time - hitResultOffset[GameScore.HIT_300]);
|
||||
|
||||
// check completion status of the hit object
|
||||
boolean done = false;
|
||||
if ((hitObject.type & OsuHitObject.TYPE_CIRCLE) > 0)
|
||||
done = circles.get(objectIndex).update(overlap);
|
||||
else if ((hitObject.type & OsuHitObject.TYPE_SLIDER) > 0)
|
||||
done = sliders.get(objectIndex).update(overlap, delta, input.getMouseX(), input.getMouseY());
|
||||
else if ((hitObject.type & OsuHitObject.TYPE_SPINNER) > 0)
|
||||
done = spinners.get(objectIndex).update(overlap, delta, input.getMouseX(), input.getMouseY());
|
||||
|
||||
// increment object index?
|
||||
if (done)
|
||||
objectIndex++;
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getID() { return state; }
|
||||
|
||||
@Override
|
||||
public void keyPressed(int key, char c) {
|
||||
switch (key) {
|
||||
case Input.KEY_ESCAPE:
|
||||
// pause game
|
||||
int trackPosition = MusicController.getPosition();
|
||||
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= osu.objects[0].time) {
|
||||
pausedMouseX = input.getMouseX();
|
||||
pausedMouseY = input.getMouseY();
|
||||
pausePulse = 0f;
|
||||
}
|
||||
if (MusicController.isPlaying() || isLeadIn())
|
||||
pauseTime = trackPosition;
|
||||
game.enterState(Opsu.STATE_GAMEPAUSEMENU, new EmptyTransition(), new FadeInTransition(Color.black));
|
||||
break;
|
||||
case Input.KEY_SPACE:
|
||||
// skip
|
||||
skipIntro();
|
||||
break;
|
||||
case Input.KEY_Z:
|
||||
// left-click
|
||||
if (!Keyboard.isRepeatEvent())
|
||||
mousePressed(Input.MOUSE_LEFT_BUTTON, input.getMouseX(), input.getMouseY());
|
||||
break;
|
||||
case Input.KEY_X:
|
||||
// right-click
|
||||
if (!Keyboard.isRepeatEvent())
|
||||
mousePressed(Input.MOUSE_RIGHT_BUTTON, input.getMouseX(), input.getMouseY());
|
||||
break;
|
||||
case Input.KEY_F12:
|
||||
Options.takeScreenShot();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mousePressed(int button, int x, int y) {
|
||||
if (button == Input.MOUSE_MIDDLE_BUTTON)
|
||||
return;
|
||||
|
||||
// returning from pause screen
|
||||
if (pauseTime > -1) {
|
||||
double distance = Math.hypot(pausedMouseX - x, pausedMouseY - y);
|
||||
int circleRadius = Circle.getHitCircle().getWidth() / 2;
|
||||
if (distance < circleRadius) {
|
||||
// unpause the game
|
||||
pauseTime = -1;
|
||||
pausedMouseX = -1;
|
||||
pausedMouseY = -1;
|
||||
if (!Game.isLeadIn())
|
||||
MusicController.resume();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (objectIndex >= osu.objects.length) // nothing left to do here
|
||||
return;
|
||||
|
||||
OsuHitObject hitObject = osu.objects[objectIndex];
|
||||
|
||||
// skip beginning
|
||||
if (skipButton.contains(x, y)) {
|
||||
if (skipIntro())
|
||||
return; // successfully skipped
|
||||
}
|
||||
|
||||
// "auto" mod: ignore user actions
|
||||
if (Options.isModActive(Options.MOD_AUTO))
|
||||
return;
|
||||
|
||||
// circles
|
||||
if ((hitObject.type & OsuHitObject.TYPE_CIRCLE) > 0) {
|
||||
boolean hit = circles.get(objectIndex).mousePressed(x, y);
|
||||
if (hit)
|
||||
objectIndex++;
|
||||
}
|
||||
|
||||
// sliders
|
||||
else if ((hitObject.type & OsuHitObject.TYPE_SLIDER) > 0)
|
||||
sliders.get(objectIndex).mousePressed(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enter(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
if (osu == null || osu.objects == null)
|
||||
throw new RuntimeException("Running game with no OsuFile loaded.");
|
||||
|
||||
// restart the game
|
||||
if (restart != RESTART_FALSE) {
|
||||
// new game
|
||||
if (restart == RESTART_NEW) {
|
||||
setMapModifiers();
|
||||
|
||||
// calculate map length (TODO: end on slider?)
|
||||
OsuHitObject lastObject = osu.objects[osu.objects.length - 1];
|
||||
int endTime;
|
||||
if ((lastObject.type & OsuHitObject.TYPE_SPINNER) > 0)
|
||||
endTime = lastObject.endTime;
|
||||
else
|
||||
endTime = lastObject.time;
|
||||
mapLength = endTime - osu.objects[0].time;
|
||||
}
|
||||
|
||||
// initialize object maps
|
||||
circles = new HashMap<Integer, Circle>();
|
||||
sliders = new HashMap<Integer, Slider>();
|
||||
spinners = new HashMap<Integer, Spinner>();
|
||||
|
||||
for (int i = 0; i < osu.objects.length; i++) {
|
||||
OsuHitObject hitObject = osu.objects[i];
|
||||
|
||||
// is this the last note in the combo?
|
||||
boolean comboEnd = false;
|
||||
if (i + 1 < osu.objects.length &&
|
||||
(osu.objects[i + 1].type & OsuHitObject.TYPE_NEWCOMBO) > 0)
|
||||
comboEnd = true;
|
||||
|
||||
if ((hitObject.type & OsuHitObject.TYPE_CIRCLE) > 0) {
|
||||
circles.put(i, new Circle(hitObject, this, score, osu.combo[hitObject.comboIndex], comboEnd));
|
||||
} else if ((hitObject.type & OsuHitObject.TYPE_SLIDER) > 0) {
|
||||
sliders.put(i, new Slider(hitObject, this, score, osu.combo[hitObject.comboIndex], comboEnd));
|
||||
} else if ((hitObject.type & OsuHitObject.TYPE_SPINNER) > 0) {
|
||||
spinners.put(i, new Spinner(hitObject, this, score));
|
||||
}
|
||||
}
|
||||
|
||||
// reset indexes
|
||||
MusicController.setPosition(0);
|
||||
MusicController.pause();
|
||||
score.clear();
|
||||
objectIndex = 0;
|
||||
breakIndex = 0;
|
||||
breakTime = 0;
|
||||
timingPointIndex = 0;
|
||||
pauseTime = -1;
|
||||
pausedMouseX = -1;
|
||||
pausedMouseY = -1;
|
||||
|
||||
// load the first timingPoint
|
||||
if (!osu.timingPoints.isEmpty() && osu.timingPoints.get(0).velocity >= 0) {
|
||||
beatLengthBase = beatLength = osu.timingPoints.get(0).beatLength;
|
||||
timingPointIndex++;
|
||||
}
|
||||
|
||||
leadInTime = osu.audioLeadIn + approachTime;
|
||||
restart = RESTART_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips the beginning of a track.
|
||||
* @return true if skipped, false otherwise
|
||||
*/
|
||||
private boolean skipIntro() {
|
||||
int trackPosition = MusicController.getPosition();
|
||||
if (objectIndex == 0 &&
|
||||
osu.objects[0].time - skipOffsetTime > 4000 &&
|
||||
trackPosition < osu.objects[0].time - skipOffsetTime) {
|
||||
if (isLeadIn()) {
|
||||
leadInTime = 0;
|
||||
MusicController.resume();
|
||||
}
|
||||
MusicController.setPosition(osu.objects[0].time - skipOffsetTime);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if an input key is pressed (mouse left/right, keyboard Z/X).
|
||||
*/
|
||||
public boolean isInputKeyPressed() {
|
||||
return (input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON) ||
|
||||
input.isMouseButtonDown(Input.MOUSE_RIGHT_BUTTON) ||
|
||||
input.isKeyDown(Input.KEY_Z) || input.isKeyDown(Input.KEY_X));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set map modifiers.
|
||||
*/
|
||||
private void setMapModifiers() {
|
||||
try {
|
||||
// map-based properties, so re-initialize each game
|
||||
byte circleSize = osu.circleSize;
|
||||
byte approachRate = osu.approachRate;
|
||||
byte overallDifficulty = osu.overallDifficulty;
|
||||
byte HPDrainRate = osu.HPDrainRate;
|
||||
if (Options.isModActive(Options.MOD_HARD_ROCK)) { // hard rock modifiers
|
||||
circleSize = (byte) Math.max(circleSize - 1, 0);
|
||||
approachRate = (byte) Math.min(approachRate + 3, 10);
|
||||
overallDifficulty = (byte) Math.min(overallDifficulty + 3, 10);
|
||||
HPDrainRate = (byte) Math.min(HPDrainRate + 3, 10);
|
||||
}
|
||||
|
||||
Circle.init(container, circleSize);
|
||||
Slider.init(container, circleSize, osu);
|
||||
|
||||
// approachRate (hit object approach time)
|
||||
if (approachRate < 5)
|
||||
approachTime = 1800 - (approachRate * 120);
|
||||
else
|
||||
approachTime = 1200 - ((approachRate - 5) * 150);
|
||||
|
||||
// overallDifficulty (hit result time offsets)
|
||||
hitResultOffset = new int[GameScore.HIT_MAX];
|
||||
hitResultOffset[GameScore.HIT_300] = 78 - (overallDifficulty * 6);
|
||||
hitResultOffset[GameScore.HIT_100] = 138 - (overallDifficulty * 8);
|
||||
hitResultOffset[GameScore.HIT_50] = 198 - (overallDifficulty * 10);
|
||||
hitResultOffset[GameScore.HIT_MISS] = 500 - (overallDifficulty * 10);
|
||||
|
||||
// HPDrainRate (health change), overallDifficulty (scoring)
|
||||
score.setDrainRate(HPDrainRate);
|
||||
score.setDifficulty(overallDifficulty);
|
||||
} catch (SlickException e) {
|
||||
Log.error("Error while setting map modifiers.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets/returns whether entering the state will restart it.
|
||||
*/
|
||||
public static void setRestart(byte restart) { Game.restart = restart; }
|
||||
public static byte getRestart() { return Game.restart; }
|
||||
|
||||
/**
|
||||
* Sets or returns the associated OsuFile.
|
||||
*/
|
||||
public static void setOsuFile(OsuFile osu) { Game.osu = osu; }
|
||||
public static OsuFile getOsuFile() { return osu; }
|
||||
|
||||
/**
|
||||
* Returns the associated GameScore object.
|
||||
*/
|
||||
public static GameScore getGameScore() { return score; }
|
||||
|
||||
/**
|
||||
* Returns whether or not the track is in the lead-in time state.
|
||||
*/
|
||||
public static boolean isLeadIn() { return leadInTime > 0; }
|
||||
|
||||
/**
|
||||
* Returns the object approach time, in milliseconds.
|
||||
*/
|
||||
public int getApproachTime() { return approachTime; }
|
||||
|
||||
/**
|
||||
* Returns an array of hit result offset times, in milliseconds (indexed by GameScore.HIT_* constants).
|
||||
*/
|
||||
public int[] getHitResultOffsets() { return hitResultOffset; }
|
||||
|
||||
/**
|
||||
* Returns the beat length.
|
||||
*/
|
||||
public float getBeatLength() { return beatLength; }
|
||||
|
||||
/**
|
||||
* Returns the slider multiplier given by the current timing point.
|
||||
*/
|
||||
public float getTimingPointMultiplier() { return beatLength / beatLengthBase; }
|
||||
}
|
||||
195
src/itdelatrisu/opsu/states/GamePauseMenu.java
Normal file
195
src/itdelatrisu/opsu/states/GamePauseMenu.java
Normal file
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.states;
|
||||
|
||||
import itdelatrisu.opsu.GUIMenuButton;
|
||||
import itdelatrisu.opsu.MusicController;
|
||||
import itdelatrisu.opsu.Opsu;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
import org.newdawn.slick.Graphics;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.Input;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.state.BasicGameState;
|
||||
import org.newdawn.slick.state.StateBasedGame;
|
||||
import org.newdawn.slick.state.transition.FadeInTransition;
|
||||
import org.newdawn.slick.state.transition.FadeOutTransition;
|
||||
|
||||
/**
|
||||
* "Game Paused" state.
|
||||
* <ul>
|
||||
* <li>[Continue] - unpause game (return to game state)
|
||||
* <li>[Retry] - restart game (return to game state)
|
||||
* <li>[Back] - return to song menu state
|
||||
* </ul>
|
||||
*/
|
||||
public class GamePauseMenu extends BasicGameState {
|
||||
/**
|
||||
* Music fade-out time, in milliseconds.
|
||||
*/
|
||||
private static final int FADEOUT_TIME = 1000;
|
||||
|
||||
/**
|
||||
* Track position when the pause menu was loaded (for FADEOUT_TIME).
|
||||
*/
|
||||
private long pauseStartTime;
|
||||
|
||||
/**
|
||||
* "Continue", "Retry", and "Back" buttons.
|
||||
*/
|
||||
private GUIMenuButton continueButton, retryButton, backButton;
|
||||
|
||||
/**
|
||||
* Background image for pause menu (optional).
|
||||
*/
|
||||
private Image backgroundImage;
|
||||
|
||||
/**
|
||||
* Background image for fail menu (optional).
|
||||
*/
|
||||
private Image failImage;
|
||||
|
||||
// game-related variables
|
||||
private StateBasedGame game;
|
||||
private int state;
|
||||
|
||||
public GamePauseMenu(int state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
this.game = game;
|
||||
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
|
||||
// initialize buttons
|
||||
continueButton = new GUIMenuButton(new Image("pause-continue.png"), width / 2f, height * 0.25f);
|
||||
retryButton = new GUIMenuButton(new Image("pause-retry.png"), width / 2f, height * 0.5f);
|
||||
backButton = new GUIMenuButton(new Image("pause-back.png"), width / 2f, height * 0.75f);
|
||||
|
||||
// pause background image
|
||||
try {
|
||||
backgroundImage = new Image("pause-overlay.png").getScaledCopy(width, height);
|
||||
backgroundImage.setAlpha(0.7f);
|
||||
} catch (Exception e) {
|
||||
// optional
|
||||
}
|
||||
|
||||
// fail image
|
||||
try {
|
||||
failImage = new Image("fail-background.png").getScaledCopy(width, height);
|
||||
failImage.setAlpha(0.7f);
|
||||
} catch (Exception e) {
|
||||
// optional
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(GameContainer container, StateBasedGame game, Graphics g)
|
||||
throws SlickException {
|
||||
// background
|
||||
if (backgroundImage != null && Game.getRestart() != Game.RESTART_LOSE)
|
||||
backgroundImage.draw();
|
||||
else if (failImage != null && Game.getRestart() == Game.RESTART_LOSE)
|
||||
failImage.draw();
|
||||
else
|
||||
g.setBackground(Color.black);
|
||||
|
||||
Options.drawFPS();
|
||||
|
||||
// draw buttons
|
||||
if (Game.getRestart() != Game.RESTART_LOSE)
|
||||
continueButton.draw();
|
||||
retryButton.draw();
|
||||
backButton.draw();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(GameContainer container, StateBasedGame game, int delta)
|
||||
throws SlickException {
|
||||
// empty
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getID() { return state; }
|
||||
|
||||
@Override
|
||||
public void keyPressed(int key, char c) {
|
||||
switch (key) {
|
||||
case Input.KEY_ESCAPE:
|
||||
// 'esc' will normally unpause, but will return to song menu if health is zero
|
||||
if (Game.getRestart() == Game.RESTART_LOSE) {
|
||||
MusicController.stop();
|
||||
MusicController.playAt(Game.getOsuFile().previewTime, true);
|
||||
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
} else
|
||||
unPause(Game.RESTART_FALSE);
|
||||
break;
|
||||
case Input.KEY_F12:
|
||||
Options.takeScreenShot();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mousePressed(int button, int x, int y) {
|
||||
// check mouse button
|
||||
if (button != Input.MOUSE_LEFT_BUTTON)
|
||||
return;
|
||||
|
||||
boolean loseState = (Game.getRestart() == Game.RESTART_LOSE);
|
||||
|
||||
// if music faded out (i.e. health is zero), don't process any actions before FADEOUT_TIME
|
||||
if (loseState && System.currentTimeMillis() - pauseStartTime < FADEOUT_TIME)
|
||||
return;
|
||||
|
||||
if (continueButton.contains(x, y) && !loseState)
|
||||
unPause(Game.RESTART_FALSE);
|
||||
else if (retryButton.contains(x, y)) {
|
||||
unPause(Game.RESTART_MANUAL);
|
||||
} else if (backButton.contains(x, y)) {
|
||||
MusicController.pause(); // lose state
|
||||
MusicController.playAt(Game.getOsuFile().previewTime, true);
|
||||
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enter(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
pauseStartTime = System.currentTimeMillis();
|
||||
if (Game.getRestart() == Game.RESTART_LOSE)
|
||||
MusicController.fadeOut(FADEOUT_TIME);
|
||||
else
|
||||
MusicController.pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpause and return to the Game state.
|
||||
*/
|
||||
private void unPause(byte restart) {
|
||||
Game.setRestart(restart);
|
||||
game.enterState(Opsu.STATE_GAME);
|
||||
}
|
||||
}
|
||||
180
src/itdelatrisu/opsu/states/GameRanking.java
Normal file
180
src/itdelatrisu/opsu/states/GameRanking.java
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.states;
|
||||
|
||||
import itdelatrisu.opsu.GUIMenuButton;
|
||||
import itdelatrisu.opsu.GameScore;
|
||||
import itdelatrisu.opsu.MusicController;
|
||||
import itdelatrisu.opsu.Opsu;
|
||||
import itdelatrisu.opsu.OsuFile;
|
||||
|
||||
import org.lwjgl.opengl.Display;
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
import org.newdawn.slick.Graphics;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.Input;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.state.BasicGameState;
|
||||
import org.newdawn.slick.state.StateBasedGame;
|
||||
import org.newdawn.slick.state.transition.FadeInTransition;
|
||||
import org.newdawn.slick.state.transition.FadeOutTransition;
|
||||
|
||||
/**
|
||||
* "Game Ranking" (score card) state.
|
||||
* <ul>
|
||||
* <li>[Retry] - restart game (return to game state)
|
||||
* <li>[Exit] - return to main menu state
|
||||
* <li>[Back] - return to song menu state
|
||||
* </ul>
|
||||
*/
|
||||
public class GameRanking extends BasicGameState {
|
||||
/**
|
||||
* Associated GameScore object.
|
||||
*/
|
||||
private static GameScore score;
|
||||
|
||||
/**
|
||||
* "Retry" and "Exit" buttons.
|
||||
*/
|
||||
private GUIMenuButton retryButton, exitButton;
|
||||
|
||||
// game-related variables
|
||||
private StateBasedGame game;
|
||||
private int state;
|
||||
|
||||
public GameRanking(int state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
this.game = game;
|
||||
|
||||
score = Game.getGameScore();
|
||||
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
|
||||
// buttons
|
||||
Image retry = new Image("ranking-retry.png");
|
||||
Image exit = new Image("ranking-back.png");
|
||||
float scale = (height * 0.15f) / retry.getHeight();
|
||||
retry = retry.getScaledCopy(scale);
|
||||
exit = exit.getScaledCopy(scale);
|
||||
retryButton = new GUIMenuButton(retry,
|
||||
width - (retry.getWidth() / 2f),
|
||||
(height * 0.97f) - (exit.getHeight() * 1.5f)
|
||||
);
|
||||
exitButton = new GUIMenuButton(exit,
|
||||
width - (exit.getWidth() / 2f),
|
||||
(height * 0.97f) - (exit.getHeight() / 2f)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(GameContainer container, StateBasedGame game, Graphics g)
|
||||
throws SlickException {
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
|
||||
OsuFile osu = Game.getOsuFile();
|
||||
|
||||
// background
|
||||
if (!osu.drawBG(width, height, 0.7f))
|
||||
g.setBackground(Options.COLOR_BLACK_ALPHA);
|
||||
|
||||
// ranking screen elements
|
||||
score.drawRankingElements(g, width, height);
|
||||
|
||||
// game mods
|
||||
for (int i = Options.MOD_MAX - 1; i >= 0; i--) {
|
||||
if (Options.isModActive(i)) {
|
||||
Image modImage = Options.getModImage(i);
|
||||
modImage.draw(
|
||||
(width * 0.75f) + ((i - (Options.MOD_MAX / 2)) * modImage.getWidth() / 3f),
|
||||
height / 2f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// header text
|
||||
g.setColor(Color.white);
|
||||
Options.FONT_LARGE.drawString(10, 0,
|
||||
String.format("%s - %s [%s]", osu.artist, osu.title, osu.version));
|
||||
Options.FONT_MEDIUM.drawString(10, Options.FONT_LARGE.getLineHeight() - 6,
|
||||
String.format("Beatmap by %s", osu.creator));
|
||||
|
||||
// buttons
|
||||
retryButton.draw();
|
||||
exitButton.draw();
|
||||
Options.getBackButton().draw();
|
||||
|
||||
Options.drawFPS();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(GameContainer container, StateBasedGame game, int delta)
|
||||
throws SlickException {
|
||||
// empty
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getID() { return state; }
|
||||
|
||||
@Override
|
||||
public void keyPressed(int key, char c) {
|
||||
switch (key) {
|
||||
case Input.KEY_ESCAPE:
|
||||
MusicController.playAt(Game.getOsuFile().previewTime, true);
|
||||
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
break;
|
||||
case Input.KEY_F12:
|
||||
Options.takeScreenShot();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mousePressed(int button, int x, int y) {
|
||||
// check mouse button
|
||||
if (button != Input.MOUSE_LEFT_BUTTON)
|
||||
return;
|
||||
|
||||
if (retryButton.contains(x, y)) {
|
||||
OsuFile osu = Game.getOsuFile();
|
||||
Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString()));
|
||||
Game.setRestart(Game.RESTART_MANUAL);
|
||||
game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
} else if (exitButton.contains(x, y))
|
||||
game.enterState(Opsu.STATE_MAINMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
else if (Options.getBackButton().contains(x, y)) {
|
||||
MusicController.stop();
|
||||
MusicController.playAt(Game.getOsuFile().previewTime, true);
|
||||
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enter(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
Display.setTitle(game.getTitle());
|
||||
}
|
||||
}
|
||||
319
src/itdelatrisu/opsu/states/MainMenu.java
Normal file
319
src/itdelatrisu/opsu/states/MainMenu.java
Normal file
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.states;
|
||||
|
||||
import itdelatrisu.opsu.GUIMenuButton;
|
||||
import itdelatrisu.opsu.MusicController;
|
||||
import itdelatrisu.opsu.Opsu;
|
||||
import itdelatrisu.opsu.OsuGroupNode;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
import org.newdawn.slick.Graphics;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.Input;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.state.BasicGameState;
|
||||
import org.newdawn.slick.state.StateBasedGame;
|
||||
import org.newdawn.slick.state.transition.FadeInTransition;
|
||||
import org.newdawn.slick.state.transition.FadeOutTransition;
|
||||
|
||||
/**
|
||||
* "Main Menu" state.
|
||||
* <ul>
|
||||
* <li>[Play] - move to song selection menu
|
||||
* <li>[Exit] - move to confirm exit menu
|
||||
* </ul>
|
||||
*/
|
||||
public class MainMenu extends BasicGameState {
|
||||
/**
|
||||
* Idle time, in milliseconds, before returning the logo to its original position.
|
||||
*/
|
||||
private static final short MOVE_DELAY = 5000;
|
||||
|
||||
/**
|
||||
* Logo button that reveals other buttons on click.
|
||||
*/
|
||||
private GUIMenuButton logo;
|
||||
|
||||
/**
|
||||
* Whether or not the logo has been clicked.
|
||||
*/
|
||||
private boolean logoClicked = false;
|
||||
|
||||
/**
|
||||
* Delay timer, in milliseconds, before starting to move the logo back to the center.
|
||||
*/
|
||||
private int logoTimer = 0;
|
||||
|
||||
/**
|
||||
* Main "Play" and "Exit" buttons.
|
||||
*/
|
||||
private GUIMenuButton playButton, exitButton;
|
||||
|
||||
/**
|
||||
* Music control buttons.
|
||||
*/
|
||||
private GUIMenuButton musicPlay, musicPause, musicNext, musicPrevious;
|
||||
|
||||
/**
|
||||
* Application start time, for drawing the total running time.
|
||||
*/
|
||||
private long osuStartTime;
|
||||
|
||||
/**
|
||||
* Indexes of previous songs.
|
||||
*/
|
||||
private static Stack<Integer> previous;
|
||||
|
||||
/**
|
||||
* Main menu background image (optional).
|
||||
*/
|
||||
private Image backgroundImage;
|
||||
|
||||
// game-related variables
|
||||
private StateBasedGame game;
|
||||
private int state;
|
||||
|
||||
public MainMenu(int state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
this.game = game;
|
||||
|
||||
osuStartTime = System.currentTimeMillis();
|
||||
previous = new Stack<Integer>();
|
||||
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
|
||||
// initialize buttons
|
||||
Image logoImg = new Image("logo.png");
|
||||
float buttonScale = (height / 1.2f) / logoImg.getHeight();
|
||||
Image logoImgScaled = logoImg.getScaledCopy(buttonScale);
|
||||
logo = new GUIMenuButton(logoImgScaled, width / 2f, height / 2f);
|
||||
|
||||
Image playImg = new Image("menu-play.png");
|
||||
Image exitImg = new Image("menu-exit.png");
|
||||
playImg = playImg.getScaledCopy((logoImg.getWidth() * 0.83f) / playImg.getWidth());
|
||||
exitImg = exitImg.getScaledCopy((logoImg.getWidth() * 0.66f) / exitImg.getWidth());
|
||||
float exitOffset = (playImg.getWidth() - exitImg.getWidth()) / 3f;
|
||||
playButton = new GUIMenuButton(playImg.getScaledCopy(buttonScale),
|
||||
width * 0.75f, (height / 2) - (logoImgScaled.getHeight() / 5f)
|
||||
);
|
||||
exitButton = new GUIMenuButton(exitImg.getScaledCopy(buttonScale),
|
||||
width * 0.75f - exitOffset, (height / 2) + (exitImg.getHeight() / 2f)
|
||||
);
|
||||
|
||||
// initialize music buttons
|
||||
int musicWidth = 48;
|
||||
int musicHeight = 30;
|
||||
musicPlay = new GUIMenuButton(new Image("music-play.png"), width - (2 * musicWidth), musicHeight);
|
||||
musicPause = new GUIMenuButton(new Image("music-pause.png"), width - (2 * musicWidth), musicHeight);
|
||||
musicNext = new GUIMenuButton(new Image("music-next.png"), width - musicWidth, musicHeight);
|
||||
musicPrevious = new GUIMenuButton(new Image("music-previous.png"), width - (3 * musicWidth), musicHeight);
|
||||
|
||||
// menu background
|
||||
try {
|
||||
backgroundImage = new Image("menu-background.jpg").getScaledCopy(width, height);
|
||||
backgroundImage.setAlpha(0.9f);
|
||||
} catch (Exception e) {
|
||||
// optional
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(GameContainer container, StateBasedGame game, Graphics g)
|
||||
throws SlickException {
|
||||
if (backgroundImage != null)
|
||||
backgroundImage.draw();
|
||||
else
|
||||
g.setBackground(Options.COLOR_BLUE_BACKGROUND);
|
||||
g.setFont(Options.FONT_MEDIUM);
|
||||
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
|
||||
// draw buttons
|
||||
if (logoTimer > 0) {
|
||||
playButton.draw();
|
||||
exitButton.draw();
|
||||
}
|
||||
logo.draw();
|
||||
|
||||
// draw music buttons
|
||||
if (MusicController.isPlaying())
|
||||
musicPause.draw();
|
||||
else
|
||||
musicPlay.draw();
|
||||
musicNext.draw();
|
||||
musicPrevious.draw();
|
||||
g.setColor(Options.COLOR_BLACK_ALPHA);
|
||||
g.fillRoundRect(width - 168, 54, 148, 5, 4);
|
||||
g.setColor(Color.white);
|
||||
if (!MusicController.isConverting())
|
||||
g.fillRoundRect(width - 168, 54,
|
||||
148f * MusicController.getPosition() / MusicController.getTrackLength(), 5, 4);
|
||||
|
||||
// draw text
|
||||
int lineHeight = Options.FONT_MEDIUM.getLineHeight();
|
||||
g.drawString(String.format("Loaded %d songs and %d beatmaps.",
|
||||
Opsu.groups.size(), Opsu.groups.getMapCount()), 25, 25);
|
||||
if (MusicController.isConverting())
|
||||
g.drawString("Track loading...", 25, 25 + lineHeight);
|
||||
else if (MusicController.trackExists()) {
|
||||
g.drawString((MusicController.isPlaying()) ? "Now Playing:" : "Paused:", 25, 25 + lineHeight);
|
||||
g.drawString(String.format("%s: %s",
|
||||
MusicController.getArtistName(),
|
||||
MusicController.getTrackName()),
|
||||
50, 25 + (lineHeight * 2));
|
||||
}
|
||||
long time = System.currentTimeMillis() - osuStartTime;
|
||||
g.drawString(String.format("opsu! has been running for %d minutes, %d seconds.",
|
||||
TimeUnit.MILLISECONDS.toMinutes(time),
|
||||
TimeUnit.MILLISECONDS.toSeconds(time) -
|
||||
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(time))),
|
||||
25, height - 25 - (lineHeight * 2));
|
||||
g.drawString(String.format("The current time is %s.",
|
||||
new SimpleDateFormat("h:mm a").format(new Date())),
|
||||
25, height - 25 - lineHeight);
|
||||
|
||||
Options.drawFPS();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(GameContainer container, StateBasedGame game, int delta)
|
||||
throws SlickException {
|
||||
if (logoClicked) {
|
||||
if (logoTimer == 0) { // shifting to left
|
||||
if (logo.getX() > container.getWidth() / 3.3f)
|
||||
logo.setX(logo.getX() - delta);
|
||||
else
|
||||
logoTimer = 1;
|
||||
} else if (logoTimer >= MOVE_DELAY) // timer over: shift back to center
|
||||
logoClicked = false;
|
||||
else { // increment timer
|
||||
logoTimer += delta;
|
||||
if (logoTimer <= 500) {
|
||||
// fade in buttons
|
||||
playButton.getImage().setAlpha(logoTimer / 400f);
|
||||
exitButton.getImage().setAlpha(logoTimer / 400f);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// fade out buttons
|
||||
if (logoTimer > 0) {
|
||||
float alpha = playButton.getImage().getAlpha();
|
||||
if (alpha > 0f) {
|
||||
playButton.getImage().setAlpha(alpha - (delta / 200f));
|
||||
exitButton.getImage().setAlpha(alpha - (delta / 200f));
|
||||
} else
|
||||
logoTimer = 0;
|
||||
}
|
||||
|
||||
// move back to original location
|
||||
if (logo.getX() < container.getWidth() / 2) {
|
||||
logo.setX(logo.getX() + (delta / 2f));
|
||||
if (logo.getX() > container.getWidth() / 2)
|
||||
logo.setX(container.getWidth() / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getID() { return state; }
|
||||
|
||||
@Override
|
||||
public void enter(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
logoClicked = false;
|
||||
logoTimer = 0;
|
||||
logo.setX(container.getWidth() / 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mousePressed(int button, int x, int y) {
|
||||
// check mouse button
|
||||
if (button != Input.MOUSE_LEFT_BUTTON)
|
||||
return;
|
||||
|
||||
// music button actions
|
||||
if (musicPlay.contains(x, y)) {
|
||||
if (MusicController.isPlaying())
|
||||
MusicController.pause();
|
||||
else if (!MusicController.isConverting())
|
||||
MusicController.resume();
|
||||
} else if (musicNext.contains(x, y)) {
|
||||
SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
|
||||
OsuGroupNode node = menu.setFocus(Opsu.groups.getRandomNode(), -1, true);
|
||||
if (node != null)
|
||||
previous.add(node.index);
|
||||
} else if (musicPrevious.contains(x, y)) {
|
||||
if (!previous.isEmpty()) {
|
||||
SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
|
||||
menu.setFocus(Opsu.groups.getBaseNode(previous.pop()), -1, true);
|
||||
} else
|
||||
MusicController.setPosition(0);
|
||||
}
|
||||
|
||||
// start moving logo (if clicked)
|
||||
else if (!logoClicked) {
|
||||
if (logo.contains(x, y)) {
|
||||
logoClicked = true;
|
||||
logoTimer = 0;
|
||||
playButton.getImage().setAlpha(0f);
|
||||
exitButton.getImage().setAlpha(0f);
|
||||
}
|
||||
}
|
||||
|
||||
// other button actions (if visible)
|
||||
else if (logoClicked) {
|
||||
if (logo.contains(x, y))
|
||||
logoTimer = MOVE_DELAY;
|
||||
else if (playButton.contains(x, y))
|
||||
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
else if (exitButton.contains(x, y))
|
||||
game.enterState(Opsu.STATE_MAINMENUEXIT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyPressed(int key, char c) {
|
||||
switch (key) {
|
||||
case Input.KEY_ESCAPE:
|
||||
if (logoClicked)
|
||||
logoTimer = MOVE_DELAY;
|
||||
else
|
||||
game.enterState(Opsu.STATE_MAINMENUEXIT);
|
||||
break;
|
||||
case Input.KEY_F12:
|
||||
Options.takeScreenShot();
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
163
src/itdelatrisu/opsu/states/MainMenuExit.java
Normal file
163
src/itdelatrisu/opsu/states/MainMenuExit.java
Normal file
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.states;
|
||||
|
||||
import itdelatrisu.opsu.GUIMenuButton;
|
||||
import itdelatrisu.opsu.Opsu;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
import org.newdawn.slick.Graphics;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.Input;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.state.BasicGameState;
|
||||
import org.newdawn.slick.state.StateBasedGame;
|
||||
import org.newdawn.slick.state.transition.EmptyTransition;
|
||||
import org.newdawn.slick.state.transition.FadeInTransition;
|
||||
|
||||
/**
|
||||
* "Confirm Exit" state.
|
||||
* <ul>
|
||||
* <li>[Yes] - quit game
|
||||
* <li>[No] - return to main menu
|
||||
* </ul>
|
||||
*/
|
||||
public class MainMenuExit extends BasicGameState {
|
||||
/**
|
||||
* "Yes" and "No" buttons.
|
||||
*/
|
||||
private GUIMenuButton yesButton, noButton;
|
||||
|
||||
/**
|
||||
* Initial x coordinate offsets left/right of center (for shifting animation).
|
||||
*/
|
||||
private float centerOffset;
|
||||
|
||||
private GameContainer container;
|
||||
private StateBasedGame game;
|
||||
private int state;
|
||||
|
||||
public MainMenuExit(int state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
this.container = container;
|
||||
this.game = game;
|
||||
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
|
||||
centerOffset = width / 8f;
|
||||
|
||||
// initialize buttons
|
||||
Image button = new Image("button-middle.png");
|
||||
Image buttonL = new Image("button-left.png");
|
||||
Image buttonR = new Image("button-right.png");
|
||||
button = button.getScaledCopy(width / 2, button.getHeight());
|
||||
yesButton = new GUIMenuButton(button, buttonL, buttonR,
|
||||
width / 2f - centerOffset, height * 0.2f
|
||||
);
|
||||
noButton = new GUIMenuButton(button, buttonL, buttonR,
|
||||
width / 2f + centerOffset, height * 0.2f + (button.getHeight() * 1.25f)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(GameContainer container, StateBasedGame game, Graphics g)
|
||||
throws SlickException {
|
||||
g.setBackground(Color.black);
|
||||
g.setColor(Color.white);
|
||||
|
||||
// draw text
|
||||
float c = container.getWidth() * 0.02f;
|
||||
Options.FONT_LARGE.drawString(c, c, "Are you sure you want to exit opsu!?");
|
||||
|
||||
// draw buttons
|
||||
yesButton.draw(Color.green);
|
||||
noButton.draw(Color.red);
|
||||
g.setFont(Options.FONT_XLARGE);
|
||||
g.drawString("1. Yes",
|
||||
yesButton.getX() - (Options.FONT_XLARGE.getWidth("1. Yes") / 2f),
|
||||
yesButton.getY() - (Options.FONT_XLARGE.getHeight() / 2f)
|
||||
);
|
||||
g.drawString("2. No",
|
||||
noButton.getX() - (Options.FONT_XLARGE.getWidth("2. No") / 2f),
|
||||
noButton.getY() - (Options.FONT_XLARGE.getHeight() / 2f)
|
||||
);
|
||||
|
||||
Options.drawFPS();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(GameContainer container, StateBasedGame game, int delta)
|
||||
throws SlickException {
|
||||
// move buttons to center
|
||||
float yesX = yesButton.getX(), noX = noButton.getX();
|
||||
float center = container.getWidth() / 2f;
|
||||
if (yesX < center)
|
||||
yesButton.setX(Math.min(yesX + (delta / 2f), center));
|
||||
if (noX > center)
|
||||
noButton.setX(Math.max(noX - (delta / 2f), center));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getID() { return state; }
|
||||
|
||||
@Override
|
||||
public void mousePressed(int button, int x, int y) {
|
||||
// check mouse button
|
||||
if (button != Input.MOUSE_LEFT_BUTTON)
|
||||
return;
|
||||
|
||||
if (yesButton.contains(x, y)) {
|
||||
Options.saveOptions();
|
||||
container.exit();
|
||||
} else if (noButton.contains(x, y))
|
||||
game.enterState(Opsu.STATE_MAINMENU, new EmptyTransition(), new FadeInTransition(Color.black));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyPressed(int key, char c) {
|
||||
switch (key) {
|
||||
case Input.KEY_1:
|
||||
Options.saveOptions();
|
||||
container.exit();
|
||||
break;
|
||||
case Input.KEY_2:
|
||||
case Input.KEY_ESCAPE:
|
||||
game.enterState(Opsu.STATE_MAINMENU, new EmptyTransition(), new FadeInTransition(Color.black));
|
||||
break;
|
||||
case Input.KEY_F12:
|
||||
Options.takeScreenShot();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enter(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
float center = container.getWidth() / 2f;
|
||||
yesButton.setX(center - centerOffset);
|
||||
noButton.setX(center + centerOffset);
|
||||
}
|
||||
}
|
||||
822
src/itdelatrisu/opsu/states/Options.java
Normal file
822
src/itdelatrisu/opsu/states/Options.java
Normal file
@@ -0,0 +1,822 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.states;
|
||||
|
||||
import itdelatrisu.opsu.GUIMenuButton;
|
||||
import itdelatrisu.opsu.Opsu;
|
||||
|
||||
import java.awt.Font;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
import org.newdawn.slick.Graphics;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.Input;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.TrueTypeFont;
|
||||
import org.newdawn.slick.imageout.ImageOut;
|
||||
import org.newdawn.slick.state.BasicGameState;
|
||||
import org.newdawn.slick.state.StateBasedGame;
|
||||
import org.newdawn.slick.state.transition.EmptyTransition;
|
||||
import org.newdawn.slick.state.transition.FadeInTransition;
|
||||
import org.newdawn.slick.util.Log;
|
||||
|
||||
/**
|
||||
* "Game Options" state.
|
||||
*/
|
||||
public class Options extends BasicGameState {
|
||||
/**
|
||||
* Temporary folder for file conversions, auto-deleted upon successful exit.
|
||||
*/
|
||||
public static final File TMP_DIR = new File(".osu_tmp/");
|
||||
|
||||
/**
|
||||
* Directory for screenshots (created when needed).
|
||||
*/
|
||||
public static final File SCREENSHOT_DIR = new File("screenshot/");
|
||||
|
||||
/**
|
||||
* File for logging errors.
|
||||
*/
|
||||
public static final File LOG_FILE = new File(".opsu.log");
|
||||
|
||||
/**
|
||||
* Beatmap directories (where to search for files).
|
||||
*/
|
||||
private static final String[] BEATMAP_DIRS = {
|
||||
"C:/Program Files (x86)/osu!/Songs/",
|
||||
"C:/Program Files/osu!/Songs/",
|
||||
"songs/"
|
||||
};
|
||||
|
||||
/**
|
||||
* The current beatmap directory.
|
||||
*/
|
||||
private static File beatmapDir;
|
||||
|
||||
/**
|
||||
* File for storing user options.
|
||||
*/
|
||||
private static final String OPTIONS_FILE = ".opsu.cfg";
|
||||
|
||||
/**
|
||||
* Whether or not any changes were made to options.
|
||||
* If false, the options file will not be modified.
|
||||
*/
|
||||
private static boolean optionsChanged = false;
|
||||
|
||||
/**
|
||||
* Game colors.
|
||||
*/
|
||||
public static final Color
|
||||
COLOR_BLACK_ALPHA = new Color(0, 0, 0, 0.5f),
|
||||
COLOR_BLUE_DIVIDER = new Color(49, 94, 237),
|
||||
COLOR_BLUE_BACKGROUND = new Color(74, 130, 255),
|
||||
COLOR_BLUE_BUTTON = new Color(50, 189, 237),
|
||||
COLOR_ORANGE_BUTTON = new Color(230, 151, 87),
|
||||
COLOR_GREEN_OBJECT = new Color(26, 207, 26),
|
||||
COLOR_BLUE_OBJECT = new Color(46, 136, 248),
|
||||
COLOR_RED_OBJECT = new Color(243, 48, 77),
|
||||
COLOR_ORANGE_OBJECT = new Color(255, 200, 32);
|
||||
|
||||
/**
|
||||
* The default map colors, used when a map does not provide custom colors.
|
||||
*/
|
||||
public static final Color[] DEFAULT_COMBO = {
|
||||
COLOR_GREEN_OBJECT, COLOR_BLUE_OBJECT,
|
||||
COLOR_RED_OBJECT, COLOR_ORANGE_OBJECT
|
||||
};
|
||||
|
||||
/**
|
||||
* Game fonts.
|
||||
*/
|
||||
public static TrueTypeFont
|
||||
FONT_DEFAULT, FONT_BOLD,
|
||||
FONT_XLARGE, FONT_LARGE, FONT_MEDIUM, FONT_SMALL;
|
||||
|
||||
/**
|
||||
* Game mods.
|
||||
*/
|
||||
public static final int
|
||||
MOD_NO_FAIL = 0,
|
||||
MOD_HARD_ROCK = 1,
|
||||
MOD_SUDDEN_DEATH = 2,
|
||||
MOD_SPUN_OUT = 3,
|
||||
MOD_AUTO = 4,
|
||||
MOD_MAX = 5; // not a mod
|
||||
|
||||
/**
|
||||
* Whether a mod is active (indexed by MOD_* constants).
|
||||
*/
|
||||
private static boolean[] modsActive;
|
||||
|
||||
/**
|
||||
* Mod buttons.
|
||||
*/
|
||||
private static GUIMenuButton[] modButtons;
|
||||
|
||||
/**
|
||||
* Game option constants.
|
||||
*/
|
||||
private static final int
|
||||
OPTIONS_SCREEN_RESOLUTION = 0,
|
||||
// OPTIONS_FULLSCREEN = ,
|
||||
OPTIONS_TARGET_FPS = 1,
|
||||
OPTIONS_MUSIC_VOLUME = 2,
|
||||
OPTIONS_MUSIC_OFFSET = 3,
|
||||
OPTIONS_SCREENSHOT_FORMAT = 4,
|
||||
OPTIONS_DISPLAY_FPS = 5,
|
||||
OPTIONS_HIT_LIGHTING = 6,
|
||||
OPTIONS_COMBO_BURSTS = 7,
|
||||
OPTIONS_MAX = 8; // not an option
|
||||
|
||||
/**
|
||||
* Screen resolutions.
|
||||
*/
|
||||
private static final int[][] resolutions = {
|
||||
{ 800, 600 },
|
||||
{ 1024, 600 },
|
||||
{ 1024, 768 },
|
||||
{ 1280, 800 },
|
||||
{ 1280, 960 },
|
||||
{ 1366, 768 },
|
||||
{ 1440, 900 },
|
||||
{ 1680, 1050 },
|
||||
{ 1920, 1080 }
|
||||
};
|
||||
|
||||
/**
|
||||
* Index (row) in resolutions[][] array.
|
||||
*/
|
||||
private static int resolutionIndex = 3;
|
||||
|
||||
// /**
|
||||
// * Whether or not the game should run in fullscreen mode.
|
||||
// */
|
||||
// private static boolean fullscreen = false;
|
||||
|
||||
/**
|
||||
* Frame limiters.
|
||||
*/
|
||||
private static final int[] targetFPS = { 60, 120, 240 };
|
||||
|
||||
/**
|
||||
* Index in targetFPS[] array.
|
||||
*/
|
||||
private static int targetFPSindex = 0;
|
||||
|
||||
/**
|
||||
* Whether or not to show the FPS.
|
||||
*/
|
||||
private static boolean showFPS = false;
|
||||
|
||||
/**
|
||||
* Whether or not to show hit lighting effects.
|
||||
*/
|
||||
private static boolean showHitLighting = true;
|
||||
|
||||
/**
|
||||
* Whether or not to show combo burst images.
|
||||
*/
|
||||
private static boolean showComboBursts = true;
|
||||
|
||||
/**
|
||||
* Default music volume.
|
||||
*/
|
||||
private static int musicVolume = 20;
|
||||
|
||||
/**
|
||||
* Offset time, in milliseconds, for music position-related elements.
|
||||
*/
|
||||
private static int musicOffset = -150;
|
||||
|
||||
/**
|
||||
* Screenshot file format.
|
||||
*/
|
||||
private static String[] screenshotFormat = { "png", "jpg", "bmp" };
|
||||
|
||||
/**
|
||||
* Index in screenshotFormat[] array.
|
||||
*/
|
||||
private static int screenshotFormatIndex = 0;
|
||||
|
||||
/**
|
||||
* Back button (shared by other states).
|
||||
*/
|
||||
private static GUIMenuButton backButton;
|
||||
|
||||
/**
|
||||
* Game option coordinate modifiers (for drawing).
|
||||
*/
|
||||
private int textY, offsetY;
|
||||
|
||||
// game-related variables
|
||||
private static GameContainer container;
|
||||
private static StateBasedGame game;
|
||||
private Input input;
|
||||
private int state;
|
||||
private boolean init = false;
|
||||
|
||||
public Options(int state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
Options.container = container;
|
||||
Options.game = game;
|
||||
this.input = container.getInput();
|
||||
|
||||
// game settings;
|
||||
container.setTargetFrameRate(60);
|
||||
container.setMouseCursor("cursor.png", 16, 16);
|
||||
container.setMusicVolume(getMusicVolume());
|
||||
container.setShowFPS(false);
|
||||
container.getInput().enableKeyRepeat();
|
||||
container.setAlwaysRender(true);
|
||||
|
||||
// create fonts
|
||||
float fontBase;
|
||||
if (container.getHeight() <= 600)
|
||||
fontBase = 9f;
|
||||
else if (container.getHeight() < 800)
|
||||
fontBase = 10f;
|
||||
else if (container.getHeight() <= 900)
|
||||
fontBase = 12f;
|
||||
else
|
||||
fontBase = 14f;
|
||||
|
||||
Font font = new Font("Lucida Sans Unicode", Font.PLAIN, (int) (fontBase * 4 / 3));
|
||||
FONT_DEFAULT = new TrueTypeFont(font, false);
|
||||
FONT_BOLD = new TrueTypeFont(font.deriveFont(Font.BOLD), false);
|
||||
FONT_XLARGE = new TrueTypeFont(font.deriveFont(fontBase * 4), false);
|
||||
FONT_LARGE = new TrueTypeFont(font.deriveFont(fontBase * 2), false);
|
||||
FONT_MEDIUM = new TrueTypeFont(font.deriveFont(fontBase * 3 / 2), false);
|
||||
FONT_SMALL = new TrueTypeFont(font.deriveFont(fontBase), false);
|
||||
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
|
||||
// game option coordinate modifiers
|
||||
textY = 10 + (FONT_XLARGE.getLineHeight() * 3 / 2);
|
||||
offsetY = (int) (((height * 0.8f) - textY) / OPTIONS_MAX);
|
||||
|
||||
// game mods
|
||||
modsActive = new boolean[MOD_MAX];
|
||||
modButtons = new GUIMenuButton[MOD_MAX];
|
||||
Image noFailImage = new Image("selection-mod-nofail.png");
|
||||
float modScale = (height * 0.12f) / noFailImage.getHeight();
|
||||
noFailImage = noFailImage.getScaledCopy(modScale);
|
||||
float modButtonOffsetX = noFailImage.getWidth() * 1.5f;
|
||||
float modButtonX = (width / 2f) - (modButtonOffsetX * modButtons.length / 2.75f);
|
||||
float modButtonY = (height * 0.8f) + (noFailImage.getHeight() / 2);
|
||||
modButtons[MOD_NO_FAIL] = new GUIMenuButton(
|
||||
noFailImage, modButtonX, modButtonY
|
||||
);
|
||||
modButtons[MOD_HARD_ROCK] = new GUIMenuButton(
|
||||
new Image("selection-mod-hardrock.png").getScaledCopy(modScale),
|
||||
modButtonX + modButtonOffsetX, modButtonY
|
||||
);
|
||||
modButtons[MOD_SUDDEN_DEATH] = new GUIMenuButton(
|
||||
new Image("selection-mod-suddendeath.png").getScaledCopy(modScale),
|
||||
modButtonX + (modButtonOffsetX * 2), modButtonY
|
||||
);
|
||||
modButtons[MOD_SPUN_OUT] = new GUIMenuButton(
|
||||
new Image("selection-mod-spunout.png").getScaledCopy(modScale),
|
||||
modButtonX + (modButtonOffsetX * 3), modButtonY
|
||||
);
|
||||
modButtons[MOD_AUTO] = new GUIMenuButton(
|
||||
new Image("selection-mod-autoplay.png").getScaledCopy(modScale),
|
||||
modButtonX + (modButtonOffsetX * 4), modButtonY
|
||||
);
|
||||
for (int i = 0; i < modButtons.length; i++)
|
||||
modButtons[i].getImage().setAlpha(0.5f);
|
||||
|
||||
// back button
|
||||
Image back = new Image("menu-back.png");
|
||||
float scale = (height * 0.1f) / back.getHeight();
|
||||
back = back.getScaledCopy(scale);
|
||||
backButton = new GUIMenuButton(back,
|
||||
back.getWidth() / 2f,
|
||||
height - (back.getHeight() / 2f));
|
||||
|
||||
game.enterState(Opsu.STATE_MAINMENU, new EmptyTransition(), new FadeInTransition(Color.black));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(GameContainer container, StateBasedGame game, Graphics g)
|
||||
throws SlickException {
|
||||
if (!init)
|
||||
return;
|
||||
|
||||
g.setBackground(COLOR_BLACK_ALPHA);
|
||||
g.setColor(Color.white);
|
||||
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
|
||||
// title
|
||||
FONT_XLARGE.drawString(
|
||||
(width / 2) - (FONT_XLARGE.getWidth("GAME OPTIONS") / 2),
|
||||
10, "GAME OPTIONS"
|
||||
);
|
||||
FONT_DEFAULT.drawString(
|
||||
(width / 2) - (FONT_DEFAULT.getWidth("Click or drag an option to change it.") / 2),
|
||||
10 + FONT_XLARGE.getHeight(), "Click or drag an option to change it."
|
||||
);
|
||||
|
||||
// game options
|
||||
g.setLineWidth(1f);
|
||||
g.setFont(FONT_LARGE);
|
||||
this.drawOption(g, OPTIONS_SCREEN_RESOLUTION, "Screen Resolution",
|
||||
String.format("%dx%d", resolutions[resolutionIndex][0], resolutions[resolutionIndex][1]),
|
||||
"Restart to apply resolution changes."
|
||||
);
|
||||
// this.drawOption(g, OPTIONS_FULLSCREEN, "Fullscreen Mode",
|
||||
// fullscreen ? "Yes" : "No",
|
||||
// "Restart to apply changes."
|
||||
// );
|
||||
this.drawOption(g, OPTIONS_TARGET_FPS, "Frame Limiter",
|
||||
String.format("%dfps", targetFPS[targetFPSindex]),
|
||||
"Higher values may cause high CPU usage."
|
||||
);
|
||||
this.drawOption(g, OPTIONS_MUSIC_VOLUME, "Music Volume",
|
||||
String.format("%d%%", musicVolume),
|
||||
"Global music volume."
|
||||
);
|
||||
this.drawOption(g, OPTIONS_MUSIC_OFFSET, "Music Offset",
|
||||
String.format("%dms", musicOffset),
|
||||
"Adjust this value if hit objects are out of sync."
|
||||
);
|
||||
this.drawOption(g, OPTIONS_SCREENSHOT_FORMAT, "Screenshot Format",
|
||||
screenshotFormat[screenshotFormatIndex].toUpperCase(),
|
||||
"Press F12 to take a screenshot."
|
||||
);
|
||||
this.drawOption(g, OPTIONS_DISPLAY_FPS, "Show FPS Counter",
|
||||
showFPS ? "Yes" : "No",
|
||||
null
|
||||
);
|
||||
this.drawOption(g, OPTIONS_HIT_LIGHTING, "Show Hit Lighting",
|
||||
showHitLighting ? "Yes" : "No",
|
||||
null
|
||||
);
|
||||
this.drawOption(g, OPTIONS_COMBO_BURSTS, "Show Combo Bursts",
|
||||
showComboBursts ? "Yes" : "No",
|
||||
null
|
||||
);
|
||||
|
||||
// game mods
|
||||
FONT_LARGE.drawString(width * 0.02f, height * 0.8f, "Game Mods:", Color.white);
|
||||
for (int i = 0; i < modButtons.length; i++)
|
||||
modButtons[i].draw();
|
||||
|
||||
backButton.draw();
|
||||
|
||||
drawFPS();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(GameContainer container, StateBasedGame game, int delta)
|
||||
throws SlickException {
|
||||
init = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getID() { return state; }
|
||||
|
||||
@Override
|
||||
public void mousePressed(int button, int x, int y) {
|
||||
// check mouse button
|
||||
if (button == Input.MOUSE_MIDDLE_BUTTON)
|
||||
return;
|
||||
|
||||
// back
|
||||
if (backButton.contains(x, y)) {
|
||||
game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black));
|
||||
return;
|
||||
}
|
||||
|
||||
// game mods
|
||||
for (int i = 0; i < modButtons.length; i++) {
|
||||
if (modButtons[i].contains(x, y)) {
|
||||
toggleMod(i);
|
||||
|
||||
// mutually exclusive mods
|
||||
if (modsActive[MOD_AUTO]) {
|
||||
if (i == MOD_AUTO) {
|
||||
if (modsActive[MOD_SPUN_OUT])
|
||||
toggleMod(MOD_SPUN_OUT);
|
||||
if (modsActive[MOD_SUDDEN_DEATH])
|
||||
toggleMod(MOD_SUDDEN_DEATH);
|
||||
} else if (i == MOD_SPUN_OUT || i == MOD_SUDDEN_DEATH) {
|
||||
if (modsActive[i])
|
||||
toggleMod(i);
|
||||
}
|
||||
} else if (modsActive[MOD_SUDDEN_DEATH] && modsActive[MOD_NO_FAIL])
|
||||
toggleMod((i == MOD_SUDDEN_DEATH) ? MOD_NO_FAIL : MOD_SUDDEN_DEATH);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// options (click only)
|
||||
if (isOptionClicked(OPTIONS_SCREEN_RESOLUTION, y)) {
|
||||
resolutionIndex = (resolutionIndex + 1) % resolutions.length;
|
||||
return;
|
||||
}
|
||||
// if (isOptionClicked(OPTIONS_FULLSCREEN, y)) {
|
||||
// fullscreen = !fullscreen;
|
||||
// return;
|
||||
// }
|
||||
if (isOptionClicked(OPTIONS_TARGET_FPS, y)) {
|
||||
targetFPSindex = (targetFPSindex + 1) % targetFPS.length;
|
||||
container.setTargetFrameRate(targetFPS[targetFPSindex]);
|
||||
return;
|
||||
}
|
||||
if (isOptionClicked(OPTIONS_SCREENSHOT_FORMAT, y)) {
|
||||
screenshotFormatIndex = (screenshotFormatIndex + 1) % screenshotFormat.length;
|
||||
return;
|
||||
}
|
||||
if (isOptionClicked(OPTIONS_DISPLAY_FPS, y)) {
|
||||
showFPS = !showFPS;
|
||||
return;
|
||||
}
|
||||
if (isOptionClicked(OPTIONS_HIT_LIGHTING, y)) {
|
||||
showHitLighting = !showHitLighting;
|
||||
return;
|
||||
}
|
||||
if (isOptionClicked(OPTIONS_COMBO_BURSTS, y)) {
|
||||
showComboBursts = !showComboBursts;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(int oldx, int oldy, int newx, int newy) {
|
||||
// check mouse button (right click scrolls faster)
|
||||
int multiplier;
|
||||
if (input.isMouseButtonDown(Input.MOUSE_RIGHT_BUTTON))
|
||||
multiplier = 4;
|
||||
else if (input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON))
|
||||
multiplier = 1;
|
||||
else
|
||||
return;
|
||||
|
||||
// get direction
|
||||
int diff = newx - oldx;
|
||||
if (diff == 0)
|
||||
return;
|
||||
diff = ((diff > 0) ? 1 : -1) * multiplier;
|
||||
|
||||
// options (drag only)
|
||||
if (isOptionClicked(OPTIONS_MUSIC_VOLUME, oldy)) {
|
||||
musicVolume += diff;
|
||||
if (musicVolume < 0)
|
||||
musicVolume = 0;
|
||||
else if (musicVolume > 100)
|
||||
musicVolume = 100;
|
||||
container.setMusicVolume(getMusicVolume());
|
||||
return;
|
||||
}
|
||||
if (isOptionClicked(OPTIONS_MUSIC_OFFSET, oldy)) {
|
||||
musicOffset += diff;
|
||||
if (musicOffset < -500)
|
||||
musicOffset = -500;
|
||||
else if (musicOffset > 500)
|
||||
musicOffset = 500;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyPressed(int key, char c) {
|
||||
switch (key) {
|
||||
case Input.KEY_ESCAPE:
|
||||
game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black));
|
||||
break;
|
||||
case Input.KEY_F12:
|
||||
Options.takeScreenShot();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a game option.
|
||||
* @param g the graphics context
|
||||
* @param pos the element position (OPTIONS_* constants)
|
||||
* @param label the option name
|
||||
* @param value the option value
|
||||
* @param notes additional notes (optional)
|
||||
*/
|
||||
private void drawOption(Graphics g, int pos, String label, String value, String notes) {
|
||||
int width = container.getWidth();
|
||||
int textHeight = FONT_LARGE.getHeight();
|
||||
float y = textY + (pos * offsetY);
|
||||
|
||||
g.drawString(label, width / 50, y);
|
||||
g.drawString(value, width / 2, y);
|
||||
g.drawLine(0, y + textHeight, width, y + textHeight);
|
||||
if (notes != null)
|
||||
FONT_SMALL.drawString(width / 50, y + textHeight, notes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not an option was clicked.
|
||||
* @param pos the element position (OPTIONS_* constants)
|
||||
* @param y the y coordinate of the click
|
||||
* @return true if clicked
|
||||
*/
|
||||
private boolean isOptionClicked(int pos, int y) {
|
||||
if (y > textY + (offsetY * pos) - FONT_LARGE.getHeight() &&
|
||||
y < textY + (offsetY * pos) + FONT_LARGE.getHeight()) {
|
||||
optionsChanged = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the active status of a game mod.
|
||||
* Note that this does not perform checks for mutual exclusivity.
|
||||
* @param mod the game mod (MOD_* constants)
|
||||
*/
|
||||
private static void toggleMod(int mod) {
|
||||
modButtons[mod].getImage().setAlpha(modsActive[mod] ? 0.5f : 1.0f);
|
||||
modsActive[mod] = !modsActive[mod];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not a game mod is active.
|
||||
* @param mod the game mod (MOD_* constants)
|
||||
* @return true if the mod is active
|
||||
*/
|
||||
public static boolean isModActive(int mod) { return modsActive[mod]; }
|
||||
|
||||
/**
|
||||
* Returns the image associated with a game mod.
|
||||
* @param mod the game mod (MOD_* constants)
|
||||
* @return the associated image
|
||||
*/
|
||||
public static Image getModImage(int mod) { return modButtons[mod].getImage(); }
|
||||
|
||||
/**
|
||||
* Returns the 'back' GUIMenuButton.
|
||||
*/
|
||||
public static GUIMenuButton getBackButton() { return backButton; }
|
||||
|
||||
/**
|
||||
* Returns the default music volume.
|
||||
* @return the volume [0, 1]
|
||||
*/
|
||||
public static float getMusicVolume() { return musicVolume / 100f; }
|
||||
|
||||
/**
|
||||
* Returns the music offset time.
|
||||
* @return the offset (in milliseconds)
|
||||
*/
|
||||
public static int getMusicOffset() { return musicOffset; }
|
||||
|
||||
/**
|
||||
* Draws the FPS at the bottom-right corner of the game container.
|
||||
* If the option is not activated, this will do nothing.
|
||||
*/
|
||||
public static void drawFPS() {
|
||||
if (showFPS) {
|
||||
String fps = String.format("FPS: %d", container.getFPS());
|
||||
FONT_DEFAULT.drawString(
|
||||
container.getWidth() - 15 - FONT_DEFAULT.getWidth(fps),
|
||||
container.getHeight() - 15 - FONT_DEFAULT.getHeight(fps),
|
||||
fps, Color.white
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a screenshot.
|
||||
* @return true if successful
|
||||
*/
|
||||
public static boolean takeScreenShot() {
|
||||
// TODO: should this be threaded?
|
||||
try {
|
||||
// create the screenshot directory
|
||||
if (!SCREENSHOT_DIR.isDirectory()) {
|
||||
if (!SCREENSHOT_DIR.mkdir())
|
||||
return false;
|
||||
}
|
||||
|
||||
// create file name
|
||||
SimpleDateFormat date = new SimpleDateFormat("yyyyMMdd_HHmmss");
|
||||
String file = date.format(new Date());
|
||||
|
||||
// copy the screen
|
||||
Image screen = new Image(container.getWidth(), container.getHeight());
|
||||
container.getGraphics().copyArea(screen, 0, 0);
|
||||
ImageOut.write(screen, String.format("%s%sscreenshot_%s.%s",
|
||||
SCREENSHOT_DIR.getName(), File.separator,
|
||||
file, screenshotFormat[screenshotFormatIndex]), false
|
||||
);
|
||||
screen.destroy();
|
||||
} catch (SlickException e) {
|
||||
Log.warn("Failed to take a screenshot.", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the screen resolution.
|
||||
* @return an array containing the resolution [width, height]
|
||||
*/
|
||||
public static int[] getContainerSize() { return resolutions[resolutionIndex]; }
|
||||
|
||||
// /**
|
||||
// * Returns whether or not fullscreen mode is enabled.
|
||||
// * @return true if enabled
|
||||
// */
|
||||
// public static boolean isFullscreen() { return fullscreen; }
|
||||
|
||||
/**
|
||||
* Returns whether or not hit lighting effects are enabled.
|
||||
* @return true if enabled
|
||||
*/
|
||||
public static boolean isHitLightingEnabled() { return showHitLighting; }
|
||||
|
||||
/**
|
||||
* Returns whether or not combo burst effects are enabled.
|
||||
* @return true if enabled
|
||||
*/
|
||||
public static boolean isComboBurstEnabled() { return showComboBursts; }
|
||||
|
||||
/**
|
||||
* Returns the current beatmap directory.
|
||||
* If invalid, this will attempt to search for the directory,
|
||||
* and if nothing found, will create one.
|
||||
*/
|
||||
public static File getBeatmapDir() {
|
||||
if (beatmapDir != null && beatmapDir.isDirectory())
|
||||
return beatmapDir;
|
||||
|
||||
// search for directory
|
||||
for (int i = 0; i < BEATMAP_DIRS.length; i++) {
|
||||
beatmapDir = new File(BEATMAP_DIRS[i]);
|
||||
if (beatmapDir.isDirectory()) {
|
||||
optionsChanged = true; // force config file creation
|
||||
return beatmapDir;
|
||||
}
|
||||
}
|
||||
beatmapDir.mkdir(); // none found, create new directory
|
||||
return beatmapDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads user options from the options file, if it exists.
|
||||
*/
|
||||
public static void parseOptions() {
|
||||
// if no config file, use default settings
|
||||
File file = new File(OPTIONS_FILE);
|
||||
if (!file.isFile()) {
|
||||
optionsChanged = true; // force file creation
|
||||
saveOptions();
|
||||
optionsChanged = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try (BufferedReader in = new BufferedReader(new FileReader(file))) {
|
||||
String line;
|
||||
String name, value;
|
||||
int i;
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.length() < 2 || line.charAt(0) == '#')
|
||||
continue;
|
||||
int index = line.indexOf('=');
|
||||
if (index == -1)
|
||||
continue;
|
||||
name = line.substring(0, index).trim();
|
||||
value = line.substring(index + 1).trim();
|
||||
switch (name) {
|
||||
case "BeatmapDirectory":
|
||||
beatmapDir = new File(value);
|
||||
break;
|
||||
case "ScreenResolution":
|
||||
i = Integer.parseInt(value);
|
||||
if (i >= 0 && i < resolutions.length)
|
||||
resolutionIndex = i;
|
||||
break;
|
||||
// case "Fullscreen":
|
||||
// fullscreen = Boolean.parseBoolean(value);
|
||||
// break;
|
||||
case "FrameSync":
|
||||
i = Integer.parseInt(value);
|
||||
if (i >= 0 && i <= targetFPS.length)
|
||||
targetFPSindex = i;
|
||||
break;
|
||||
case "VolumeMusic":
|
||||
i = Integer.parseInt(value);
|
||||
if (i >= 0 && i <= 100)
|
||||
musicVolume = i;
|
||||
break;
|
||||
case "Offset":
|
||||
i = Integer.parseInt(value);
|
||||
if (i >= -500 && i <= 500)
|
||||
musicOffset = i;
|
||||
break;
|
||||
case "ScreenshotFormat":
|
||||
i = Integer.parseInt(value);
|
||||
if (i >= 0 && i < screenshotFormat.length)
|
||||
screenshotFormatIndex = i;
|
||||
break;
|
||||
case "FpsCounter":
|
||||
showFPS = Boolean.parseBoolean(value);
|
||||
break;
|
||||
case "HitLighting":
|
||||
showHitLighting = Boolean.parseBoolean(value);
|
||||
break;
|
||||
case "ComboBurst":
|
||||
showComboBursts = Boolean.parseBoolean(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.error(String.format("Failed to read file '%s'.", OPTIONS_FILE), e);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.warn("Format error in options file.", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (Over)writes user options to a file.
|
||||
*/
|
||||
public static void saveOptions() {
|
||||
// only overwrite when needed
|
||||
if (!optionsChanged)
|
||||
return;
|
||||
|
||||
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
|
||||
new FileOutputStream(OPTIONS_FILE), "utf-8"))) {
|
||||
// header
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("EEEE, MMMM dd, yyyy");
|
||||
String date = dateFormat.format(new Date());
|
||||
writer.write("# opsu! configuration");
|
||||
writer.newLine();
|
||||
writer.write("# last updated on ");
|
||||
writer.write(date);
|
||||
writer.newLine();
|
||||
writer.newLine();
|
||||
|
||||
// options
|
||||
if (beatmapDir != null) {
|
||||
writer.write(String.format("BeatmapDirectory = %s", beatmapDir.getAbsolutePath()));
|
||||
writer.newLine();
|
||||
}
|
||||
writer.write(String.format("ScreenResolution = %d", resolutionIndex));
|
||||
writer.newLine();
|
||||
// writer.write(String.format("Fullscreen = %b", fullscreen));
|
||||
// writer.newLine();
|
||||
writer.write(String.format("FrameSync = %d", targetFPSindex));
|
||||
writer.newLine();
|
||||
writer.write(String.format("VolumeMusic = %d", musicVolume));
|
||||
writer.newLine();
|
||||
writer.write(String.format("Offset = %d", musicOffset));
|
||||
writer.newLine();
|
||||
writer.write(String.format("ScreenshotFormat = %d", screenshotFormatIndex));
|
||||
writer.newLine();
|
||||
writer.write(String.format("FpsCounter = %b", showFPS));
|
||||
writer.newLine();
|
||||
writer.write(String.format("HitLighting = %b", showHitLighting));
|
||||
writer.newLine();
|
||||
writer.write(String.format("ComboBurst = %b", showComboBursts));
|
||||
writer.newLine();
|
||||
writer.close();
|
||||
} catch (IOException e) {
|
||||
Log.error(String.format("Failed to write to file '%s'.", OPTIONS_FILE), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
601
src/itdelatrisu/opsu/states/SongMenu.java
Normal file
601
src/itdelatrisu/opsu/states/SongMenu.java
Normal file
@@ -0,0 +1,601 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.states;
|
||||
|
||||
import itdelatrisu.opsu.GUIMenuButton;
|
||||
import itdelatrisu.opsu.MusicController;
|
||||
import itdelatrisu.opsu.Opsu;
|
||||
import itdelatrisu.opsu.OsuFile;
|
||||
import itdelatrisu.opsu.OsuGroupList;
|
||||
import itdelatrisu.opsu.OsuGroupNode;
|
||||
import itdelatrisu.opsu.OsuParser;
|
||||
|
||||
import org.lwjgl.opengl.Display;
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
import org.newdawn.slick.Graphics;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.Input;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.gui.TextField;
|
||||
import org.newdawn.slick.state.BasicGameState;
|
||||
import org.newdawn.slick.state.StateBasedGame;
|
||||
import org.newdawn.slick.state.transition.EmptyTransition;
|
||||
import org.newdawn.slick.state.transition.FadeInTransition;
|
||||
import org.newdawn.slick.state.transition.FadeOutTransition;
|
||||
|
||||
/**
|
||||
* "Song Selection" state.
|
||||
* <ul>
|
||||
* <li>[Song] - start game (move to game state)
|
||||
* <li>[Back] - return to main menu
|
||||
* </ul>
|
||||
*/
|
||||
public class SongMenu extends BasicGameState {
|
||||
/**
|
||||
* The number of buttons to be shown on each screen.
|
||||
*/
|
||||
private static final int MAX_BUTTONS = 6;
|
||||
|
||||
/**
|
||||
* Delay time, in milliseconds, between each search.
|
||||
*/
|
||||
private static final int SEARCH_DELAY = 300;
|
||||
|
||||
/**
|
||||
* Current start node (topmost menu entry).
|
||||
*/
|
||||
private OsuGroupNode startNode;
|
||||
|
||||
/**
|
||||
* Current focused (selected) node.
|
||||
*/
|
||||
private OsuGroupNode focusNode;
|
||||
|
||||
/**
|
||||
* Button coordinate values.
|
||||
*/
|
||||
private float
|
||||
buttonX, buttonY, buttonOffset,
|
||||
buttonWidth, buttonHeight;
|
||||
|
||||
/**
|
||||
* Sorting tab buttons (indexed by SORT_* constants).
|
||||
*/
|
||||
private GUIMenuButton[] sortTabs;
|
||||
|
||||
/**
|
||||
* The current sort order (SORT_* constant).
|
||||
*/
|
||||
private byte currentSort;
|
||||
|
||||
/**
|
||||
* The options button (to enter the "Game Options" menu).
|
||||
*/
|
||||
private GUIMenuButton optionsButton;
|
||||
|
||||
/**
|
||||
* The search textfield.
|
||||
*/
|
||||
private TextField search;
|
||||
|
||||
/**
|
||||
* Delay timer, in milliseconds, before running another search.
|
||||
* This is overridden by character entry (reset) and 'esc' (immediate search).
|
||||
*/
|
||||
private int searchTimer;
|
||||
|
||||
/**
|
||||
* Information text to display based on the search query.
|
||||
*/
|
||||
private String searchResultString;
|
||||
|
||||
/**
|
||||
* Search icon.
|
||||
*/
|
||||
private Image searchIcon;
|
||||
|
||||
/**
|
||||
* Music note icon.
|
||||
*/
|
||||
private Image musicNote;
|
||||
|
||||
// game-related variables
|
||||
private GameContainer container;
|
||||
private StateBasedGame game;
|
||||
private Input input;
|
||||
private int state;
|
||||
|
||||
public SongMenu(int state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
this.container = container;
|
||||
this.game = game;
|
||||
this.input = container.getInput();
|
||||
|
||||
// initialize song list
|
||||
currentSort = OsuGroupList.SORT_TITLE;
|
||||
Opsu.groups.init(currentSort);
|
||||
setFocus(Opsu.groups.getRandomNode(), -1, true);
|
||||
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
|
||||
// song button background & graphics context
|
||||
Image menuBackground = new Image("menu-button-background.png").getScaledCopy(width / 2, height / 6);
|
||||
OsuGroupNode.setBackground(menuBackground);
|
||||
|
||||
// song button coordinates
|
||||
buttonX = width * 0.6f;
|
||||
buttonY = height * 0.16f;
|
||||
buttonWidth = menuBackground.getWidth();
|
||||
buttonHeight = menuBackground.getHeight();
|
||||
buttonOffset = (height * 0.8f) / MAX_BUTTONS;
|
||||
|
||||
// sorting tabs
|
||||
sortTabs = new GUIMenuButton[OsuGroupList.SORT_MAX];
|
||||
Image tab = new Image("selection-tab.png");
|
||||
float tabScale = (height * 0.033f) / tab.getHeight();
|
||||
tab = tab.getScaledCopy(tabScale);
|
||||
|
||||
float tabX = buttonX + (tab.getWidth() / 2f);
|
||||
float tabY = (height * 0.15f) - (tab.getHeight() / 2f) - 2f;
|
||||
float tabOffset = (width - buttonX) / sortTabs.length;
|
||||
for (int i = 0; i < sortTabs.length; i++)
|
||||
sortTabs[i] = new GUIMenuButton(tab, tabX + (i * tabOffset), tabY);
|
||||
|
||||
// search
|
||||
searchTimer = 0;
|
||||
searchResultString = "Type to search!";
|
||||
|
||||
searchIcon = new Image("search.png");
|
||||
float iconScale = Options.FONT_BOLD.getLineHeight() * 2f / searchIcon.getHeight();
|
||||
searchIcon = searchIcon.getScaledCopy(iconScale);
|
||||
|
||||
search = new TextField(
|
||||
container, Options.FONT_DEFAULT,
|
||||
(int) tabX + searchIcon.getWidth(), (int) ((height * 0.15f) - (tab.getHeight() * 5 / 2f)),
|
||||
(int) (buttonWidth / 2), Options.FONT_DEFAULT.getHeight()
|
||||
);
|
||||
search.setBackgroundColor(Color.transparent);
|
||||
search.setBorderColor(Color.transparent);
|
||||
search.setTextColor(Color.white);
|
||||
search.setConsumeEvents(false);
|
||||
search.setMaxLength(60);
|
||||
|
||||
// options button
|
||||
Image optionsIcon = new Image("options.png").getScaledCopy(iconScale);
|
||||
optionsButton = new GUIMenuButton(optionsIcon, search.getX() - (optionsIcon.getWidth() * 1.5f), search.getY());
|
||||
|
||||
musicNote = new Image("music-note.png");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(GameContainer container, StateBasedGame game, Graphics g)
|
||||
throws SlickException {
|
||||
g.setBackground(Color.black);
|
||||
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
|
||||
// background
|
||||
if (focusNode != null)
|
||||
focusNode.osuFiles.get(focusNode.osuFileIndex).drawBG(width, height, 1.0f);
|
||||
|
||||
// header setup
|
||||
float lowerBound = height * 0.15f;
|
||||
g.setColor(Options.COLOR_BLACK_ALPHA);
|
||||
g.fillRect(0, 0, width, lowerBound);
|
||||
g.setColor(Options.COLOR_BLUE_DIVIDER);
|
||||
g.setLineWidth(2f);
|
||||
g.drawLine(0, lowerBound, width, lowerBound);
|
||||
g.resetLineWidth();
|
||||
|
||||
// header
|
||||
if (focusNode != null) {
|
||||
musicNote.draw();
|
||||
int musicNoteWidth = musicNote.getWidth();
|
||||
int musicNoteHeight = musicNote.getHeight();
|
||||
|
||||
String[] info = focusNode.getInfo();
|
||||
g.setColor(Color.white);
|
||||
Options.FONT_LARGE.drawString(
|
||||
musicNoteWidth + 5, -3, info[0]);
|
||||
float y1 = -3 + Options.FONT_LARGE.getHeight() * 0.75f;
|
||||
Options.FONT_DEFAULT.drawString(
|
||||
musicNoteWidth + 5, y1, info[1]);
|
||||
Options.FONT_BOLD.drawString(
|
||||
5, Math.max(y1 + 4, musicNoteHeight - 3), info[2]);
|
||||
Options.FONT_DEFAULT.drawString(
|
||||
5, musicNoteHeight + Options.FONT_BOLD.getLineHeight() - 9, info[3]);
|
||||
Options.FONT_SMALL.drawString(
|
||||
5, musicNoteHeight + Options.FONT_BOLD.getLineHeight() + Options.FONT_DEFAULT.getLineHeight() - 13, info[4]);
|
||||
}
|
||||
|
||||
// song buttons
|
||||
OsuGroupNode node = startNode;
|
||||
for (int i = 0; i < MAX_BUTTONS && node != null; i++) {
|
||||
node.draw(buttonX, buttonY + (i*buttonOffset), (node == focusNode));
|
||||
node = node.next;
|
||||
}
|
||||
|
||||
// options button
|
||||
optionsButton.draw();
|
||||
|
||||
// sorting tabs
|
||||
float tabTextY = sortTabs[0].getY() - (sortTabs[0].getImage().getHeight() / 2f);
|
||||
for (int i = sortTabs.length - 1; i >= 0; i--) {
|
||||
sortTabs[i].getImage().setAlpha((i == currentSort) ? 1.0f : 0.7f);
|
||||
sortTabs[i].draw();
|
||||
float tabTextX = sortTabs[i].getX() - (Options.FONT_MEDIUM.getWidth(OsuGroupList.SORT_NAMES[i]) / 2);
|
||||
Options.FONT_MEDIUM.drawString(tabTextX, tabTextY, OsuGroupList.SORT_NAMES[i], Color.white);
|
||||
}
|
||||
|
||||
// search
|
||||
Options.FONT_BOLD.drawString(
|
||||
search.getX(), search.getY() - Options.FONT_BOLD.getLineHeight(),
|
||||
searchResultString, Color.white
|
||||
);
|
||||
searchIcon.draw(search.getX() - searchIcon.getWidth(),
|
||||
search.getY() - Options.FONT_DEFAULT.getLineHeight());
|
||||
g.setColor(Color.white);
|
||||
search.render(container, g);
|
||||
|
||||
// scroll bar
|
||||
if (focusNode != null) {
|
||||
float scrollStartY = height * 0.16f;
|
||||
float scrollEndY = height * 0.82f;
|
||||
g.setColor(Options.COLOR_BLACK_ALPHA);
|
||||
g.fillRoundRect(width - 10, scrollStartY, 5, scrollEndY, 4);
|
||||
g.setColor(Color.white);
|
||||
g.fillRoundRect(width - 10, scrollStartY + (scrollEndY * startNode.index / Opsu.groups.size()), 5, 20, 4);
|
||||
}
|
||||
|
||||
// back button
|
||||
Options.getBackButton().draw();
|
||||
|
||||
Options.drawFPS();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(GameContainer container, StateBasedGame game, int delta)
|
||||
throws SlickException {
|
||||
// search
|
||||
search.setFocus(true);
|
||||
searchTimer += delta;
|
||||
if (searchTimer >= SEARCH_DELAY) {
|
||||
searchTimer = 0;
|
||||
|
||||
// store the start/focus nodes
|
||||
OsuGroupNode oldFocusNode = null;
|
||||
int oldFileIndex = -1;
|
||||
if (focusNode != null) {
|
||||
oldFocusNode = Opsu.groups.getBaseNode(focusNode.index);
|
||||
oldFileIndex = focusNode.osuFileIndex;
|
||||
}
|
||||
|
||||
if (Opsu.groups.search(search.getText())) {
|
||||
// empty search
|
||||
if (search.getText().isEmpty())
|
||||
searchResultString = "Type to search!";
|
||||
|
||||
// search produced new list: re-initialize it
|
||||
startNode = focusNode = null;
|
||||
if (Opsu.groups.size() > 0) {
|
||||
Opsu.groups.init(currentSort);
|
||||
if (search.getText().isEmpty()) { // cleared search
|
||||
// use previous start/focus if possible
|
||||
if (oldFocusNode != null)
|
||||
setFocus(oldFocusNode, oldFileIndex + 1, true);
|
||||
else
|
||||
setFocus(Opsu.groups.getRandomNode(), -1, true);
|
||||
} else {
|
||||
searchResultString = String.format("%d matches found!", Opsu.groups.size());
|
||||
setFocus(Opsu.groups.getRandomNode(), -1, true);
|
||||
}
|
||||
} else if (!search.getText().isEmpty())
|
||||
searchResultString = "No matches found. Hit 'esc' to reset.";
|
||||
}
|
||||
}
|
||||
|
||||
// slide buttons
|
||||
int height = container.getHeight();
|
||||
float targetY = height * 0.16f;
|
||||
if (buttonY > targetY) {
|
||||
buttonY -= height * delta / 20000f;
|
||||
if (buttonY < targetY)
|
||||
buttonY = targetY;
|
||||
} else if (buttonY < targetY) {
|
||||
buttonY += height * delta / 20000f;
|
||||
if (buttonY > targetY)
|
||||
buttonY = targetY;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getID() { return state; }
|
||||
|
||||
@Override
|
||||
public void mousePressed(int button, int x, int y) {
|
||||
// check mouse button
|
||||
if (button != Input.MOUSE_LEFT_BUTTON)
|
||||
return;
|
||||
|
||||
// back
|
||||
if (Options.getBackButton().contains(x, y)) {
|
||||
game.enterState(Opsu.STATE_MAINMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
return;
|
||||
}
|
||||
|
||||
// options
|
||||
if (optionsButton.contains(x, y)) {
|
||||
game.enterState(Opsu.STATE_OPTIONS, new EmptyTransition(), new FadeInTransition(Color.black));
|
||||
return;
|
||||
}
|
||||
|
||||
if (focusNode == null)
|
||||
return;
|
||||
|
||||
// sorting buttons
|
||||
for (byte i = 0; i < sortTabs.length; i++) {
|
||||
if (sortTabs[i].contains(x, y) && i != currentSort) {
|
||||
currentSort = i;
|
||||
OsuGroupNode oldFocusBase = Opsu.groups.getBaseNode(focusNode.index);
|
||||
int oldFocusFileIndex = focusNode.osuFileIndex;
|
||||
focusNode = null;
|
||||
Opsu.groups.init(i);
|
||||
setFocus(oldFocusBase, oldFocusFileIndex + 1, true);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < MAX_BUTTONS; i++) {
|
||||
if ((x > buttonX && x < buttonX + buttonWidth) &&
|
||||
(y > buttonY + (i*buttonOffset) && y < buttonY + (i*buttonOffset) + buttonHeight)) {
|
||||
OsuGroupNode node = Opsu.groups.getNode(startNode, i);
|
||||
if (node == null) // out of bounds
|
||||
break;
|
||||
|
||||
int expandedIndex = Opsu.groups.getExpandedIndex();
|
||||
|
||||
// clicked node is already expanded
|
||||
if (node.index == expandedIndex) {
|
||||
if (node.osuFileIndex == -1) {
|
||||
// check bounds
|
||||
int max = Math.max(Opsu.groups.size() - MAX_BUTTONS, 0);
|
||||
if (startNode.index > max)
|
||||
startNode = Opsu.groups.getBaseNode(max);
|
||||
|
||||
// if group button clicked, undo expansion
|
||||
Opsu.groups.expand(node.index);
|
||||
|
||||
} else if (node.osuFileIndex == focusNode.osuFileIndex) {
|
||||
// if already focused, load the beatmap
|
||||
startGame(focusNode.osuFiles.get(focusNode.osuFileIndex));
|
||||
|
||||
} else {
|
||||
// focus the node
|
||||
setFocus(node, 0, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// if current start node is expanded,
|
||||
// set it to the base node before undoing the expansion
|
||||
if (startNode.index == expandedIndex) {
|
||||
int max = Math.max(Opsu.groups.size() - MAX_BUTTONS, 0);
|
||||
if (startNode.index > max) // check bounds
|
||||
startNode = Opsu.groups.getBaseNode(max);
|
||||
else
|
||||
startNode = Opsu.groups.getBaseNode(startNode.index);
|
||||
}
|
||||
setFocus(node, -1, false);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyPressed(int key, char c) {
|
||||
switch (key) {
|
||||
case Input.KEY_ESCAPE:
|
||||
if (!search.getText().isEmpty()) {
|
||||
search.setText("");
|
||||
searchTimer = SEARCH_DELAY;
|
||||
} else
|
||||
game.enterState(Opsu.STATE_MAINMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
break;
|
||||
case Input.KEY_F1:
|
||||
game.enterState(Opsu.STATE_OPTIONS, new EmptyTransition(), new FadeInTransition(Color.black));
|
||||
break;
|
||||
case Input.KEY_F2:
|
||||
setFocus(Opsu.groups.getRandomNode(), -1, true);
|
||||
break;
|
||||
case Input.KEY_F12:
|
||||
Options.takeScreenShot();
|
||||
break;
|
||||
case Input.KEY_ENTER:
|
||||
if (focusNode != null)
|
||||
startGame(focusNode.osuFiles.get(focusNode.osuFileIndex));
|
||||
break;
|
||||
case Input.KEY_DOWN:
|
||||
changeIndex(1);
|
||||
break;
|
||||
case Input.KEY_UP:
|
||||
changeIndex(-1);
|
||||
break;
|
||||
case Input.KEY_RIGHT:
|
||||
if (focusNode == null)
|
||||
break;
|
||||
OsuGroupNode next = focusNode.next;
|
||||
if (next != null) {
|
||||
setFocus(next, (next.index == focusNode.index) ? 0 : 1, false);
|
||||
changeIndex(1);
|
||||
}
|
||||
break;
|
||||
case Input.KEY_LEFT:
|
||||
if (focusNode == null)
|
||||
break;
|
||||
OsuGroupNode prev = focusNode.prev;
|
||||
if (prev != null) {
|
||||
if (prev.index == focusNode.index && prev.osuFileIndex < 0) {
|
||||
// skip the group node
|
||||
prev = prev.prev;
|
||||
if (prev == null) // this is the first node
|
||||
break;
|
||||
setFocus(prev, prev.osuFiles.size(), true);
|
||||
|
||||
// move the start node forward if off the screen
|
||||
int size = prev.osuFiles.size();
|
||||
while (size-- >= MAX_BUTTONS)
|
||||
startNode = startNode.next;
|
||||
} else {
|
||||
setFocus(prev, 0, false);
|
||||
changeIndex(-1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Input.KEY_NEXT:
|
||||
changeIndex(MAX_BUTTONS);
|
||||
break;
|
||||
case Input.KEY_PRIOR:
|
||||
changeIndex(-MAX_BUTTONS);
|
||||
break;
|
||||
default:
|
||||
// wait for user to finish typing
|
||||
if (Character.isLetterOrDigit(c) || key == Input.KEY_BACK)
|
||||
searchTimer = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(int oldx, int oldy, int newx, int newy) {
|
||||
// check mouse button (right click scrolls faster)
|
||||
int multiplier;
|
||||
if (input.isMouseButtonDown(Input.MOUSE_RIGHT_BUTTON))
|
||||
multiplier = 4;
|
||||
else if (input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON))
|
||||
multiplier = 1;
|
||||
else
|
||||
return;
|
||||
|
||||
int diff = newy - oldy;
|
||||
if (diff != 0) {
|
||||
diff = ((diff < 0) ? 1 : -1) * multiplier;
|
||||
changeIndex(diff);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseWheelMoved(int newValue) {
|
||||
changeIndex((newValue < 0) ? 1 : -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enter(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
Display.setTitle(game.getTitle());
|
||||
}
|
||||
|
||||
/**
|
||||
* Shifts the startNode forward (+) or backwards (-) by a given number of nodes.
|
||||
* Initiates sliding "animation" by shifting the button Y position.
|
||||
*/
|
||||
private void changeIndex(int shift) {
|
||||
while (shift != 0) {
|
||||
if (startNode == null)
|
||||
break;
|
||||
|
||||
int height = container.getHeight();
|
||||
if (shift < 0 && startNode.prev != null) {
|
||||
startNode = startNode.prev;
|
||||
buttonY += buttonOffset / 4;
|
||||
if (buttonY > height * 0.18f)
|
||||
buttonY = height * 0.18f;
|
||||
shift++;
|
||||
} else if (shift > 0 && startNode.next != null &&
|
||||
Opsu.groups.getNode(startNode, MAX_BUTTONS) != null) {
|
||||
startNode = startNode.next;
|
||||
buttonY -= buttonOffset / 4;
|
||||
if (buttonY < height * 0.14f)
|
||||
buttonY = height * 0.14f;
|
||||
shift--;
|
||||
} else
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new focus node.
|
||||
* @param node the base node; it will be expanded if it isn't already
|
||||
* @param pos the OsuFile element to focus; if out of bounds, it will be randomly chosen
|
||||
* @param flag if true, startNode will be set to the song group node
|
||||
* @return the old focus node
|
||||
*/
|
||||
public OsuGroupNode setFocus(OsuGroupNode node, int pos, boolean flag) {
|
||||
if (node == null)
|
||||
return null;
|
||||
|
||||
OsuGroupNode oldFocus = focusNode;
|
||||
|
||||
// expand node before focusing it
|
||||
if (node.index != Opsu.groups.getExpandedIndex())
|
||||
Opsu.groups.expand(node.index);
|
||||
|
||||
// check pos bounds
|
||||
int length = node.osuFiles.size();
|
||||
if (pos < 0 || pos > length) // set a random pos
|
||||
pos = (int) (Math.random() * length) + 1;
|
||||
|
||||
if (flag)
|
||||
startNode = node;
|
||||
focusNode = Opsu.groups.getNode(node, pos);
|
||||
MusicController.play(focusNode.osuFiles.get(focusNode.osuFileIndex), true);
|
||||
|
||||
// check startNode bounds
|
||||
if (focusNode.index - startNode.index == MAX_BUTTONS - 1)
|
||||
changeIndex(1);
|
||||
while (startNode.index >= Opsu.groups.size() + length + 1 - MAX_BUTTONS &&
|
||||
startNode.prev != null)
|
||||
changeIndex(-1);
|
||||
|
||||
return oldFocus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the game.
|
||||
* @param osu the OsuFile to send to the game
|
||||
*/
|
||||
private void startGame(OsuFile osu) {
|
||||
if (MusicController.isConverting())
|
||||
return;
|
||||
|
||||
Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString()));
|
||||
OsuParser.parseHitObjects(osu);
|
||||
Game.setOsuFile(osu);
|
||||
Game.setRestart(Game.RESTART_NEW);
|
||||
game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user