Merge pull request #99 from fluddokt/ReplayTest

Replay importing, spinner fixes (fixes #67), replay seeking, in-place MD5 calculation, pitch change time sync (fixes #86).
This commit is contained in:
Jeffrey Han
2015-06-28 21:14:10 -05:00
22 changed files with 622 additions and 96 deletions

View File

@@ -38,6 +38,9 @@ public class Circle implements GameObject {
/** The amount of time, in milliseconds, to fade in the circle. */
private static final int FADE_IN_TIME = 375;
/** The diameter of Circle Hitobjects */
private static float diameter;
/** The associated HitObject. */
private HitObject hitObject;
@@ -62,11 +65,12 @@ public class Circle implements GameObject {
* @param circleSize the map's circleSize value
*/
public static void init(GameContainer container, float circleSize) {
int diameter = (int) (104 - (circleSize * 8));
diameter = (int) (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameter, diameter));
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameter, diameter));
GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameter, diameter));
diameter = (104 - (circleSize * 8));
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
int diameterInt = (int)diameter;
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
}
/**
@@ -119,15 +123,16 @@ public class Circle implements GameObject {
private int hitResult(int time) {
int timeDiff = Math.abs(time);
int[] hitResultOffset = game.getHitResultOffsets();
int result = -1;
if (timeDiff < hitResultOffset[GameData.HIT_300])
if (timeDiff <= hitResultOffset[GameData.HIT_300])
result = GameData.HIT_300;
else if (timeDiff < hitResultOffset[GameData.HIT_100])
else if (timeDiff <= hitResultOffset[GameData.HIT_100])
result = GameData.HIT_100;
else if (timeDiff < hitResultOffset[GameData.HIT_50])
else if (timeDiff <= hitResultOffset[GameData.HIT_50])
result = GameData.HIT_50;
else if (timeDiff < hitResultOffset[GameData.HIT_MISS])
else if (timeDiff <= hitResultOffset[GameData.HIT_MISS])
result = GameData.HIT_MISS;
//else not a hit
@@ -137,8 +142,7 @@ public class Circle implements GameObject {
@Override
public boolean mousePressed(int x, int y, int trackPosition) {
double distance = Math.hypot(this.x - x, this.y - y);
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2;
if (distance < circleRadius) {
if (distance < diameter/2) {
int timeDiff = trackPosition - hitObject.getTime();
int result = hitResult(timeDiff);
@@ -158,7 +162,7 @@ public class Circle implements GameObject {
int[] hitResultOffset = game.getHitResultOffsets();
boolean isAutoMod = GameMod.AUTO.isActive();
if (overlap || trackPosition > time + hitResultOffset[GameData.HIT_50]) {
if (trackPosition > time + hitResultOffset[GameData.HIT_50]) {
if (isAutoMod) // "auto" mod: catch any missed notes due to lag
data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, 0, HitObjectType.CIRCLE, null, true);
@@ -193,4 +197,9 @@ public class Circle implements GameObject {
this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY();
}
@Override
public void reset() {
}
}

View File

@@ -63,4 +63,9 @@ public class DummyObject implements GameObject {
this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY();
}
@Override
public void reset() {
}
}

View File

@@ -69,4 +69,11 @@ public interface GameObject {
* Updates the position of the hit object.
*/
public void updatePosition();
/**
* Resets the hit object so that it can be reused.
*/
public void reset();
}

View File

