Merge remote-tracking branch 'org/master' into ReplayTest

Conflicts:
	src/itdelatrisu/opsu/objects/HitObject.java
	src/itdelatrisu/opsu/states/Game.java
This commit is contained in:
fd 2015-03-17 23:47:33 -04:00
commit 2877d9bc3d
17 changed files with 553 additions and 198 deletions

BIN
res/alpha.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -133,6 +133,14 @@ public class ErrorHandler {
sb.append(timestamp); sb.append(timestamp);
sb.append('\n'); sb.append('\n');
} }
sb.append("**OS:** ");
sb.append(System.getProperty("os.name"));
sb.append(" (");
sb.append(System.getProperty("os.arch"));
sb.append(")\n");
sb.append("**JRE:** ");
sb.append(System.getProperty("java.version"));
sb.append('\n');
if (error != null) { if (error != null) {
sb.append("**Error:** `"); sb.append("**Error:** `");
sb.append(error); sb.append(error);

View File

@ -917,22 +917,22 @@ public class GameData {
* @param hit100 the number of 100s * @param hit100 the number of 100s
* @param hit50 the number of 50s * @param hit50 the number of 50s
* @param miss the number of misses * @param miss the number of misses
* @param silver whether or not a silver SS/S should be awarded (if applicable)
* @return the current Grade * @return the current Grade
*/ */
public static Grade getGrade(int hit300, int hit100, int hit50, int miss) { public static Grade getGrade(int hit300, int hit100, int hit50, int miss, boolean silver) {
int objectCount = hit300 + hit100 + hit50 + miss; int objectCount = hit300 + hit100 + hit50 + miss;
if (objectCount < 1) // avoid division by zero if (objectCount < 1) // avoid division by zero
return Grade.NULL; return Grade.NULL;
// TODO: silvers
float percent = getScorePercent(hit300, hit100, hit50, miss); float percent = getScorePercent(hit300, hit100, hit50, miss);
float hit300ratio = hit300 * 100f / objectCount; float hit300ratio = hit300 * 100f / objectCount;
float hit50ratio = hit50 * 100f / objectCount; float hit50ratio = hit50 * 100f / objectCount;
boolean noMiss = (miss == 0); boolean noMiss = (miss == 0);
if (percent >= 100f) if (percent >= 100f)
return Grade.SS; return (silver) ? Grade.SSH : Grade.SS;
else if (hit300ratio >= 90f && hit50ratio < 1.0f && noMiss) else if (hit300ratio >= 90f && hit50ratio < 1.0f && noMiss)
return Grade.S; return (silver) ? Grade.SH : Grade.S;
else if ((hit300ratio >= 80f && noMiss) || hit300ratio >= 90f) else if ((hit300ratio >= 80f && noMiss) || hit300ratio >= 90f)
return Grade.A; return Grade.A;
else if ((hit300ratio >= 70f && noMiss) || hit300ratio >= 80f) else if ((hit300ratio >= 70f && noMiss) || hit300ratio >= 80f)
@ -950,7 +950,8 @@ public class GameData {
private Grade getGrade() { private Grade getGrade() {
return getGrade( return getGrade(
hitResultCount[HIT_300], hitResultCount[HIT_100], hitResultCount[HIT_300], hitResultCount[HIT_100],
hitResultCount[HIT_50], hitResultCount[HIT_MISS] hitResultCount[HIT_50], hitResultCount[HIT_MISS],
(GameMod.HIDDEN.isActive() || GameMod.FLASHLIGHT.isActive())
); );
} }
@ -1031,6 +1032,11 @@ public class GameData {
} }
} }
/**
* Returns the current combo streak.
*/
public int getComboStreak() { return combo; }
/** /**
* Increases the combo streak by one. * Increases the combo streak by one.
*/ */

View File

@ -327,7 +327,9 @@ public enum GameImage {
protected Image process_sub(Image img, int w, int h) { protected Image process_sub(Image img, int w, int h) {
return REPOSITORY.process_sub(img, w, h); return REPOSITORY.process_sub(img, w, h);
} }
}; },
// TODO: ensure this image hasn't been modified (checksum?)
ALPHA_MAP ("alpha", "png", false, false);
/** Image file types. */ /** Image file types. */
private static final byte private static final byte

View File

@ -47,11 +47,11 @@ public enum GameMod {
// "Nightcore", "uguuuuuuuu"), // "Nightcore", "uguuuuuuuu"),
HIDDEN (Category.HARD, 3, GameImage.MOD_HIDDEN, "HD", 8, Input.KEY_F, 1.06f, false, HIDDEN (Category.HARD, 3, GameImage.MOD_HIDDEN, "HD", 8, Input.KEY_F, 1.06f, false,
"Hidden", "Play with no approach circles and fading notes for a slight score advantage."), "Hidden", "Play with no approach circles and fading notes for a slight score advantage."),
FLASHLIGHT (Category.HARD, 4, GameImage.MOD_FLASHLIGHT, "FL", 1024, Input.KEY_G, 1.12f, false, FLASHLIGHT (Category.HARD, 4, GameImage.MOD_FLASHLIGHT, "FL", 1024, Input.KEY_G, 1.12f,
"Flashlight", "Restricted view area."), "Flashlight", "Restricted view area."),
RELAX (Category.SPECIAL, 0, GameImage.MOD_RELAX, "RL", 128, Input.KEY_Z, 0f, RELAX (Category.SPECIAL, 0, GameImage.MOD_RELAX, "RL", 128, Input.KEY_Z, 0f,
"Relax", "You don't need to click.\nGive your clicking/tapping finger a break from the heat of things.\n**UNRANKED**"), "Relax", "You don't need to click.\nGive your clicking/tapping finger a break from the heat of things.\n**UNRANKED**"),
AUTOPILOT (Category.SPECIAL, 1, GameImage.MOD_AUTOPILOT, "AP", 8192, Input.KEY_X, 0f, false, AUTOPILOT (Category.SPECIAL, 1, GameImage.MOD_AUTOPILOT, "AP", 8192, Input.KEY_X, 0f,
"Relax2", "Automatic cursor movement - just follow the rhythm.\n**UNRANKED**"), "Relax2", "Automatic cursor movement - just follow the rhythm.\n**UNRANKED**"),
SPUN_OUT (Category.SPECIAL, 2, GameImage.MOD_SPUN_OUT, "SO", 4096, Input.KEY_V, 0.9f, SPUN_OUT (Category.SPECIAL, 2, GameImage.MOD_SPUN_OUT, "SO", 4096, Input.KEY_V, 0.9f,
"SpunOut", "Spinners will be automatically completed."), "SpunOut", "Spinners will be automatically completed."),
@ -355,17 +355,16 @@ public enum GameMod {
scoreMultiplier = -1f; scoreMultiplier = -1f;
if (checkInverse) { if (checkInverse) {
boolean b = (this == SUDDEN_DEATH || this == NO_FAIL || this == RELAX || this == AUTOPILOT);
if (AUTO.isActive()) { if (AUTO.isActive()) {
if (this == AUTO) { if (this == AUTO) {
SPUN_OUT.active = false; SPUN_OUT.active = false;
SUDDEN_DEATH.active = false; SUDDEN_DEATH.active = false;
RELAX.active = false; RELAX.active = false;
AUTOPILOT.active = false; AUTOPILOT.active = false;
} else if (b) } else if (this == SPUN_OUT || this == SUDDEN_DEATH || this == RELAX || this == AUTOPILOT)
this.active = false; this.active = false;
} }
if (active && b) { if (active && (this == SUDDEN_DEATH || this == NO_FAIL || this == RELAX || this == AUTOPILOT)) {
SUDDEN_DEATH.active = false; SUDDEN_DEATH.active = false;
NO_FAIL.active = false; NO_FAIL.active = false;
RELAX.active = false; RELAX.active = false;
@ -384,6 +383,12 @@ public enum GameMod {
else else
EASY.active = false; EASY.active = false;
} }
if (HALF_TIME.isActive() && DOUBLE_TIME.isActive()) {
if (this == HALF_TIME)
DOUBLE_TIME.active = false;
else
HALF_TIME.active = false;
}
} }
} }

View File

@ -187,11 +187,12 @@ public class ScoreData implements Comparable<ScoreData> {
/** /**
* Returns letter grade based on score data, * Returns letter grade based on score data,
* or Grade.NULL if no objects have been processed. * or Grade.NULL if no objects have been processed.
* @see GameData#getGrade(int, int, int, int) * @see GameData#getGrade(int, int, int, int, boolean)
*/ */
public Grade getGrade() { public Grade getGrade() {
if (grade == null) if (grade == null)
grade = GameData.getGrade(hit300, hit100, hit50, miss); grade = GameData.getGrade(hit300, hit100, hit50, miss,
((mods & GameMod.HIDDEN.getBit()) > 0 || (mods & GameMod.FLASHLIGHT.getBit()) > 0));
return grade; return grade;
} }

View File

@ -173,6 +173,7 @@ public class UI {
public static void enter() { public static void enter() {
backButton.resetHover(); backButton.resetHover();
resetBarNotification(); resetBarNotification();
resetCursorLocations();
resetTooltip(); resetTooltip();
} }
@ -371,10 +372,16 @@ public class UI {
GameImage.CURSOR_MIDDLE.destroySkinImage(); GameImage.CURSOR_MIDDLE.destroySkinImage();
GameImage.CURSOR_TRAIL.destroySkinImage(); GameImage.CURSOR_TRAIL.destroySkinImage();
cursorAngle = 0f; cursorAngle = 0f;
GameImage.CURSOR.getImage().setRotation(0f);
}
/**
* Resets all cursor location data.
*/
private static void resetCursorLocations() {
lastX = lastY = -1; lastX = lastY = -1;
cursorX.clear(); cursorX.clear();
cursorY.clear(); cursorY.clear();
GameImage.CURSOR.getImage().setRotation(0f);
} }
/** /**

View File

@ -24,8 +24,14 @@ import itdelatrisu.opsu.OsuFile;
import itdelatrisu.opsu.OsuParser; import itdelatrisu.opsu.OsuParser;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.nio.IntBuffer; import java.nio.IntBuffer;
import java.util.Map;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import org.lwjgl.BufferUtils; import org.lwjgl.BufferUtils;
import org.lwjgl.openal.AL; import org.lwjgl.openal.AL;
@ -35,6 +41,7 @@ import org.newdawn.slick.MusicListener;
import org.newdawn.slick.SlickException; import org.newdawn.slick.SlickException;
import org.newdawn.slick.openal.Audio; import org.newdawn.slick.openal.Audio;
import org.newdawn.slick.openal.SoundStore; import org.newdawn.slick.openal.SoundStore;
import org.tritonus.share.sampled.file.TAudioFileFormat;
/** /**
* Controller for all music. * Controller for all music.
@ -252,6 +259,30 @@ public class MusicController {
return (trackExists() && position >= 0 && player.setPosition(position / 1000f)); return (trackExists() && position >= 0 && player.setPosition(position / 1000f));
} }
/**
* Returns the duration of the current track, in milliseconds.
* Currently only works for MP3s.
* @return the duration, or -1 if no track exists, else the {@code endTime}
* field of the OsuFile loaded
* @author Tom Brito (http://stackoverflow.com/a/3056161)
*/
public static int getDuration() {
if (!trackExists() || lastOsu == null)
return -1;
if (lastOsu.audioFilename.getName().endsWith(".mp3")) {
try {
AudioFileFormat fileFormat = AudioSystem.getAudioFileFormat(lastOsu.audioFilename);
if (fileFormat instanceof TAudioFileFormat) {
Map<?, ?> properties = ((TAudioFileFormat) fileFormat).properties();
Long microseconds = (Long) properties.get("duration");
return (int) (microseconds / 1000);
}
} catch (UnsupportedAudioFileException | IOException e) {}
}
return lastOsu.endTime;
}
/** /**
* Plays the current track. * Plays the current track.
* @param loop whether or not to loop the track * @param loop whether or not to loop the track

View File

@ -16,7 +16,7 @@
* along with opsu!. If not, see <http://www.gnu.org/licenses/>. * along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/ */
package itdelatrisu.opsu; package itdelatrisu.opsu.io;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.File; import java.io.File;

View File

@ -16,7 +16,7 @@
* along with opsu!. If not, see <http://www.gnu.org/licenses/>. * along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/ */
package itdelatrisu.opsu; package itdelatrisu.opsu.io;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.File; import java.io.File;

View File

@ -178,4 +178,10 @@ public class Circle implements HitObject {
return false; return false;
} }
@Override
public float[] getPointAt(int trackPosition) { return new float[] { x, y }; }
@Override
public int getEndTime() { return hitObject.getTime(); }
} }

View File

@ -51,4 +51,17 @@ public interface HitObject {
* @return true if a hit result was processed * @return true if a hit result was processed
*/ */
public boolean mousePressed(int x, int y, int trackPosition); public boolean mousePressed(int x, int y, int trackPosition);
/**
* Returns the coordinates of the hit object at a given track position.
* @param trackPosition the track position
* @return the [x,y] coordinates
*/
public float[] getPointAt(int trackPosition);
/**
* Returns the end time of the hit object.
* @return the end time, in milliseconds
*/
public int getEndTime();
} }

