Hit object refactoring.

- Moved the bulk of hit object parsing into the OsuHitObject constructor, and made all fields private.  Only combo-related data is still set by OsuParser.
- Added 'isCircle()', 'isSlider()', 'isSpinner()', and 'isNewCombo()' methods for convenience.

Other changes:
- Fixed difficulty overrides are no longer affected by game mods.

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han 2014-07-18 15:11:57 -04:00
parent 2380b11f48
commit 717605564d
7 changed files with 410 additions and 231 deletions

View File

@ -500,7 +500,7 @@ public class GameScore {
g.drawOval(circleX, symbolHeight, circleDiameter, circleDiameter);
OsuFile osu = MusicController.getOsuFile();
int firstObjectTime = osu.objects[0].time;
int firstObjectTime = osu.objects[0].getTime();
int trackPosition = MusicController.getPosition();
if (trackPosition > firstObjectTime) {
// map progress (white)

View File

@ -32,7 +32,7 @@ public class OsuHitObject {
TYPE_SPINNER = 8;
/**
* Hit sound types.
* Hit sound types (bits).
*/
public static final byte
SOUND_NORMAL = 0,
@ -53,39 +53,263 @@ public class OsuHitObject {
/**
* Max hit object coordinates.
*/
public static final int
private 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)
/**
* 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
/**
* Starting coordinates (scaled).
*/
private float x, y;
/**
* Start time (in ms).
*/
private int time;
/**
* Hit object type (TYPE_* bitmask).
*/
private int type;
/**
* Hit sound type (SOUND_* bitmask).
*/
private byte hitSound;
/**
* Slider curve type (SLIDER_* constant).
*/
private char sliderType;
/**
* Slider coordinate lists (scaled).
*/
private float[] sliderX, sliderY;
/**
* Slider repeat count.
*/
private int repeat;
/**
* Slider pixel length.
*/
private float pixelLength;
/**
* Spinner end time (in ms).
*/
private int endTime;
// 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
/**
* Current index in combo color array.
*/
private int comboIndex;
/**
* Constructor with all required fields.
* Number to display in hit object.
*/
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;
private int comboNumber;
/**
* Initializes the OsuHitObject data type with container dimensions.
* @param width the container width
* @param height the container height
*/
public static void init(int width, int height) {
xMultiplier = (width * 0.6f) / MAX_X;
yMultiplier = (height * 0.6f) / MAX_Y;
xOffset = width / 5;
yOffset = height / 5;
}
/**
* Constructor.
* @param line the line to be parsed
*/
public OsuHitObject(String line) {
/**
* [OBJECT FORMATS]
* Circles:
* x,y,time,type,hitSound,addition
* 256,148,9466,1,2,0:0:0:0:
*
* Sliders:
* 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:
* x,y,time,type,hitSound,endTime,addition
* 256,192,654,12,0,4029,0:0:0:0:
*
* NOTE: 'addition' is optional, and defaults to "0:0:0:0:".
*/
String tokens[] = line.split(",");
// common fields
this.x = Integer.parseInt(tokens[0]) * xMultiplier + xOffset;
this.y = Integer.parseInt(tokens[1]) * yMultiplier + yOffset;
this.time = Integer.parseInt(tokens[2]);
this.type = Integer.parseInt(tokens[3]);
this.hitSound = Byte.parseByte(tokens[4]);
// type-specific fields
if ((type & OsuHitObject.TYPE_CIRCLE) > 0) {
/* 'addition' not implemented. */
} else if ((type & OsuHitObject.TYPE_SLIDER) > 0) {
// slider curve type and coordinates
String[] sliderTokens = tokens[5].split("\\|");
this.sliderType = sliderTokens[0].charAt(0);
this.sliderX = new float[sliderTokens.length - 1];
this.sliderY = new float[sliderTokens.length - 1];
for (int j = 1; j < sliderTokens.length; j++) {
String[] sliderXY = sliderTokens[j].split(":");
this.sliderX[j - 1] = Integer.parseInt(sliderXY[0]) * xMultiplier + xOffset;
this.sliderY[j - 1] = Integer.parseInt(sliderXY[1]) * yMultiplier + yOffset;
}
this.repeat = Integer.parseInt(tokens[6]);
this.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);
this.endTime = Integer.parseInt(tokens[5]);
/* 'addition' not implemented. */
}
}
/**
* Returns the starting x coordinate.
* @return the x coordinate
*/
public float getX() { return x; }
/**
* Returns the starting y coordinate.
* @return the y coordinate
*/
public float getY() { return y; }
/**
* Returns the start time.
* @return the start time (in ms)
*/
public int getTime() { return time; }
/**
* Returns the hit object type.
* @return the object type (TYPE_* bitmask)
*/
public int getType() { return type; }
/**
* Returns the hit sound type.
* @return the sound type (SOUND_* bitmask)
*/
public byte getHitSoundType() { return hitSound; }
/**
* Returns the slider type.
* @return the slider type (SLIDER_* constant)
*/
public char getSliderType() { return sliderType; }
/**
* Returns a list of slider x coordinates.
* @return the slider x coordinates
*/
public float[] getSliderX() { return sliderX; }
/**
* Returns a list of slider y coordinates.
* @return the slider y coordinates
*/
public float[] getSliderY() { return sliderY; }
/**
* Returns the slider repeat count.
* @return the repeat count
*/
public int getRepeatCount() { return repeat; }
/**
* Returns the slider pixel length.
* @return the pixel length
*/
public float getPixelLength() { return pixelLength; }
/**
* Returns the spinner end time.
* @return the end time (in ms)
*/
public int getEndTime() { return endTime; }
/**
* Sets the current index in the combo color array.
* @param comboIndex the combo index
*/
public void setComboIndex(int comboIndex) { this.comboIndex = comboIndex; }
/**
* Returns the current index in the combo color array.
* @return the combo index
*/
public int getComboIndex() { return comboIndex; }
/**
* Sets the number to display in the hit object.
* @param comboNumber the combo number
*/
public void setComboNumber(int comboNumber) { this.comboNumber = comboNumber; }
/**
* Returns the number to display in the hit object.
* @return the combo number
*/
public int getComboNumber() { return comboNumber; }
/**
* Returns whether or not the hit object is a circle.
* @return true if circle
*/
public boolean isCircle() { return (type & TYPE_CIRCLE) > 0; }
/**
* Returns whether or not the hit object is a slider.
* @return true if slider
*/
public boolean isSlider() { return (type & TYPE_SLIDER) > 0; }
/**
* Returns whether or not the hit object is a spinner.
* @return true if spinner
*/
public boolean isSpinner() { return (type & TYPE_SPINNER) > 0; }
/**
* Returns whether or not the hit object starts a new combo.
* @return true if new combo
*/
public boolean isNewCombo() { return (type & TYPE_NEWCOMBO) > 0; }
}

View File

@ -49,18 +49,6 @@ public class OsuParser {
*/
private static int totalDirectories = -1;
/**
* 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() {}
@ -71,11 +59,8 @@ public class OsuParser {
* @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;
// initialize hit objects
OsuHitObject.init(width, height);
// progress tracking
File[] folders = root.listFiles();
@ -438,23 +423,7 @@ public class OsuParser {
/**
* 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.
* @param osu the OsuFile to parse
*/
public static void parseHitObjects(OsuFile osu) {
if (osu.objects != null) // already parsed
@ -467,72 +436,47 @@ public class OsuParser {
String line = in.readLine();
while (line != null) {
line = line.trim();
if (!line.equals("[HitObjects]")) {
if (!line.equals("[HitObjects]"))
line = in.readLine();
else
break;
}
if (line == null) {
Log.warn(String.format("No hit objects found in OsuFile '%s'.", osu.toString()));
return;
}
// combo info
int comboIndex = 0; // color index
int comboNumber = 1; // combo number
int objectIndex = 0;
while ((line = in.readLine()) != null && objectIndex < osu.objects.length) {
line = line.trim();
if (!isValidLine(line))
continue;
if (line.charAt(0) == '[')
break;
// lines must have at minimum 5 parameters
int tokenCount = line.length() - line.replace(",", "").length();
if (tokenCount < 4)
continue;
// create a new OsuHitObject for each line
OsuHitObject hitObject = new OsuHitObject(line);
// set combo info
// - new combo: get next combo index, reset combo number
// - else: maintain combo index, increase combo number
if (hitObject.isNewCombo()) {
comboIndex = (comboIndex + 1) % osu.combo.length;
comboNumber = 1;
}
hitObject.setComboIndex(comboIndex);
hitObject.setComboNumber(comboNumber++);
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(",");
if (tokens.length < 5)
continue;
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;
osu.objects[objectIndex++] = hitObject;
}
} catch (IOException e) {
Log.error(String.format("Failed to read file '%s'.", osu.getFile().getAbsolutePath()), e);

View File

@ -94,15 +94,15 @@ public class Circle {
* @param trackPosition the current track position
*/
public void draw(int trackPosition) {
int timeDiff = hitObject.time - trackPosition;
int timeDiff = hitObject.getTime() - trackPosition;
if (timeDiff >= 0) {
float x = hitObject.getX(), y = hitObject.getY();
float approachScale = 1 + (timeDiff * 2f / game.getApproachTime());
Utils.drawCentered(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale),
hitObject.x, hitObject.y, color);
Utils.drawCentered(GameImage.HITCIRCLE_OVERLAY.getImage(), hitObject.x, hitObject.y, Color.white);
Utils.drawCentered(GameImage.HITCIRCLE.getImage(), hitObject.x, hitObject.y, color);
score.drawSymbolNumber(hitObject.comboNumber, hitObject.x, hitObject.y,
Utils.drawCentered(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale), x, y, color);
Utils.drawCentered(GameImage.HITCIRCLE_OVERLAY.getImage(), x, y, Color.white);
Utils.drawCentered(GameImage.HITCIRCLE.getImage(), x, y, color);
score.drawSymbolNumber(hitObject.getComboNumber(), x, y,
GameImage.HITCIRCLE.getImage().getWidth() * 0.40f / score.getDefaultSymbolImage(0).getHeight());
}
}
@ -139,13 +139,15 @@ public class Circle {
* @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);
double distance = Math.hypot(hitObject.getX() - x, hitObject.getY() - y);
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2;
if (distance < circleRadius) {
int result = hitResult(hitObject.time);
int result = hitResult(hitObject.getTime());
if (result > -1) {
score.hitResult(hitObject.time, result, hitObject.x, hitObject.y,
color, comboEnd, hitObject.hitSound
score.hitResult(
hitObject.getTime(), result,
hitObject.getX(), hitObject.getY(),
color, comboEnd, hitObject.getHitSoundType()
);
return true;
}
@ -159,26 +161,27 @@ public class Circle {
* @return true if a hit result (miss) was processed
*/
public boolean update(boolean overlap) {
int time = hitObject.getTime();
float x = hitObject.getX(), y = hitObject.getY();
byte hitSound = hitObject.getHitSoundType();
int trackPosition = MusicController.getPosition();
int[] hitResultOffset = game.getHitResultOffsets();
boolean isAutoMod = GameMod.AUTO.isActive();
if (overlap || trackPosition > hitObject.time + hitResultOffset[GameScore.HIT_50]) {
if (overlap || trackPosition > 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, hitObject.hitSound);
score.hitResult(time, GameScore.HIT_300, x, y, color, comboEnd, hitSound);
else // no more points can be scored, so send a miss
score.hitResult(hitObject.time, GameScore.HIT_MISS,
hitObject.x, hitObject.y, null, comboEnd, hitObject.hitSound);
score.hitResult(time, GameScore.HIT_MISS, x, y, null, comboEnd, hitSound);
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, hitObject.hitSound);
if (Math.abs(trackPosition - time) < hitResultOffset[GameScore.HIT_300]) {
score.hitResult(time, GameScore.HIT_300, x, y, color, comboEnd, hitSound);
return true;
}
}

View File

@ -155,8 +155,8 @@ public class Slider {
* Constructor.
*/
public Bezier() {
this.order = hitObject.sliderX.length + 1;
this.step = 5 / hitObject.pixelLength;
this.order = hitObject.getSliderX().length + 1;
this.step = 5 / hitObject.getPixelLength();
// calculate curve points for drawing
int N = (int) (1 / step);
@ -172,7 +172,7 @@ public class Slider {
curveY[N] = getY(order - 1);
// calculate angles (if needed)
if (hitObject.repeat > 1) {
if (hitObject.getRepeatCount() > 1) {
float[] c1 = pointAt(0f);
float[] c2 = pointAt(step);
startAngle = (float) (Math.atan2(c2[1] - c1[1], c2[0] - c1[0]) * 180 / Math.PI);
@ -186,14 +186,14 @@ public class Slider {
* 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];
return (i == 0) ? hitObject.getX() : hitObject.getSliderX()[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];
return (i == 0) ? hitObject.getY() : hitObject.getSliderY()[i - 1];
}
/**
@ -341,7 +341,9 @@ public class Slider {
* @param currentObject true if this is the current hit object
*/
public void draw(int trackPosition, boolean currentObject) {
int timeDiff = hitObject.time - trackPosition;
float x = hitObject.getX(), y = hitObject.getY();
float[] sliderX = hitObject.getSliderX(), sliderY = hitObject.getSliderY();
int timeDiff = hitObject.getTime() - trackPosition;
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
Image hitCircle = GameImage.HITCIRCLE.getImage();
@ -358,36 +360,35 @@ public class Slider {
}
// end circle
int lastIndex = hitObject.sliderX.length - 1;
Utils.drawCentered(hitCircleOverlay, hitObject.sliderX[lastIndex], hitObject.sliderY[lastIndex], Color.white);
Utils.drawCentered(hitCircle, hitObject.sliderX[lastIndex], hitObject.sliderY[lastIndex], color);
int lastIndex = sliderX.length - 1;
Utils.drawCentered(hitCircleOverlay, sliderX[lastIndex], sliderY[lastIndex], Color.white);
Utils.drawCentered(hitCircle, sliderX[lastIndex], sliderY[lastIndex], color);
// start circle
Utils.drawCentered(hitCircleOverlay, hitObject.x, hitObject.y, Color.white);
Utils.drawCentered(hitCircle, hitObject.x, hitObject.y, color);
Utils.drawCentered(hitCircleOverlay, x, y, Color.white);
Utils.drawCentered(hitCircle, x, y, color);
if (sliderClicked)
; // don't draw current combo number if already clicked
else
score.drawSymbolNumber(hitObject.comboNumber, hitObject.x, hitObject.y,
score.drawSymbolNumber(hitObject.getComboNumber(), x, y,
hitCircle.getWidth() * 0.40f / score.getDefaultSymbolImage(0).getHeight());
// repeats
if (hitObject.repeat - 1 > currentRepeats) {
if (hitObject.getRepeatCount() - 1 > currentRepeats) {
Image arrow = GameImage.REVERSEARROW.getImage();
if (currentRepeats % 2 == 0) { // last circle
arrow.setRotation(bezier.getEndAngle());
arrow.drawCentered(hitObject.sliderX[lastIndex], hitObject.sliderY[lastIndex]);
arrow.drawCentered(sliderX[lastIndex], sliderY[lastIndex]);
} else { // first circle
arrow.setRotation(bezier.getStartAngle());
arrow.drawCentered(hitObject.x, hitObject.y);
arrow.drawCentered(x, y);
}
}
if (timeDiff >= 0) {
// approach circle
float approachScale = 1 + (timeDiff * 2f / game.getApproachTime());
Utils.drawCentered(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale),
hitObject.x, hitObject.y, color);
Utils.drawCentered(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale), x, y, color);
} else {
float[] c = bezier.pointAt(getT(trackPosition, false));
@ -407,7 +408,7 @@ public class Slider {
* @return the hit result (GameScore.HIT_* constants)
*/
public int hitResult() {
int lastIndex = hitObject.sliderX.length - 1;
int lastIndex = hitObject.getSliderX().length - 1;
float tickRatio = (float) ticksHit / tickIntervals;
int result;
@ -421,12 +422,12 @@ public class Slider {
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, hitObject.hitSound);
score.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result,
hitObject.getSliderX()[lastIndex], hitObject.getSliderY()[lastIndex],
color, comboEnd, hitObject.getHitSoundType());
else // first circle
score.hitResult(hitObject.time + (int) sliderTimeTotal, result,
hitObject.x, hitObject.y, color, comboEnd, hitObject.hitSound);
score.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result,
hitObject.getX(), hitObject.getY(), color, comboEnd, hitObject.getHitSoundType());
return result;
}
@ -442,11 +443,11 @@ public class Slider {
if (sliderClicked) // first circle already processed
return false;
double distance = Math.hypot(hitObject.x - x, hitObject.y - y);
double distance = Math.hypot(hitObject.getX() - x, hitObject.getY() - y);
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2;
if (distance < circleRadius) {
int trackPosition = MusicController.getPosition();
int timeDiff = Math.abs(trackPosition - hitObject.time);
int timeDiff = Math.abs(trackPosition - hitObject.getTime());
int[] hitResultOffset = game.getHitResultOffsets();
int result = -1;
@ -459,8 +460,8 @@ public class Slider {
if (result > -1) {
sliderClicked = true;
score.sliderTickResult(hitObject.time, result,
hitObject.x, hitObject.y, hitObject.hitSound);
score.sliderTickResult(hitObject.getTime(), result,
hitObject.getX(), hitObject.getY(), hitObject.getHitSoundType());
return true;
}
}
@ -476,15 +477,17 @@ public class Slider {
* @return true if slider ended
*/
public boolean update(boolean overlap, int delta, int mouseX, int mouseY) {
int repeatCount = hitObject.getRepeatCount();
// slider time and tick calculations
if (sliderTimeTotal == 0f) {
// slider time
this.sliderTime = game.getBeatLength() * (hitObject.pixelLength / sliderMultiplier) / 100f;
this.sliderTimeTotal = sliderTime * hitObject.repeat;
this.sliderTime = game.getBeatLength() * (hitObject.getPixelLength() / sliderMultiplier) / 100f;
this.sliderTimeTotal = sliderTime * repeatCount;
// ticks
float tickLengthDiv = 100f * sliderMultiplier / sliderTickRate / game.getTimingPointMultiplier();
int tickCount = (int) Math.ceil(hitObject.pixelLength / tickLengthDiv) - 1;
int tickCount = (int) Math.ceil(hitObject.getPixelLength() / tickLengthDiv) - 1;
if (tickCount > 0) {
this.ticksT = new float[tickCount];
float tickTOffset = 1f / (tickCount + 1);
@ -494,37 +497,40 @@ public class Slider {
}
}
byte hitSound = hitObject.getHitSoundType();
int trackPosition = MusicController.getPosition();
int[] hitResultOffset = game.getHitResultOffsets();
int lastIndex = hitObject.sliderX.length - 1;
int lastIndex = hitObject.getSliderX().length - 1;
boolean isAutoMod = GameMod.AUTO.isActive();
if (!sliderClicked) {
int time = hitObject.getTime();
// start circle time passed
if (trackPosition > hitObject.time + hitResultOffset[GameScore.HIT_50]) {
if (trackPosition > 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, hitObject.hitSound);
score.sliderTickResult(time, GameScore.HIT_SLIDER30,
hitObject.getX(), hitObject.getY(), hitSound);
} else
score.sliderTickResult(hitObject.time, GameScore.HIT_MISS,
hitObject.x, hitObject.y, hitObject.hitSound);
score.sliderTickResult(time, GameScore.HIT_MISS,
hitObject.getX(), hitObject.getY(), hitSound);
}
// "auto" mod: send a perfect hit result
else if (isAutoMod) {
if (Math.abs(trackPosition - hitObject.time) < hitResultOffset[GameScore.HIT_300]) {
if (Math.abs(trackPosition - time) < hitResultOffset[GameScore.HIT_300]) {
ticksHit++;
sliderClicked = true;
score.sliderTickResult(hitObject.time, GameScore.HIT_SLIDER30,
hitObject.x, hitObject.y, hitObject.hitSound);
score.sliderTickResult(time, GameScore.HIT_SLIDER30,
hitObject.getX(), hitObject.getY(), hitSound);
}
}
}
// end of slider
if (overlap || trackPosition > hitObject.time + sliderTimeTotal) {
if (overlap || trackPosition > hitObject.getTime() + sliderTimeTotal) {
tickIntervals++;
// "auto" mod: send a perfect hit result
@ -533,7 +539,7 @@ public class Slider {
// check if cursor pressed and within end circle
else if (Utils.isGameKeyPressed()) {
double distance = Math.hypot(hitObject.sliderX[lastIndex] - mouseX, hitObject.sliderY[lastIndex] - mouseY);
double distance = Math.hypot(hitObject.getSliderX()[lastIndex] - mouseX, hitObject.getSliderY()[lastIndex] - mouseY);
int followCircleRadius = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getWidth() / 2;
if (distance < followCircleRadius)
ticksHit++;
@ -546,7 +552,7 @@ public class Slider {
// repeats
boolean isNewRepeat = false;
if (hitObject.repeat - 1 > currentRepeats) {
if (repeatCount - 1 > currentRepeats) {
float t = getT(trackPosition, true);
if (Math.floor(t) > currentRepeats) {
currentRepeats++;
@ -558,8 +564,8 @@ public class Slider {
// ticks
boolean isNewTick = false;
if (ticksT != null &&
tickIntervals < (ticksT.length * (currentRepeats + 1)) + hitObject.repeat &&
tickIntervals < (ticksT.length * hitObject.repeat) + hitObject.repeat) {
tickIntervals < (ticksT.length * (currentRepeats + 1)) + repeatCount &&
tickIntervals < (ticksT.length * repeatCount) + repeatCount) {
float t = getT(trackPosition, true);
if (t - Math.floor(t) >= ticksT[tickIndex]) {
tickIntervals++;
@ -582,11 +588,10 @@ public class Slider {
ticksHit++;
if (currentRepeats % 2 > 0) // last circle
score.sliderTickResult(trackPosition, GameScore.HIT_SLIDER30,
hitObject.sliderX[lastIndex], hitObject.sliderY[lastIndex],
hitObject.hitSound);
hitObject.getSliderX()[lastIndex], hitObject.getSliderY()[lastIndex], hitSound);
else // first circle
score.sliderTickResult(trackPosition, GameScore.HIT_SLIDER30,
c[0], c[1], hitObject.hitSound);
c[0], c[1], hitSound);
}
// held during new tick
@ -599,7 +604,7 @@ public class Slider {
followCircleActive = false;
if (isNewRepeat)
score.sliderTickResult(trackPosition, GameScore.HIT_MISS, 0, 0, hitObject.hitSound);
score.sliderTickResult(trackPosition, GameScore.HIT_MISS, 0, 0, hitSound);
if (isNewTick)
score.sliderTickResult(trackPosition, GameScore.HIT_MISS, 0, 0, (byte) -1);
}
@ -614,7 +619,7 @@ public class Slider {
* @return the t value: raw [0, repeats] or looped [0, 1]
*/
public float getT(int trackPosition, boolean raw) {
float t = (trackPosition - hitObject.time) / sliderTime;
float t = (trackPosition - hitObject.getTime()) / sliderTime;
if (raw)
return t;
else {

View File

@ -78,7 +78,8 @@ public class Spinner {
Image spinnerCircle = GameImage.SPINNER_CIRCLE.getImage();
GameImage.SPINNER_CIRCLE.setImage(spinnerCircle.getScaledCopy(height * 9 / 10, height * 9 / 10));
GameImage.SPINNER_APPROACHCIRCLE.setImage(GameImage.SPINNER_APPROACHCIRCLE.getImage().getScaledCopy(spinnerCircle.getWidth(), spinnerCircle.getHeight()));
GameImage.SPINNER_APPROACHCIRCLE.setImage(GameImage.SPINNER_APPROACHCIRCLE.getImage().getScaledCopy(
spinnerCircle.getWidth(), spinnerCircle.getHeight()));
GameImage.SPINNER_METRE.setImage(GameImage.SPINNER_METRE.getImage().getScaledCopy(width, height));
}
@ -94,7 +95,7 @@ public class Spinner {
// calculate rotations needed
float spinsPerMinute = 100 + (score.getDifficulty() * 15);
rotationsNeeded = spinsPerMinute * (hitObject.endTime - hitObject.time) / 60000f;
rotationsNeeded = spinsPerMinute * (hitObject.getEndTime() - hitObject.getTime()) / 60000f;
}
/**
@ -103,7 +104,7 @@ public class Spinner {
* @param g the graphics context
*/
public void draw(int trackPosition, Graphics g) {
int timeDiff = hitObject.time - trackPosition;
int timeDiff = hitObject.getTime() - trackPosition;
boolean spinnerComplete = (rotations >= rotationsNeeded);
// TODO: draw "OSU!" image after spinner ends
@ -126,8 +127,9 @@ public class Spinner {
spinnerMetreSub.draw(0, height - spinnerMetreSub.getHeight());
// main spinner elements
float approachScale = 1 - ((float) timeDiff / (hitObject.getTime() - hitObject.getEndTime()));
GameImage.SPINNER_CIRCLE.getImage().drawCentered(width / 2, height / 2);
GameImage.SPINNER_APPROACHCIRCLE.getImage().getScaledCopy(1 - ((float) timeDiff / (hitObject.time - hitObject.endTime))).drawCentered(width / 2, height / 2);
GameImage.SPINNER_APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(width / 2, height / 2);
GameImage.SPINNER_SPIN.getImage().drawCentered(width / 2, height * 3 / 4);
if (spinnerComplete) {
@ -158,7 +160,7 @@ public class Spinner {
else
result = GameScore.HIT_MISS;
score.hitResult(hitObject.endTime, result, width / 2, height / 2,
score.hitResult(hitObject.getEndTime(), result, width / 2, height / 2,
Color.transparent, true, (byte) -1);
return result;
}
@ -177,7 +179,7 @@ public class Spinner {
return true;
// end of spinner
if (trackPosition > hitObject.endTime) {
if (trackPosition > hitObject.getEndTime()) {
hitResult();
return true;
}

View File

@ -316,8 +316,8 @@ public class Game extends BasicGameState {
// skip beginning
if (objectIndex == 0 &&
osu.objects[0].time - SKIP_OFFSET > 5000 &&
trackPosition < osu.objects[0].time - SKIP_OFFSET)
osu.objects[0].getTime() - SKIP_OFFSET > 5000 &&
trackPosition < osu.objects[0].getTime() - SKIP_OFFSET)
skipButton.draw();
if (isLeadIn())
@ -325,7 +325,7 @@ public class Game extends BasicGameState {
// countdown
if (osu.countdown > 0) { // TODO: implement half/double rate settings
int timeDiff = osu.objects[0].time - trackPosition;
int timeDiff = osu.objects[0].getTime() - trackPosition;
if (timeDiff >= 500 && timeDiff < 3000) {
if (timeDiff >= 1500) {
GameImage.COUNTDOWN_READY.getImage().drawCentered(width / 2, height / 2);
@ -368,18 +368,18 @@ public class Game extends BasicGameState {
// 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++)
for (int i = objectIndex; i < osu.objects.length && osu.objects[i].getTime() < 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)
if (hitObject.isCircle())
circles.get(i).draw(trackPosition);
else if ((hitObject.type & OsuHitObject.TYPE_SLIDER) > 0)
else if (hitObject.isSlider())
sliders.get(i).draw(trackPosition, stack.isEmpty());
else if ((hitObject.type & OsuHitObject.TYPE_SPINNER) > 0) {
else if (hitObject.isSpinner()) {
if (stack.isEmpty()) // only draw spinner at objectIndex
spinners.get(i).draw(trackPosition, g);
else
@ -487,7 +487,7 @@ public class Game extends BasicGameState {
// song beginning
if (objectIndex == 0) {
if (trackPosition < osu.objects[0].time)
if (trackPosition < osu.objects[0].getTime())
return; // nothing to do here
}
@ -544,20 +544,20 @@ public class Game extends BasicGameState {
}
// update objects (loop in unlikely event of any skipped indexes)
while (objectIndex < osu.objects.length && trackPosition > osu.objects[objectIndex].time) {
while (objectIndex < osu.objects.length && trackPosition > osu.objects[objectIndex].getTime()) {
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]);
trackPosition > osu.objects[objectIndex + 1].getTime() - hitResultOffset[GameScore.HIT_300]);
// check completion status of the hit object
boolean done = false;
if ((hitObject.type & OsuHitObject.TYPE_CIRCLE) > 0)
if (hitObject.isCircle())
done = circles.get(objectIndex).update(overlap);
else if ((hitObject.type & OsuHitObject.TYPE_SLIDER) > 0)
else if (hitObject.isSlider())
done = sliders.get(objectIndex).update(overlap, delta, input.getMouseX(), input.getMouseY());
else if ((hitObject.type & OsuHitObject.TYPE_SPINNER) > 0)
else if (hitObject.isSpinner())
done = spinners.get(objectIndex).update(overlap, delta, input.getMouseX(), input.getMouseY());
// increment object index?
@ -586,7 +586,7 @@ public class Game extends BasicGameState {
// pause game
int trackPosition = MusicController.getPosition();
if (pauseTime < 0 && breakTime <= 0 &&
trackPosition >= osu.objects[0].time &&
trackPosition >= osu.objects[0].getTime() &&
!GameMod.AUTO.isActive()) {
pausedMouseX = input.getMouseX();
pausedMouseY = input.getMouseY();
@ -642,7 +642,7 @@ public class Game extends BasicGameState {
// skip to checkpoint
MusicController.setPosition(checkpoint);
while (objectIndex < osu.objects.length &&
osu.objects[objectIndex++].time <= MusicController.getPosition())
osu.objects[objectIndex++].getTime() <= MusicController.getPosition())
;
objectIndex--;
} catch (SlickException e) {
@ -692,14 +692,14 @@ public class Game extends BasicGameState {
return;
// circles
if ((hitObject.type & OsuHitObject.TYPE_CIRCLE) > 0) {
if (hitObject.isCircle()) {
boolean hit = circles.get(objectIndex).mousePressed(x, y);
if (hit)
objectIndex++;
}
// sliders
else if ((hitObject.type & OsuHitObject.TYPE_SLIDER) > 0)
else if (hitObject.isSlider())
sliders.get(objectIndex).mousePressed(x, y);
}
@ -733,15 +733,15 @@ public class Game extends BasicGameState {
// 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)
if (i + 1 < osu.objects.length && osu.objects[i + 1].isNewCombo())
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) {
Color color = osu.combo[hitObject.getComboIndex()];
if (hitObject.isCircle()) {
circles.put(i, new Circle(hitObject, this, score, color, comboEnd));
} else if (hitObject.isSlider()) {
sliders.put(i, new Slider(hitObject, this, score, color, comboEnd));
} else if (hitObject.isSpinner()) {
spinners.put(i, new Spinner(hitObject, this, score));
}
}
@ -794,15 +794,16 @@ public class Game extends BasicGameState {
* @return true if skipped, false otherwise
*/
private boolean skipIntro() {
int firstObjectTime = osu.objects[0].getTime();
int trackPosition = MusicController.getPosition();
if (objectIndex == 0 &&
osu.objects[0].time - SKIP_OFFSET > 4000 &&
trackPosition < osu.objects[0].time - SKIP_OFFSET) {
firstObjectTime - SKIP_OFFSET > 4000 &&
trackPosition < firstObjectTime - SKIP_OFFSET) {
if (isLeadIn()) {
leadInTime = 0;
MusicController.resume();
}
MusicController.setPosition(osu.objects[0].time - SKIP_OFFSET);
MusicController.setPosition(firstObjectTime - SKIP_OFFSET);
SoundController.playSound(SoundController.SOUND_MENUHIT);
return true;
}
@ -883,16 +884,6 @@ public class Game extends BasicGameState {
float overallDifficulty = osu.overallDifficulty;
float HPDrainRate = osu.HPDrainRate;
// fixed difficulty overrides
if (Options.getFixedCS() > 0f)
circleSize = Options.getFixedCS();
if (Options.getFixedAR() > 0f)
approachRate = Options.getFixedAR();
if (Options.getFixedOD() > 0f)
overallDifficulty = Options.getFixedOD();
if (Options.getFixedHP() > 0f)
HPDrainRate = Options.getFixedHP();
// "Hard Rock" modifiers
if (GameMod.HARD_ROCK.isActive()) {
circleSize = Math.min(circleSize * 1.4f, 10);
@ -909,6 +900,16 @@ public class Game extends BasicGameState {
HPDrainRate /= 2f;
}
// fixed difficulty overrides
if (Options.getFixedCS() > 0f)
circleSize = Options.getFixedCS();
if (Options.getFixedAR() > 0f)
approachRate = Options.getFixedAR();
if (Options.getFixedOD() > 0f)
overallDifficulty = Options.getFixedOD();
if (Options.getFixedHP() > 0f)
HPDrainRate = Options.getFixedHP();
// initialize objects
Circle.init(container, circleSize);
Slider.init(container, circleSize, osu);