@@ -49,6 +49,10 @@ public class Slider implements GameObject {
/** Rate at which slider ticks are placed. */
private static float sliderTickRate = 1.0f;
private static float followRadius;
private static float diameter;
/** The amount of time, in milliseconds, to fade in the slider. */
private static final int FADE_IN_TIME = 375;
@@ -100,6 +104,8 @@ public class Slider implements GameObject {
/** Container dimensions. */
private static int containerWidth, containerHeight;
/**
* Initializes the Slider data type with images and dimensions.
@@ -110,10 +116,12 @@ public class Slider implements GameObject {
public static void init(GameContainer container, float circleSize, Beatmap beatmap) {
containerWidth = container.getWidth();
containerHeight = container.getHeight();
int diameter = (int) (104 - (circleSize * 8));
diameter = (int) (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
diameter = (104 - (circleSize * 8));
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
int diameterInt = (int)diameter;
followRadius = diameter / 2 * 3f;
// slider ball
if (GameImage.SLIDER_BALL.hasSkinImages() ||
(!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
@@ -121,11 +129,11 @@ public class Slider implements GameObject {
else
sliderBallImages = new Image[]{ GameImage.SLIDER_BALL.getImage() };
for (int i = 0; i < sliderBallImages.length; i++)
sliderBallImages[i] = sliderBallImages[i].getScaledCopy(diameter * 118 / 128, diameter * 118 / 128);
sliderBallImages[i] = sliderBallImages[i].getScaledCopy(diameterInt * 118 / 128, diameterInt * 118 / 128);
GameImage.SLIDER_FOLLOWCIRCLE.setImage(GameImage.SLIDER_FOLLOWCIRCLE.getImage().getScaledCopy(diameter * 259 / 128, diameter * 259 / 128));
GameImage.REVERSEARROW.setImage(GameImage.REVERSEARROW.getImage().getScaledCopy(diameter, diameter));
GameImage.SLIDER_TICK.setImage(GameImage.SLIDER_TICK.getImage().getScaledCopy(diameter / 4, diameter / 4));
GameImage.SLIDER_FOLLOWCIRCLE.setImage(GameImage.SLIDER_FOLLOWCIRCLE.getImage().getScaledCopy(diameterInt * 259 / 128, diameterInt * 259 / 128));
GameImage.REVERSEARROW.setImage(GameImage.REVERSEARROW.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.SLIDER_TICK.setImage(GameImage.SLIDER_TICK.getImage().getScaledCopy(diameterInt / 4, diameterInt / 4));
sliderMultiplier = beatmap.sliderMultiplier;
sliderTickRate = beatmap.sliderTickRate;
@@ -272,6 +280,55 @@ public class Slider implements GameObject {
* @return the hit result (GameData.HIT_* constants)
*/
private int hitResult() {
/*
time scoredelta score-hit-initial-tick= unaccounted
(1/4 - 1) 396 - 300 - 30 46
(1+1/4 - 2) 442 - 300 - 30 - 10
(2+1/4 - 3) 488 - 300 - 30 - 2*10 896 (408)5x
(3+1/4 - 4) 534 - 300 - 30 - 3*10
(4+1/4 - 5) 580 - 300 - 30 - 4*10
(5+1/4 - 6) 626 - 300 - 30 - 5*10
(6+1/4 - 7) 672 - 300 - 30 - 6*10
difficultyMulti = 3 (+36 per combo)
score =
(t)ticks(10) * nticks +
(h)hitValue
(c)combo (hitValue/25 * difficultyMultiplier*(combo-1))
(i)initialHit (30) +
(f)finalHit(30) +
s t h c i f
626 - 10*5 - 300 - 276(-216 - 30 - 30) (all)(7x)
240 - 10*5 - 100 - 90 (-60 <- 30>) (no final or initial)(6x)
218 - 10*4 - 100 - 78 (-36 - 30) (4 tick no initial)(5x)
196 - 10*3 - 100 - 66 (-24 - 30 ) (3 tick no initial)(4x)
112 - 10*2 - 50 - 42 (-12 - 30 ) (2 tick no initial)(3x)
96 - 10 - 50 - 36 ( -6 - 30 ) (1 tick no initial)(2x)
206 - 10*4 - 100 - 66 (-36 - 30 ) (4 tick no initial)(4x)
184 - 10*3 - 100 - 54 (-24 - 30 ) (3 tick no initial)(3x)
90 - 10 - 50 - 30 ( - 30 ) (1 tick no initial)(0x)
194 - 10*4 - 100 - 54 (-24 - 30 ) (4 tick no initial)(3x)
170 - 10*4 - 100 - 30 ( - 30 ) (4 tick no final)(0x)
160 - 10*3 - 100 - 30 ( - 30 ) (3 tick no final)(0x)
100 - 10*2 - 50 - 30 ( - 30 ) (2 tick no final)(0x)
198 - 10*5 - 100 - 48 (-36 ) (no initial and final)(5x)
110 - 50 - ( - 30 - 30 ) (final and initial no tick)(0x)
80 - 50 - ( <- 30> ) (only final or initial)(0x)
140 - 10*4 - 100 - 0 (4 ticks only)(0x)
80 - 10*3 - 50 - 0 (3 tick only)(0x)
70 - 10*2 - 50 - 0 (2 tick only)(0x)
60 - 10 - 50 - 0 (1 tick only)(0x)
*/
float tickRatio = (float) ticksHit / tickIntervals;
int result;
@@ -308,8 +365,7 @@ public class Slider implements GameObject {
return false;
double distance = Math.hypot(this.x - x, this.y - y);
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2;
if (distance < circleRadius) {
if (distance < diameter / 2) {
int timeDiff = Math.abs(trackPosition - hitObject.getTime());
int[] hitResultOffset = game.getHitResultOffsets();
@@ -365,26 +421,29 @@ public class Slider implements GameObject {
}
// end of slider
if (overlap || trackPosition > hitObject.getTime() + sliderTimeTotal) {
if (trackPosition > hitObject.getTime() + sliderTimeTotal) {
tickIntervals++;
// check if cursor pressed and within end circle
if (keyPressed || GameMod.RELAX.isActive()) {
float[] c = curve.pointAt(getT(trackPosition, false));
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
int followCircleRadius = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getWidth() / 2;
if (distance < followCircleRadius)
if (distance < followRadius)
sliderClickedFinal = true;
}
// final circle hit
if (sliderClickedFinal)
if (sliderClickedFinal){
ticksHit++;
data.sliderFinalResult(hitObject.getTime(), GameData.HIT_SLIDER30, this.x, this.y, hitObject, currentRepeats);
}
// "auto" mod: always send a perfect hit result
if (isAutoMod)
ticksHit = tickIntervals;
//TODO missing the final shouldn't increment the combo
// calculate and send slider result
hitResult();
return true;
@@ -417,8 +476,7 @@ public class Slider implements GameObject {
// holding slider...
float[] c = curve.pointAt(getT(trackPosition, false));
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
int followCircleRadius = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getWidth() / 2;
if (((keyPressed || GameMod.RELAX.isActive()) && distance < followCircleRadius) || isAutoMod) {
if (((keyPressed || GameMod.RELAX.isActive()) && distance < followRadius) || isAutoMod) {
// mouse pressed and within follow circle
followCircleActive = true;
data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER);
@@ -501,4 +559,16 @@ public class Slider implements GameObject {
return (floor % 2 == 0) ? t - floor : floor + 1 - t;
}
}
@Override
public void reset() {
sliderClickedInitial = false;
sliderClickedFinal = false;
followCircleActive = false;
currentRepeats = 0;
tickIndex = 0;
ticksHit = 0;
tickIntervals = 1;
}
}

View File

@@ -45,11 +45,10 @@ public class Spinner implements GameObject {
private static float overallDifficulty = 5f;
/** The number of rotation velocities to store. */
// note: currently takes about 200ms to spin up (4 * 50)
private static final int MAX_ROTATION_VELOCITIES = 50;
private int maxStoredDeltaAngles;
/** The amount of time, in milliseconds, before another velocity is stored. */
private static final int DELTA_UPDATE_TIME = 4;
private static final float DELTA_UPDATE_TIME = 1000 / 60f;
/** The amount of time, in milliseconds, to fade in the spinner. */
private static final int FADE_IN_TIME = 500;
@@ -64,6 +63,8 @@ public class Spinner implements GameObject {
TWO_PI = (float) (Math.PI * 2),
HALF_PI = (float) (Math.PI / 2);
private static final float MAX_ANG_DIFF = DELTA_UPDATE_TIME * AUTO_MULTIPLIER; // ~95.3
/** The associated HitObject. */
private HitObject hitObject;
@@ -83,19 +84,25 @@ public class Spinner implements GameObject {
private float rotationsNeeded;
/** The remaining amount of time that was not used. */
private int deltaOverflow;
private float deltaOverflow;
/** The sum of all the velocities in storedVelocities. */
private float sumVelocity = 0f;
private float sumDeltaAngle = 0f;
/** Array holding the most recent rotation velocities. */
private float[] storedVelocities = new float[MAX_ROTATION_VELOCITIES];
private float[] storedDeltaAngle;
/** True if the mouse cursor is pressed. */
private boolean isSpinning;
/** Current index of the stored velocities in rotations/second. */
private int velocityIndex = 0;
private int deltaAngleIndex = 0;
/** The remaining amount of the angle that was not used. */
private float deltaAngleOverflow = 0;
/** The RPM that is drawn to the screen. */
private int drawnRPM = 0;
/**
* Initializes the Spinner data type with images and dimensions.
@@ -117,7 +124,46 @@ public class Spinner implements GameObject {
public Spinner(HitObject hitObject, Game game, GameData data) {
this.hitObject = hitObject;
this.data = data;
/*
1 beat = 731.707317073171ms
RPM at frame X with spinner Y beats long
10 20 30 40 50 60 <frame#
1.00 306 418 457 470
1.25 323 424 459 471 475
1.5 305 417 456 470 475 477
1.75 322 417 456 471 475
2.00 304 410 454 469 474 476
2.25 303 410 451 467 474 476
2.50 303 417 456 470 475 476
2.75 302 416 456 470 475 476
3.00 301 416 456 470 475 <-- ~2sec
4.00 274 414 453 470 475
5.00 281 409 454 469 475
6.00 232 392 451 467 472 476
6.25 193 378 443 465
6.50 133 344 431 461
6.75 85 228 378 435 463 472 <-- ~5sec
7.00 53 154 272 391 447
8.00 53 154 272 391 447
9.00 53 154 272 400 450
10.00 53 154 272 400 450
15.00 53 154 272 391 444 466
20.00 61 154 272 400 447
25.00 53 154 272 391 447 466
^beats
*/
//TODO not correct at all, but close enough?
//<2sec ~ 12 ~ 200ms
//>5sec ~ 48 ~ 800ms
final int minVel = 12;
final int maxVel = 48;
final int minTime = 2000;
final int maxTime = 5000;
maxStoredDeltaAngles = (int) Utils.clamp(
(hitObject.getEndTime() - hitObject.getTime() - minTime) * (maxVel-minVel)/(maxTime-minTime) + minVel
, minVel, maxVel);
storedDeltaAngle = new float[maxStoredDeltaAngles];
// calculate rotations needed
float spinsPerMinute = 100 + (overallDifficulty * 15);
rotationsNeeded = spinsPerMinute * (hitObject.getEndTime() - hitObject.getTime()) / 60000f;
@@ -144,12 +190,11 @@ public class Spinner implements GameObject {
}
// rpm
int rpm = Math.abs(Math.round(sumVelocity / storedVelocities.length * 60));
Image rpmImg = GameImage.SPINNER_RPM.getImage();
rpmImg.setAlpha(alpha);
rpmImg.drawCentered(width / 2f, height - rpmImg.getHeight() / 2f);
if (timeDiff < 0)
data.drawSymbolString(Integer.toString(rpm), (width + rpmImg.getWidth() * 0.95f) / 2f,
data.drawSymbolString(Integer.toString(drawnRPM), (width + rpmImg.getWidth() * 0.95f) / 2f,
height - data.getScoreSymbolImage('0').getHeight() * 1.025f, 1f, 1f, true);
// spinner meter (subimage)
@@ -205,7 +250,10 @@ public class Spinner implements GameObject {
}
@Override
public boolean mousePressed(int x, int y, int trackPosition) { return false; } // not used
public boolean mousePressed(int x, int y, int trackPosition) {
lastAngle = (float) Math.atan2(x - (height / 2), y - (width / 2));
return false;
}
@Override
public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed, int trackPosition) {
@@ -222,17 +270,18 @@ public class Spinner implements GameObject {
// spin automatically
// http://osu.ppy.sh/wiki/FAQ#Spinners
float angle;
deltaOverflow += delta;
float angleDiff = 0;
if (GameMod.AUTO.isActive()) {
lastAngle = 0;
angle = delta * AUTO_MULTIPLIER;
angleDiff = delta * AUTO_MULTIPLIER;
isSpinning = true;
} else if (GameMod.SPUN_OUT.isActive() || GameMod.AUTOPILOT.isActive()) {
lastAngle = 0;
angle = delta * SPUN_OUT_MULTIPLIER;
angleDiff = delta * SPUN_OUT_MULTIPLIER;
isSpinning = true;
} else {
angle = (float) Math.atan2(mouseY - (height / 2), mouseX - (width / 2));
float angle = (float) Math.atan2(mouseY - (height / 2), mouseX - (width / 2));
// set initial angle to current mouse position to skip first click
if (!isSpinning && (keyPressed || GameMod.RELAX.isActive())) {
@@ -240,35 +289,52 @@ public class Spinner implements GameObject {
isSpinning = true;
return false;
}
angleDiff = angle - lastAngle;
if(Math.abs(angleDiff) > 0.01f){
lastAngle = angle;
}else{
angleDiff = 0;
}
}
// make angleDiff the smallest angle change possible
// (i.e. 1/4 rotation instead of 3/4 rotation)
float angleDiff = angle - lastAngle;
if (angleDiff < -Math.PI)
angleDiff += TWO_PI;
else if (angleDiff > Math.PI)
angleDiff -= TWO_PI;
// spin caused by the cursor
float cursorVelocity = 0;
//may be a problem at higher frame rate due to float point round off
if (isSpinning)
cursorVelocity = Utils.clamp(angleDiff / TWO_PI / delta * 1000, -8f, 8f);
deltaOverflow += delta;
deltaAngleOverflow += angleDiff;
while (deltaOverflow >= DELTA_UPDATE_TIME) {
sumVelocity -= storedVelocities[velocityIndex];
sumVelocity += cursorVelocity;
storedVelocities[velocityIndex++] = cursorVelocity;
velocityIndex %= storedVelocities.length;
// spin caused by the cursor
float deltaAngle = 0;
if (isSpinning){
deltaAngle = deltaAngleOverflow * DELTA_UPDATE_TIME / deltaOverflow;
deltaAngleOverflow -= deltaAngle;
deltaAngle = Utils.clamp(deltaAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF);
}
sumDeltaAngle -= storedDeltaAngle[deltaAngleIndex];
sumDeltaAngle += deltaAngle;
storedDeltaAngle[deltaAngleIndex++] = deltaAngle;
deltaAngleIndex %= storedDeltaAngle.length;
deltaOverflow -= DELTA_UPDATE_TIME;
}
float rotationAngle = sumVelocity / storedVelocities.length * TWO_PI * delta / 1000;
rotate(rotationAngle);
if (Math.abs(rotationAngle) > 0.00001f)
data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER);
float rotationAngle = sumDeltaAngle / maxStoredDeltaAngles;
rotationAngle = Utils.clamp(rotationAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF);
float rotationPerSec = rotationAngle * (1000/DELTA_UPDATE_TIME) / TWO_PI;
lastAngle = angle;
drawnRPM = (int)(Math.abs(rotationPerSec * 60));
rotate(rotationAngle);
if (Math.abs(rotationAngle) > 0.00001f)
data.changeHealth(DELTA_UPDATE_TIME * GameData.HP_DRAIN_MULTIPLIER);
}
//TODO may need to update 1 more time when the spinner ends?
return false;
}
@@ -311,15 +377,38 @@ public class Spinner implements GameObject {
// added one whole rotation...
if (Math.floor(newRotations) > rotations) {
//TODO seems to give 1100 points per spin but also an extra 100 for some spinners
if (newRotations > rotationsNeeded) { // extra rotations
data.changeScore(1000);
SoundController.playSound(SoundEffect.SPINNERBONUS);
} else {
}
data.changeScore(100);
SoundController.playSound(SoundEffect.SPINNERSPIN);
}
/*
//The extra 100 for some spinners (mostly wrong)
if (Math.floor(newRotations + 0.5f) > rotations + 0.5f) {
if (newRotations + 0.5f > rotationsNeeded) { // extra rotations
data.changeScore(100);
SoundController.playSound(SoundEffect.SPINNERSPIN);
}
}
//*/
rotations = newRotations;
}
@Override
public void reset() {
deltaAngleIndex = 0;
sumDeltaAngle = 0;
for(int i=0; i<storedDeltaAngle.length; i++){
storedDeltaAngle[i] = 0;
}
drawRotation = 0;
rotations = 0;
deltaOverflow = 0;
isSpinning = false;
}
}