View File

@ -92,6 +92,9 @@ public class Slider implements HitObject {
/** Number of ticks hit and tick intervals so far. */ /** Number of ticks hit and tick intervals so far. */
private int ticksHit = 0, tickIntervals = 1; private int ticksHit = 0, tickIntervals = 1;
/** Container dimensions. */
private static int containerWidth, containerHeight;
/** /**
* Initializes the Slider data type with images and dimensions. * Initializes the Slider data type with images and dimensions.
* @param container the game container * @param container the game container
@ -99,6 +102,9 @@ public class Slider implements HitObject {
* @param osu the associated OsuFile object * @param osu the associated OsuFile object
*/ */
public static void init(GameContainer container, float circleSize, OsuFile osu) { public static void init(GameContainer container, float circleSize, OsuFile osu) {
containerWidth = container.getWidth();
containerHeight = container.getHeight();
int diameter = (int) (104 - (circleSize * 8)); int diameter = (int) (104 - (circleSize * 8));
diameter = (int) (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480) diameter = (int) (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480)
@ -188,7 +194,7 @@ public class Slider implements HitObject {
color.a = oldAlpha; color.a = oldAlpha;
// repeats // repeats
for(int tcurRepeat = currentRepeats; tcurRepeat<=currentRepeats+1; tcurRepeat++){ for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) {
if (hitObject.getRepeatCount() - 1 > tcurRepeat) { if (hitObject.getRepeatCount() - 1 > tcurRepeat) {
Image arrow = GameImage.REVERSEARROW.getImage(); Image arrow = GameImage.REVERSEARROW.getImage();
if (tcurRepeat != currentRepeats) { if (tcurRepeat != currentRepeats) {
@ -233,8 +239,18 @@ public class Slider implements HitObject {
sliderBallFrame.drawCentered(c[0], c[1]); sliderBallFrame.drawCentered(c[0], c[1]);
// follow circle // follow circle
if (followCircleActive) if (followCircleActive) {
GameImage.SLIDER_FOLLOWCIRCLE.getImage().drawCentered(c[0], c[1]); GameImage.SLIDER_FOLLOWCIRCLE.getImage().drawCentered(c[0], c[1]);
// "flashlight" mod: dim the screen
if (GameMod.FLASHLIGHT.isActive()) {
float oldAlphaBlack = Utils.COLOR_BLACK_ALPHA.a;
Utils.COLOR_BLACK_ALPHA.a = 0.75f;
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.fillRect(0, 0, containerWidth, containerHeight);
Utils.COLOR_BLACK_ALPHA.a = oldAlphaBlack;
}
}
} }
} }
@ -441,6 +457,24 @@ public class Slider implements HitObject {
return false; return false;
} }
@Override
public float[] getPointAt(int trackPosition) {
if (trackPosition <= hitObject.getTime())
return new float[] { x, y };
else if (trackPosition >= hitObject.getTime() + sliderTimeTotal) {
if (hitObject.getRepeatCount() % 2 == 0)
return new float[] { x, y };
else {
int lastIndex = hitObject.getSliderX().length;
return new float[] { curve.getX(lastIndex), curve.getY(lastIndex) };
}
} else
return curve.pointAt(getT(trackPosition, false));
}
@Override
public int getEndTime() { return hitObject.getTime() + (int) sliderTimeTotal; }
/** /**
* Returns the t value based on the given track position. * Returns the t value based on the given track position.
* @param trackPosition the current track position * @param trackPosition the current track position

View File

@ -50,8 +50,15 @@ public class Spinner implements HitObject {
/** The amount of time, in milliseconds, to fade in the spinner. */ /** The amount of time, in milliseconds, to fade in the spinner. */
private static final int FADE_IN_TIME = 500; private static final int FADE_IN_TIME = 500;
/** Angle mod multipliers: "auto" (477rpm), "spun out" (287rpm) */
private static final float
AUTO_MULTIPLIER = 1 / 20f, // angle = 477/60f * delta/1000f * TWO_PI;
SPUN_OUT_MULTIPLIER = 1 / 33.25f; // angle = 287/60f * delta/1000f * TWO_PI;
/** PI constants. */ /** PI constants. */
private static final float TWO_PI = (float) (Math.PI * 2); private static final float
TWO_PI = (float) (Math.PI * 2),
HALF_PI = (float) (Math.PI / 2);
/** The associated OsuHitObject. */ /** The associated OsuHitObject. */
private OsuHitObject hitObject; private OsuHitObject hitObject;
@ -174,8 +181,7 @@ public class Spinner implements HitObject {
// TODO: verify ratios // TODO: verify ratios
int result; int result;
float ratio = rotations / rotationsNeeded; float ratio = rotations / rotationsNeeded;
if (ratio >= 1.0f || if (ratio >= 1.0f || GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive() || GameMod.SPUN_OUT.isActive()) {
GameMod.AUTO.isActive() || GameMod.SPUN_OUT.isActive()) {
result = GameData.HIT_300; result = GameData.HIT_300;
SoundController.playSound(SoundEffect.SPINNEROSU); SoundController.playSound(SoundEffect.SPINNEROSU);
} else if (ratio >= 0.9f) } else if (ratio >= 0.9f)
@ -210,14 +216,12 @@ public class Spinner implements HitObject {
// http://osu.ppy.sh/wiki/FAQ#Spinners // http://osu.ppy.sh/wiki/FAQ#Spinners
float angle; float angle;
if (GameMod.AUTO.isActive()) { if (GameMod.AUTO.isActive()) {
// "auto" mod (fast: 477rpm)
lastAngle = 0; lastAngle = 0;
angle = delta / 20f; // angle = 477/60f * delta/1000f * TWO_PI; angle = delta * AUTO_MULTIPLIER;
isSpinning = true; isSpinning = true;
} else if (GameMod.SPUN_OUT.isActive()) { } else if (GameMod.SPUN_OUT.isActive() || GameMod.AUTOPILOT.isActive()) {
// "spun out" mod (slow: 287rpm)
lastAngle = 0; lastAngle = 0;
angle = delta / 33.25f; // angle = 287/60f * delta/1000f * TWO_PI; angle = delta * SPUN_OUT_MULTIPLIER;
isSpinning = true; isSpinning = true;
} else { } else {
angle = (float) Math.atan2(mouseY - (height / 2), mouseX - (width / 2)); angle = (float) Math.atan2(mouseY - (height / 2), mouseX - (width / 2));
@ -260,6 +264,31 @@ public class Spinner implements HitObject {
return false; return false;
} }
@Override
public float[] getPointAt(int trackPosition) {
// get spinner time
int timeDiff;
float x = hitObject.getScaledX(), y = hitObject.getScaledY();
if (trackPosition <= hitObject.getTime())
timeDiff = 0;
else if (trackPosition >= hitObject.getEndTime())
timeDiff = hitObject.getEndTime() - hitObject.getTime();
else
timeDiff = trackPosition - hitObject.getTime();
// calculate point
float multiplier = (GameMod.AUTO.isActive()) ? AUTO_MULTIPLIER : SPUN_OUT_MULTIPLIER;
float angle = (timeDiff * multiplier) - HALF_PI;
final float r = height / 10f;
return new float[] {
(float) (x + r * Math.cos(angle)),
(float) (y + r * Math.sin(angle))
};
}
@Override
public int getEndTime() { return hitObject.getEndTime(); }
/** /**
* Rotates the spinner by an angle. * Rotates the spinner by an angle.
* @param angle the angle to rotate (in radians) * @param angle the angle to rotate (in radians)

View File

@ -20,9 +20,9 @@ package itdelatrisu.opsu.replay;
import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OsuReader;
import itdelatrisu.opsu.OsuWriter;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.io.OsuReader;
import itdelatrisu.opsu.io.OsuWriter;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;

View File

@ -50,7 +50,6 @@ import java.io.FileNotFoundException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Stack; import java.util.Stack;
import java.util.concurrent.TimeUnit;
import org.lwjgl.input.Keyboard; import org.lwjgl.input.Keyboard;
import org.lwjgl.opengl.Display; import org.lwjgl.opengl.Display;
@ -189,6 +188,21 @@ public class Game extends BasicGameState {
/** The list of current replay frames (for recording replays). */ /** The list of current replay frames (for recording replays). */
private LinkedList<ReplayFrame> replayFrames; private LinkedList<ReplayFrame> replayFrames;
/** The offscreen image rendered to. */
private Image offscreen;
/** The offscreen graphics. */
private Graphics gOffscreen;
/** The current flashlight area radius. */
private int flashlightRadius;
/** The cursor coordinates using the "auto" or "relax" mods. */
private int autoMouseX = 0, autoMouseY = 0;
/** Whether or not the cursor should be pressed using the "auto" mod. */
private boolean autoMousePressed;
// game-related variables // game-related variables
private GameContainer container; private GameContainer container;
private StateBasedGame game; private StateBasedGame game;
@ -209,6 +223,11 @@ public class Game extends BasicGameState {
int width = container.getWidth(); int width = container.getWidth();
int height = container.getHeight(); int height = container.getHeight();
// create offscreen graphics
offscreen = new Image(width, height);
gOffscreen = offscreen.getGraphics();
gOffscreen.setBackground(Color.black);
// create the associated GameData object // create the associated GameData object
data = new GameData(width, height); data = new GameData(width, height);
} }
@ -218,9 +237,15 @@ public class Game extends BasicGameState {
throws SlickException { throws SlickException {
int width = container.getWidth(); int width = container.getWidth();
int height = container.getHeight(); int height = container.getHeight();
g.setBackground(Color.black);
// "flashlight" mod: initialize offscreen graphics
if (GameMod.FLASHLIGHT.isActive()) {
gOffscreen.clear();
Graphics.setCurrent(gOffscreen);
}
// background // background
g.setBackground(Color.black);
float dimLevel = Options.getBackgroundDim(); float dimLevel = Options.getBackgroundDim();
if (Options.isDefaultPlayfieldForced() || !osu.drawBG(width, height, dimLevel, false)) { if (Options.isDefaultPlayfieldForced() || !osu.drawBG(width, height, dimLevel, false)) {
Image playfield = GameImage.PLAYFIELD.getImage(); Image playfield = GameImage.PLAYFIELD.getImage();
@ -229,6 +254,9 @@ public class Game extends BasicGameState {
playfield.setAlpha(1f); playfield.setAlpha(1f);
} }
if (GameMod.FLASHLIGHT.isActive())
Graphics.setCurrent(g);
int trackPosition = MusicController.getPosition(); int trackPosition = MusicController.getPosition();
if (pauseTime > -1) // returning from pause screen if (pauseTime > -1) // returning from pause screen
trackPosition = pauseTime; trackPosition = pauseTime;
@ -237,25 +265,112 @@ public class Game extends BasicGameState {
int firstObjectTime = osu.objects[0].getTime(); int firstObjectTime = osu.objects[0].getTime();
int timeDiff = firstObjectTime - trackPosition; int timeDiff = firstObjectTime - trackPosition;
// checkpoint // "auto" and "autopilot" mods: move cursor automatically
if (checkpointLoaded) { // TODO: this should really be in update(), not render()
int checkpoint = Options.getCheckpoint(); autoMouseX = width / 2;
String checkpointText = String.format( autoMouseY = height / 2;
"Playing from checkpoint at %02d:%02d.", autoMousePressed = false;
TimeUnit.MILLISECONDS.toMinutes(checkpoint), if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
TimeUnit.MILLISECONDS.toSeconds(checkpoint) - float[] autoXY = null;
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(checkpoint)) if (isLeadIn()) {
); // lead-in
Utils.FONT_MEDIUM.drawString( float progress = Math.max((float) (leadInTime - osu.audioLeadIn) / approachTime, 0f);
(width - Utils.FONT_MEDIUM.getWidth(checkpointText)) / 2, autoMouseY = (int) (height / (2f - progress));
height - 15 - Utils.FONT_MEDIUM.getLineHeight(), } else if (objectIndex == 0 && trackPosition < firstObjectTime) {
checkpointText, Color.white // before first object
); timeDiff = firstObjectTime - trackPosition;
if (timeDiff < approachTime) {
float[] xy = hitObjects[0].getPointAt(trackPosition);
autoXY = getPointAt(autoMouseX, autoMouseY, xy[0], xy[1], 1f - ((float) timeDiff / approachTime));
}
} else if (objectIndex < osu.objects.length) {
// normal object
int objectTime = osu.objects[objectIndex].getTime();
if (trackPosition < objectTime) {
float[] xyStart = hitObjects[objectIndex - 1].getPointAt(trackPosition);
int startTime = hitObjects[objectIndex - 1].getEndTime();
if (osu.breaks != null && breakIndex < osu.breaks.size()) {
// starting a break: keep cursor at previous hit object position
if (breakTime > 0 || objectTime > osu.breaks.get(breakIndex))
autoXY = xyStart;
// after a break ends: move startTime to break end time
else if (breakIndex > 1) {
int lastBreakEndTime = osu.breaks.get(breakIndex - 1);
if (objectTime > lastBreakEndTime && startTime < lastBreakEndTime)
startTime = lastBreakEndTime;
}
}
if (autoXY == null) {
float[] xyEnd = hitObjects[objectIndex].getPointAt(trackPosition);
int totalTime = objectTime - startTime;
autoXY = getPointAt(xyStart[0], xyStart[1], xyEnd[0], xyEnd[1], (float) (trackPosition - startTime) / totalTime);
// hit circles: show a mouse press
int offset300 = hitResultOffset[GameData.HIT_300];
if ((osu.objects[objectIndex].isCircle() && objectTime - trackPosition < offset300) ||
(osu.objects[objectIndex - 1].isCircle() && trackPosition - osu.objects[objectIndex - 1].getTime() < offset300))
autoMousePressed = true;
}
} else {
autoXY = hitObjects[objectIndex].getPointAt(trackPosition);
autoMousePressed = true;
}
} else {
// last object
autoXY = hitObjects[objectIndex - 1].getPointAt(trackPosition);
}
// set mouse coordinates
if (autoXY != null) {
autoMouseX = (int) autoXY[0];
autoMouseY = (int) autoXY[1];
}
}
// "flashlight" mod: restricted view of hit objects around cursor
if (GameMod.FLASHLIGHT.isActive()) {
// render hit objects offscreen
Graphics.setCurrent(gOffscreen);
int trackPos = (isLeadIn()) ? (leadInTime - Options.getMusicOffset()) * -1 : trackPosition;
drawHitObjects(gOffscreen, trackPos);
// restore original graphics context
gOffscreen.flush();
Graphics.setCurrent(g);
// draw alpha map around cursor
g.setDrawMode(Graphics.MODE_ALPHA_MAP);
g.clearAlphaMap();
int mouseX, mouseY;
if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) {
mouseX = pausedMouseX;
mouseY = pausedMouseY;
} else if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
mouseX = autoMouseX;
mouseY = autoMouseY;
} else if (isReplay) {
mouseX = replayX;
mouseY = replayY;
} else {
mouseX = input.getMouseX();
mouseY = input.getMouseY();
}
int alphaRadius = flashlightRadius * 256 / 215;
int alphaX = mouseX - alphaRadius / 2;
int alphaY = mouseY - alphaRadius / 2;
GameImage.ALPHA_MAP.getImage().draw(alphaX, alphaY, alphaRadius, alphaRadius);
// blend offscreen image
g.setDrawMode(Graphics.MODE_ALPHA_BLEND);
g.setClip(alphaX, alphaY, alphaRadius, alphaRadius);
g.drawImage(offscreen, 0, 0);
g.clearClip();
g.setDrawMode(Graphics.MODE_NORMAL);
} }
// break periods // break periods
if (osu.breaks != null && breakIndex < osu.breaks.size()) { if (osu.breaks != null && breakIndex < osu.breaks.size() && breakTime > 0) {
if (breakTime > 0) {
int endTime = osu.breaks.get(breakIndex); int endTime = osu.breaks.get(breakIndex);
int breakLength = endTime - breakTime; int breakLength = endTime - breakTime;
@ -299,17 +414,10 @@ public class Game extends BasicGameState {
arrow.draw(width * 0.75f, height * 0.75f); arrow.draw(width * 0.75f, height * 0.75f);
} }
} }
if (GameMod.AUTO.isActive())
GameImage.UNRANKED.getImage().drawCentered(width / 2, height * 0.077f);
if (!isReplay)
UI.draw(g);
else
UI.draw(g, replayX, replayY, replayKeyPressed);
return;
}
} }
// non-break
else {
// game elements // game elements
data.drawGameElements(g, false, objectIndex == 0); data.drawGameElements(g, false, objectIndex == 0);
@ -381,16 +489,10 @@ public class Game extends BasicGameState {
} }
} }
// draw hit objects in reverse order, or else overlapping objects are unreadable // draw hit objects
Stack<Integer> stack = new Stack<Integer>(); if (!GameMod.FLASHLIGHT.isActive())
for (int i = objectIndex; i < hitObjects.length && osu.objects[i].getTime() < trackPosition + approachTime; i++) drawHitObjects(g, trackPosition);
stack.add(i); }
while (!stack.isEmpty())
hitObjects[stack.pop()].draw(g, trackPosition);
// draw OsuHitObjectResult objects
data.drawHitResults(trackPosition);
if (GameMod.AUTO.isActive()) if (GameMod.AUTO.isActive())
GameImage.UNRANKED.getImage().drawCentered(width / 2, height * 0.077f); GameImage.UNRANKED.getImage().drawCentered(width / 2, height * 0.077f);
@ -411,7 +513,11 @@ public class Game extends BasicGameState {
cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY); cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY);
} }
if (!isReplay) if (GameMod.AUTO.isActive())
UI.draw(g, autoMouseX, autoMouseY, autoMousePressed);
else if (GameMod.AUTOPILOT.isActive())
UI.draw(g, autoMouseX, autoMouseY, Utils.isGameKeyPressed());
else if (!isReplay)
UI.draw(g); UI.draw(g);
else else
UI.draw(g, replayX, replayY, replayKeyPressed); UI.draw(g, replayX, replayY, replayKeyPressed);
@ -429,7 +535,11 @@ public class Game extends BasicGameState {
return; return;
} }
int trackPosition = MusicController.getPosition(); int trackPosition = MusicController.getPosition();
if (!isReplay) { if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
mouseX = autoMouseX;
mouseY = autoMouseY;
frameAndRun(mouseX, mouseY, lastKeysPressed, trackPosition);
} else if (!isReplay) {
mouseX = input.getMouseX(); mouseX = input.getMouseX();
mouseY = input.getMouseY(); mouseY = input.getMouseY();
frameAndRun(mouseX, mouseY, lastKeysPressed, trackPosition); frameAndRun(mouseX, mouseY, lastKeysPressed, trackPosition);
@ -455,6 +565,61 @@ public class Game extends BasicGameState {
// addReplayFrame(mouseX, mouseY, keysPressed, trackPosition); // addReplayFrame(mouseX, mouseY, keysPressed, trackPosition);
skipButton.hoverUpdate(delta, mouseX, mouseY); skipButton.hoverUpdate(delta, mouseX, mouseY);
// "flashlight" mod: calculate visible area radius
if (GameMod.FLASHLIGHT.isActive()) {
int width = container.getWidth(), height = container.getHeight();
boolean firstObject = (objectIndex == 0 && trackPosition < osu.objects[0].getTime());
if (isLeadIn()) {
// lead-in: expand area
float progress = Math.max((float) (leadInTime - osu.audioLeadIn) / approachTime, 0f);
flashlightRadius = width - (int) ((width - (height * 2 / 3)) * progress);
} else if (firstObject) {
// before first object: shrink area
int timeDiff = osu.objects[0].getTime() - trackPosition;
flashlightRadius = width;
if (timeDiff < approachTime) {
float progress = (float) timeDiff / approachTime;
flashlightRadius -= (width - (height * 2 / 3)) * (1 - progress);
}
} else {
// gameplay: size based on combo
int targetRadius;
int combo = data.getComboStreak();
if (combo < 100)
targetRadius = height * 2 / 3;
else if (combo < 200)
targetRadius = height / 2;
else
targetRadius = height / 3;
if (osu.breaks != null && breakIndex < osu.breaks.size() && breakTime > 0) {
// breaks: expand at beginning, shrink at end
flashlightRadius = targetRadius;
int endTime = osu.breaks.get(breakIndex);
int breakLength = endTime - breakTime;
if (breakLength > approachTime * 3) {
float progress = 1f;
if (trackPosition - breakTime < approachTime)
progress = (float) (trackPosition - breakTime) / approachTime;
else if (endTime - trackPosition < approachTime)
progress = (float) (endTime - trackPosition) / approachTime;
flashlightRadius += (width - flashlightRadius) * progress;
}
} else if (flashlightRadius != targetRadius) {
// radius size change
float radiusDiff = height * delta / 2000f;
if (flashlightRadius > targetRadius) {
flashlightRadius -= radiusDiff;
if (flashlightRadius < targetRadius)
flashlightRadius = targetRadius;
} else {
flashlightRadius += radiusDiff;
if (flashlightRadius > targetRadius)
flashlightRadius = targetRadius;
}
}
}
}
// returning from pause screen: must click previous mouse position // returning from pause screen: must click previous mouse position
if (pauseTime > -1) { if (pauseTime > -1) {
@ -505,6 +670,7 @@ public class Game extends BasicGameState {
// go to ranking screen // go to ranking screen
else { else {
boolean unranked = (GameMod.AUTO.isActive() || GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive());
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data); ((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
if (isReplay) if (isReplay)
data.setReplay(replay); data.setReplay(replay);
@ -515,13 +681,13 @@ public class Game extends BasicGameState {
replayFrames.addFirst(ReplayFrame.getStartFrame(replaySkipTime)); replayFrames.addFirst(ReplayFrame.getStartFrame(replaySkipTime));
replayFrames.addFirst(ReplayFrame.getStartFrame(0)); replayFrames.addFirst(ReplayFrame.getStartFrame(0));
Replay r = data.getReplay(replayFrames.toArray(new ReplayFrame[replayFrames.size()]), osu); Replay r = data.getReplay(replayFrames.toArray(new ReplayFrame[replayFrames.size()]), osu);
if (r != null) if (r != null && !unranked)
r.save(); r.save();
} }
ScoreData score = data.getScoreData(osu); ScoreData score = data.getScoreData(osu);
// add score to database // add score to database
if (!GameMod.AUTO.isActive() && !GameMod.RELAX.isActive() && !GameMod.AUTOPILOT.isActive() && !isReplay) if (!unranked && !isReplay)
ScoreDB.addScore(score); ScoreDB.addScore(score);
game.enterState(Opsu.STATE_GAMERANKING, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); game.enterState(Opsu.STATE_GAMERANKING, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
@ -810,13 +976,23 @@ public class Game extends BasicGameState {
if (GameMod.AUTO.isActive() || GameMod.RELAX.isActive()) if (GameMod.AUTO.isActive() || GameMod.RELAX.isActive())
return; return;
// "autopilot" mod: ignore actual cursor coordinates
int cx, cy;
if (GameMod.AUTOPILOT.isActive()) {
cx = autoMouseX;
cy = autoMouseY;
} else {
cx = x;
cy = y;
}
// circles // circles
if (hitObject.isCircle() && hitObjects[objectIndex].mousePressed(x, y, trackPosition)) if (hitObject.isCircle() && hitObjects[objectIndex].mousePressed(cx, cy, trackPosition))
objectIndex++; // circle hit objectIndex++; // circle hit
// sliders // sliders
else if (hitObject.isSlider()) else if (hitObject.isSlider())
hitObjects[objectIndex].mousePressed(x, y, trackPosition); hitObjects[objectIndex].mousePressed(cx, cy, trackPosition);
} }
@Override @Override
@ -837,9 +1013,7 @@ public class Game extends BasicGameState {
public void keyReleased(int key, char c) { public void keyReleased(int key, char c) {
if (!isReplay && (key == Options.getGameKeyLeft() || key == Options.getGameKeyRight())) { if (!isReplay && (key == Options.getGameKeyLeft() || key == Options.getGameKeyRight())) {
lastKeysPressed &= ~((key == Options.getGameKeyLeft()) ? ReplayFrame.KEY_K1 : ReplayFrame.KEY_K2); lastKeysPressed &= ~((key == Options.getGameKeyLeft()) ? ReplayFrame.KEY_K1 : ReplayFrame.KEY_K2);
int mouseX = input.getMouseX(); frameAndRun(input.getMouseX(), input.getMouseY(), lastKeysPressed, MusicController.getPosition());
int mouseY = input.getMouseY();
frameAndRun(mouseX, mouseY, lastKeysPressed, MusicController.getPosition());
} }
} }
@ -920,11 +1094,13 @@ public class Game extends BasicGameState {
} }
} }
// load replay frames // unhide cursor for "auto" mod and replays
if (isReplay) { if (GameMod.AUTO.isActive() || isReplay)
// unhide cursor
UI.showCursor(); UI.showCursor();
lastReplayTime = 0;
// load replay frames
if (isReplay) {
// load mods // load mods
previousMods = GameMod.getModState(); previousMods = GameMod.getModState();
GameMod.loadModState(replay.mods); GameMod.loadModState(replay.mods);
@ -953,7 +1129,6 @@ public class Game extends BasicGameState {
// initialize replay-recording structures // initialize replay-recording structures
else { else {
lastReplayTime = 0;
lastKeysPressed = ReplayFrame.KEY_NONE; lastKeysPressed = ReplayFrame.KEY_NONE;
replaySkipTime = -1; replaySkipTime = -1;
replayFrames = new LinkedList<ReplayFrame>(); replayFrames = new LinkedList<ReplayFrame>();
@ -972,13 +1147,34 @@ public class Game extends BasicGameState {
throws SlickException { throws SlickException {
// container.setMouseGrabbed(false); // container.setMouseGrabbed(false);
// re-hide cursor
if (GameMod.AUTO.isActive() || isReplay)
UI.hideCursor();
// replays // replays
if (isReplay) { if (isReplay) {
GameMod.loadModState(previousMods); GameMod.loadModState(previousMods);
UI.hideCursor();
} }
} }
/**
* Draws hit objects and hit results.
* @param g the graphics context
* @param trackPosition the track position
*/
private void drawHitObjects(Graphics g, int trackPosition) {
// draw hit objects in reverse order, or else overlapping objects are unreadable
Stack<Integer> stack = new Stack<Integer>();
for (int i = objectIndex; i < hitObjects.length && osu.objects[i].getTime() < trackPosition + approachTime; i++)
stack.add(i);
while (!stack.isEmpty())
hitObjects[stack.pop()].draw(g, trackPosition);
// draw OsuHitObjectResult objects
data.drawHitResults(trackPosition);
}
/** /**
* Loads all required data from an OsuFile. * Loads all required data from an OsuFile.
* @param osu the OsuFile to load * @param osu the OsuFile to load
@ -1016,6 +1212,9 @@ public class Game extends BasicGameState {
deaths = 0; deaths = 0;
deathTime = -1; deathTime = -1;
replayFrames = null; replayFrames = null;
autoMouseX = 0;
autoMouseY = 0;
autoMousePressed = false;
System.gc(); System.gc();
} }
@ -1216,16 +1415,31 @@ public class Game extends BasicGameState {
private ReplayFrame addReplayFrame(int x, int y, int keys, int time) { private ReplayFrame addReplayFrame(int x, int y, int keys, int time) {
int timeDiff = time - lastReplayTime; int timeDiff = time - lastReplayTime;
lastReplayTime = time; lastReplayTime = time;
int cx = unscaleX(x); int cx = (int) ((x - OsuHitObject.getXOffset()) / OsuHitObject.getXMultiplier());
int cy = unscaleY(y); int cy = (int) ((y - OsuHitObject.getYOffset()) / OsuHitObject.getYMultiplier());
ReplayFrame tFrame = new ReplayFrame(timeDiff, time, cx, cy, keys); ReplayFrame tFrame = new ReplayFrame(timeDiff, time, cx, cy, keys);
if(replayFrames != null)
replayFrames.add(tFrame); replayFrames.add(tFrame);
return tFrame; return tFrame;
} }
private int unscaleX(int x){
return (int) ((x - OsuHitObject.getXOffset()) / OsuHitObject.getXMultiplier()); /**
} * Returns the point at the t value between a start and end point.
private int unscaleY(int y){ * @param startX the starting x coordinate
return (int) ((y - OsuHitObject.getYOffset()) / OsuHitObject.getYMultiplier()); * @param startY the starting y coordinate
* @param endX the ending x coordinate
* @param endY the ending y coordinate
* @param t the t value [0, 1]
* @return the [x,y] coordinates
*/
private float[] getPointAt(float startX, float startY, float endX, float endY, float t) {
// "autopilot" mod: move quicker between objects
if (GameMod.AUTOPILOT.isActive())
t = Utils.clamp(t * 2f, 0f, 1f);
float[] xy = new float[2];
xy[0] = startX + (endX - startX) * t;
xy[1] = startY + (endY - startY) * t;
return xy;
} }
} }

View File

@ -239,7 +239,7 @@ public class MainMenu extends BasicGameState {
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight, 4); g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight, 4);
g.setColor(Color.white); g.setColor(Color.white);
if (!MusicController.isTrackLoading() && osu != null) { if (!MusicController.isTrackLoading() && osu != null) {
float musicBarPosition = Math.min((float) MusicController.getPosition() / osu.endTime, 1f); float musicBarPosition = Math.min((float) MusicController.getPosition() / MusicController.getDuration(), 1f);
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth * musicBarPosition, musicBarHeight, 4); g.fillRoundRect(musicBarX, musicBarY, musicBarWidth * musicBarPosition, musicBarHeight, 4);
} }
@ -430,8 +430,7 @@ public class MainMenu extends BasicGameState {
if (MusicController.isPlaying()) { if (MusicController.isPlaying()) {
if (musicPositionBarContains(x, y)) { if (musicPositionBarContains(x, y)) {
float pos = (x - musicBarX) / musicBarWidth; float pos = (x - musicBarX) / musicBarWidth;
OsuFile osu = MusicController.getOsuFile(); MusicController.setPosition((int) (pos * MusicController.getDuration()));
MusicController.setPosition((int) (pos * osu.endTime));
return; return;
} }
} }