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:
Jeffrey Han 2014-07-08 22:17:48 -04:00
parent 2ed8e66bbf
commit 50fb71e353
5 changed files with 129 additions and 20 deletions

View File

@ -550,11 +550,14 @@ 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
);
}
} }
} }
@ -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();

View File

@ -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())

View File

@ -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);
} }
} }

View File

@ -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()) {

View File

@ -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);