Implemented saving/loading from checkpoints.
- A checkpoint (track position) can be set in the options screen, and also while playing by pressing "CTRL+S". - A checkpoint can be loaded by pressing "CTRL+L" while playing. This will reset all game data and begin from the checkpoint time; the ranking screen will be skipped upon song completion. Other changes: - Don't draw grade if no objects have been processed (previously defaulted to GRADE_D). - Calculate slider start/end angles based on a step difference (previously an arbitrary 0.01 difference). - Always end a slider curve base on the last control point. Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
parent
2ed8e66bbf
commit
50fb71e353
|
@ -550,13 +550,16 @@ public class GameScore {
|
||||||
drawSymbolString(String.format("%dx", combo), 10, height - 10 - symbolHeight, 1.0f, false);
|
drawSymbolString(String.format("%dx", combo), 10, height - 10 - symbolHeight, 1.0f, false);
|
||||||
} else {
|
} else {
|
||||||
// grade
|
// grade
|
||||||
Image grade = gradesSmall[getGrade()];
|
int grade = getGrade();
|
||||||
float gradeScale = symbolHeight * 0.75f / grade.getHeight();
|
if (grade != -1) {
|
||||||
gradesSmall[getGrade()].getScaledCopy(gradeScale).draw(
|
Image gradeImage = gradesSmall[grade];
|
||||||
circleX - grade.getWidth(), symbolHeight
|
float gradeScale = symbolHeight * 0.75f / gradeImage.getHeight();
|
||||||
|
gradeImage.getScaledCopy(gradeScale).draw(
|
||||||
|
circleX - gradeImage.getWidth(), symbolHeight
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws ranking elements: score, results, ranking.
|
* Draws ranking elements: score, results, ranking.
|
||||||
|
@ -566,10 +569,13 @@ public class GameScore {
|
||||||
*/
|
*/
|
||||||
public void drawRankingElements(Graphics g, int width, int height) {
|
public void drawRankingElements(Graphics g, int width, int height) {
|
||||||
// grade
|
// grade
|
||||||
Image grade = gradesLarge[getGrade()];
|
int grade = getGrade();
|
||||||
float gradeScale = (height * 0.5f) / grade.getHeight();
|
if (grade != -1) {
|
||||||
grade = grade.getScaledCopy(gradeScale);
|
Image gradeImage = gradesLarge[grade];
|
||||||
grade.draw(width - grade.getWidth(), height * 0.09f);
|
float gradeScale = (height * 0.5f) / gradeImage.getHeight();
|
||||||
|
gradeImage = gradeImage.getScaledCopy(gradeScale);
|
||||||
|
gradeImage.draw(width - gradeImage.getWidth(), height * 0.09f);
|
||||||
|
}
|
||||||
|
|
||||||
// header & "Ranking" text
|
// header & "Ranking" text
|
||||||
Image rankingTitle = GameImage.RANKING_TITLE.getImage();
|
Image rankingTitle = GameImage.RANKING_TITLE.getImage();
|
||||||
|
@ -719,10 +725,11 @@ public class GameScore {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns (current) letter grade.
|
* Returns (current) letter grade.
|
||||||
|
* If no objects have been processed, -1 will be returned.
|
||||||
*/
|
*/
|
||||||
private int getGrade() {
|
private int getGrade() {
|
||||||
if (objectCount < 1) // avoid division by zero
|
if (objectCount < 1) // avoid division by zero
|
||||||
return GRADE_D;
|
return -1;
|
||||||
|
|
||||||
// TODO: silvers
|
// TODO: silvers
|
||||||
float percent = getScorePercent();
|
float percent = getScorePercent();
|
||||||
|
|
|
@ -227,8 +227,8 @@ public class MusicController {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the position in the current track.
|
* Returns the position in the current track, in ms.
|
||||||
* If no track is playing, 0f will be returned.
|
* If no track is playing, 0 will be returned.
|
||||||
*/
|
*/
|
||||||
public static int getPosition() {
|
public static int getPosition() {
|
||||||
if (isPlaying())
|
if (isPlaying())
|
||||||
|
|
|
@ -160,22 +160,24 @@ public class Slider {
|
||||||
|
|
||||||
// calculate curve points for drawing
|
// calculate curve points for drawing
|
||||||
int N = (int) (1 / step);
|
int N = (int) (1 / step);
|
||||||
this.curveX = new float[N];
|
this.curveX = new float[N + 1];
|
||||||
this.curveY = new float[N];
|
this.curveY = new float[N + 1];
|
||||||
float t = 0f;
|
float t = 0f;
|
||||||
for (int i = 0; i < N; i++, t += step) {
|
for (int i = 0; i < N; i++, t += step) {
|
||||||
float[] c = pointAt(t);
|
float[] c = pointAt(t);
|
||||||
curveX[i] = c[0];
|
curveX[i] = c[0];
|
||||||
curveY[i] = c[1];
|
curveY[i] = c[1];
|
||||||
}
|
}
|
||||||
|
curveX[N] = getX(order - 1);
|
||||||
|
curveY[N] = getY(order - 1);
|
||||||
|
|
||||||
// calculate angles (if needed)
|
// calculate angles (if needed)
|
||||||
if (hitObject.repeat > 1) {
|
if (hitObject.repeat > 1) {
|
||||||
float[] c1 = pointAt(0f);
|
float[] c1 = pointAt(0f);
|
||||||
float[] c2 = pointAt(0.01f);
|
float[] c2 = pointAt(step);
|
||||||
startAngle = (float) (Math.atan2(c2[1] - c1[1], c2[0] - c1[0]) * 180 / Math.PI);
|
startAngle = (float) (Math.atan2(c2[1] - c1[1], c2[0] - c1[0]) * 180 / Math.PI);
|
||||||
c1 = pointAt(1f);
|
c1 = pointAt(1f);
|
||||||
c2 = pointAt(0.99f);
|
c2 = pointAt(1f - step);
|
||||||
endAngle = (float) (Math.atan2(c2[1] - c1[1], c2[0] - c1[0]) * 180 / Math.PI);
|
endAngle = (float) (Math.atan2(c2[1] - c1[1], c2[0] - c1[0]) * 180 / Math.PI);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -181,6 +181,11 @@ public class Game extends BasicGameState {
|
||||||
*/
|
*/
|
||||||
private Image playfield;
|
private Image playfield;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a checkpoint has been loaded during this game.
|
||||||
|
*/
|
||||||
|
private boolean checkpointLoaded = false;
|
||||||
|
|
||||||
// game-related variables
|
// game-related variables
|
||||||
private GameContainer container;
|
private GameContainer container;
|
||||||
private StateBasedGame game;
|
private StateBasedGame game;
|
||||||
|
@ -233,6 +238,16 @@ public class Game extends BasicGameState {
|
||||||
if (pauseTime > -1) // returning from pause screen
|
if (pauseTime > -1) // returning from pause screen
|
||||||
trackPosition = pauseTime;
|
trackPosition = pauseTime;
|
||||||
|
|
||||||
|
// checkpoint
|
||||||
|
if (checkpointLoaded) {
|
||||||
|
String checkpointText = "~ Playing from checkpoint. ~";
|
||||||
|
Utils.FONT_MEDIUM.drawString(
|
||||||
|
(container.getWidth() - Utils.FONT_MEDIUM.getWidth(checkpointText)) / 2,
|
||||||
|
container.getHeight() - 15 - Utils.FONT_MEDIUM.getLineHeight(),
|
||||||
|
checkpointText, Color.white
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// break periods
|
// break periods
|
||||||
if (osu.breaks != null && breakIndex < osu.breaks.size()) {
|
if (osu.breaks != null && breakIndex < osu.breaks.size()) {
|
||||||
if (breakTime > 0) {
|
if (breakTime > 0) {
|
||||||
|
@ -430,7 +445,9 @@ public class Game extends BasicGameState {
|
||||||
|
|
||||||
// map complete!
|
// map complete!
|
||||||
if (objectIndex >= osu.objects.length) {
|
if (objectIndex >= osu.objects.length) {
|
||||||
game.enterState(Opsu.STATE_GAMERANKING, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
// if checkpoint used, don't show the ranking screen
|
||||||
|
int state = (checkpointLoaded) ? Opsu.STATE_SONGMENU : Opsu.STATE_GAMERANKING;
|
||||||
|
game.enterState(state, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -544,10 +561,11 @@ public class Game extends BasicGameState {
|
||||||
game.enterState(Opsu.STATE_GAMEPAUSEMENU, new EmptyTransition(), new FadeInTransition(Color.black));
|
game.enterState(Opsu.STATE_GAMEPAUSEMENU, new EmptyTransition(), new FadeInTransition(Color.black));
|
||||||
break;
|
break;
|
||||||
case Input.KEY_SPACE:
|
case Input.KEY_SPACE:
|
||||||
// skip
|
// skip intro
|
||||||
skipIntro();
|
skipIntro();
|
||||||
break;
|
break;
|
||||||
case Input.KEY_R:
|
case Input.KEY_R:
|
||||||
|
// restart
|
||||||
if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) {
|
if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) {
|
||||||
try {
|
try {
|
||||||
restart = RESTART_MANUAL;
|
restart = RESTART_MANUAL;
|
||||||
|
@ -558,6 +576,44 @@ public class Game extends BasicGameState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case Input.KEY_S:
|
||||||
|
// save checkpoint
|
||||||
|
if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) {
|
||||||
|
if (isLeadIn())
|
||||||
|
break;
|
||||||
|
|
||||||
|
int position = (pauseTime > -1) ? pauseTime : MusicController.getPosition();
|
||||||
|
if (Options.setCheckpoint(position / 1000))
|
||||||
|
SoundController.playSound(SoundController.SOUND_MENUCLICK);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Input.KEY_L:
|
||||||
|
// load checkpoint
|
||||||
|
if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) {
|
||||||
|
int checkpoint = Options.getCheckpoint();
|
||||||
|
if (checkpoint == 0 || checkpoint > MusicController.getTrackLength())
|
||||||
|
break; // invalid checkpoint
|
||||||
|
try {
|
||||||
|
restart = RESTART_MANUAL;
|
||||||
|
enter(container, game);
|
||||||
|
checkpointLoaded = true;
|
||||||
|
if (isLeadIn()) {
|
||||||
|
leadInTime = 0;
|
||||||
|
MusicController.resume();
|
||||||
|
}
|
||||||
|
SoundController.playSound(SoundController.SOUND_MENUHIT);
|
||||||
|
|
||||||
|
// skip to checkpoint
|
||||||
|
MusicController.setPosition(checkpoint);
|
||||||
|
while (objectIndex < osu.objects.length &&
|
||||||
|
osu.objects[objectIndex++].time <= MusicController.getPosition())
|
||||||
|
;
|
||||||
|
objectIndex--;
|
||||||
|
} catch (SlickException e) {
|
||||||
|
Log.error("Failed to load checkpoint.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
case Input.KEY_Z:
|
case Input.KEY_Z:
|
||||||
// left-click
|
// left-click
|
||||||
if (!Keyboard.isRepeatEvent())
|
if (!Keyboard.isRepeatEvent())
|
||||||
|
@ -690,6 +746,7 @@ public class Game extends BasicGameState {
|
||||||
countdown1Sound = false;
|
countdown1Sound = false;
|
||||||
countdown2Sound = false;
|
countdown2Sound = false;
|
||||||
countdownGoSound = false;
|
countdownGoSound = false;
|
||||||
|
checkpointLoaded = false;
|
||||||
|
|
||||||
// load the first timingPoint
|
// load the first timingPoint
|
||||||
if (!osu.timingPoints.isEmpty()) {
|
if (!osu.timingPoints.isEmpty()) {
|
||||||
|
|
|
@ -32,6 +32,7 @@ import java.io.IOException;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.newdawn.slick.Color;
|
import org.newdawn.slick.Color;
|
||||||
import org.newdawn.slick.GameContainer;
|
import org.newdawn.slick.GameContainer;
|
||||||
|
@ -140,7 +141,8 @@ public class Options extends BasicGameState {
|
||||||
FIXED_HP,
|
FIXED_HP,
|
||||||
FIXED_AR,
|
FIXED_AR,
|
||||||
FIXED_OD,
|
FIXED_OD,
|
||||||
LOAD_VERBOSE;
|
LOAD_VERBOSE,
|
||||||
|
CHECKPOINT;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -216,7 +218,8 @@ public class Options extends BasicGameState {
|
||||||
GameOption.FIXED_CS,
|
GameOption.FIXED_CS,
|
||||||
GameOption.FIXED_HP,
|
GameOption.FIXED_HP,
|
||||||
GameOption.FIXED_AR,
|
GameOption.FIXED_AR,
|
||||||
GameOption.FIXED_OD
|
GameOption.FIXED_OD,
|
||||||
|
GameOption.CHECKPOINT
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -353,6 +356,11 @@ public class Options extends BasicGameState {
|
||||||
*/
|
*/
|
||||||
private static boolean loadVerbose = true;
|
private static boolean loadVerbose = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track checkpoint time, in seconds.
|
||||||
|
*/
|
||||||
|
private static int checkpoint = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Game option coordinate modifiers (for drawing).
|
* Game option coordinate modifiers (for drawing).
|
||||||
*/
|
*/
|
||||||
|
@ -651,6 +659,9 @@ public class Options extends BasicGameState {
|
||||||
case FIXED_OD:
|
case FIXED_OD:
|
||||||
fixedOD = getBoundedValue(fixedOD, diff / 10f, 0f, 10f);
|
fixedOD = getBoundedValue(fixedOD, diff / 10f, 0f, 10f);
|
||||||
break;
|
break;
|
||||||
|
case CHECKPOINT:
|
||||||
|
checkpoint = getBoundedValue(checkpoint, diff * multiplier, 0, 3599);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -855,6 +866,14 @@ public class Options extends BasicGameState {
|
||||||
"Determines the time window for hit results."
|
"Determines the time window for hit results."
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case CHECKPOINT:
|
||||||
|
drawOption(pos, "Track Checkpoint",
|
||||||
|
(checkpoint == 0) ? "Disabled" : String.format("%02d:%02d",
|
||||||
|
TimeUnit.SECONDS.toMinutes(checkpoint),
|
||||||
|
checkpoint - TimeUnit.MINUTES.toSeconds(TimeUnit.SECONDS.toMinutes(checkpoint))),
|
||||||
|
"Press CTRL+L while playing to load a checkpoint, and CTRL+S to set one."
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1075,6 +1094,25 @@ public class Options extends BasicGameState {
|
||||||
*/
|
*/
|
||||||
public static boolean isLoadVerbose() { return loadVerbose; }
|
public static boolean isLoadVerbose() { return loadVerbose; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the track checkpoint time.
|
||||||
|
* @return the checkpoint time (in ms)
|
||||||
|
*/
|
||||||
|
public static int getCheckpoint() { return checkpoint * 1000; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the track checkpoint time, if within bounds.
|
||||||
|
* @param time the track position (in ms)
|
||||||
|
* @return true if within bounds
|
||||||
|
*/
|
||||||
|
public static boolean setCheckpoint(int time) {
|
||||||
|
if (time >= 0 && time < 3600) {
|
||||||
|
checkpoint = time;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the beatmap directory.
|
* Returns the beatmap directory.
|
||||||
* If invalid, this will attempt to search for the directory,
|
* If invalid, this will attempt to search for the directory,
|
||||||
|
@ -1242,6 +1280,9 @@ public class Options extends BasicGameState {
|
||||||
case "FixedOD":
|
case "FixedOD":
|
||||||
fixedOD = Float.parseFloat(value);
|
fixedOD = Float.parseFloat(value);
|
||||||
break;
|
break;
|
||||||
|
case "Checkpoint":
|
||||||
|
setCheckpoint(Integer.parseInt(value));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -1321,6 +1362,8 @@ public class Options extends BasicGameState {
|
||||||
writer.newLine();
|
writer.newLine();
|
||||||
writer.write(String.format("FixedOD = %.1f", fixedOD));
|
writer.write(String.format("FixedOD = %.1f", fixedOD));
|
||||||
writer.newLine();
|
writer.newLine();
|
||||||
|
writer.write(String.format("Checkpoint = %d", checkpoint));
|
||||||
|
writer.newLine();
|
||||||
writer.close();
|
writer.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.error(String.format("Failed to write to file '%s'.", OPTIONS_FILE.getAbsolutePath()), e);
|
Log.error(String.format("Failed to write to file '%s'.", OPTIONS_FILE.getAbsolutePath()), e);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user