diff --git a/res/alpha.png b/res/alpha.png new file mode 100644 index 00000000..35f9344e Binary files /dev/null and b/res/alpha.png differ diff --git a/src/itdelatrisu/opsu/ErrorHandler.java b/src/itdelatrisu/opsu/ErrorHandler.java index 97505e75..3976902e 100644 --- a/src/itdelatrisu/opsu/ErrorHandler.java +++ b/src/itdelatrisu/opsu/ErrorHandler.java @@ -133,6 +133,14 @@ public class ErrorHandler { sb.append(timestamp); 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) { sb.append("**Error:** `"); sb.append(error); diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index 465e1eda..8ce29af7 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -917,22 +917,22 @@ public class GameData { * @param hit100 the number of 100s * @param hit50 the number of 50s * @param miss the number of misses + * @param silver whether or not a silver SS/S should be awarded (if applicable) * @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; if (objectCount < 1) // avoid division by zero return Grade.NULL; - // TODO: silvers float percent = getScorePercent(hit300, hit100, hit50, miss); float hit300ratio = hit300 * 100f / objectCount; - float hit50ratio = hit50 * 100f / objectCount; - boolean noMiss = (miss == 0); + float hit50ratio = hit50 * 100f / objectCount; + boolean noMiss = (miss == 0); if (percent >= 100f) - return Grade.SS; + return (silver) ? Grade.SSH : Grade.SS; else if (hit300ratio >= 90f && hit50ratio < 1.0f && noMiss) - return Grade.S; + return (silver) ? Grade.SH : Grade.S; else if ((hit300ratio >= 80f && noMiss) || hit300ratio >= 90f) return Grade.A; else if ((hit300ratio >= 70f && noMiss) || hit300ratio >= 80f) @@ -950,7 +950,8 @@ public class GameData { private Grade getGrade() { return getGrade( 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. */ diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index 74592995..e8de7423 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -327,7 +327,9 @@ public enum GameImage { protected Image process_sub(Image img, int w, int 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. */ private static final byte diff --git a/src/itdelatrisu/opsu/GameMod.java b/src/itdelatrisu/opsu/GameMod.java index dc7b1146..a7bd3e97 100644 --- a/src/itdelatrisu/opsu/GameMod.java +++ b/src/itdelatrisu/opsu/GameMod.java @@ -47,11 +47,11 @@ public enum GameMod { // "Nightcore", "uguuuuuuuu"), 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."), - 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."), 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**"), - 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**"), SPUN_OUT (Category.SPECIAL, 2, GameImage.MOD_SPUN_OUT, "SO", 4096, Input.KEY_V, 0.9f, "SpunOut", "Spinners will be automatically completed."), @@ -355,17 +355,16 @@ public enum GameMod { scoreMultiplier = -1f; if (checkInverse) { - boolean b = (this == SUDDEN_DEATH || this == NO_FAIL || this == RELAX || this == AUTOPILOT); if (AUTO.isActive()) { if (this == AUTO) { SPUN_OUT.active = false; SUDDEN_DEATH.active = false; RELAX.active = false; AUTOPILOT.active = false; - } else if (b) + } else if (this == SPUN_OUT || this == SUDDEN_DEATH || this == RELAX || this == AUTOPILOT) this.active = false; } - if (active && b) { + if (active && (this == SUDDEN_DEATH || this == NO_FAIL || this == RELAX || this == AUTOPILOT)) { SUDDEN_DEATH.active = false; NO_FAIL.active = false; RELAX.active = false; @@ -384,6 +383,12 @@ public enum GameMod { else EASY.active = false; } + if (HALF_TIME.isActive() && DOUBLE_TIME.isActive()) { + if (this == HALF_TIME) + DOUBLE_TIME.active = false; + else + HALF_TIME.active = false; + } } } diff --git a/src/itdelatrisu/opsu/ScoreData.java b/src/itdelatrisu/opsu/ScoreData.java index 71ea5d74..fc25daf8 100644 --- a/src/itdelatrisu/opsu/ScoreData.java +++ b/src/itdelatrisu/opsu/ScoreData.java @@ -187,11 +187,12 @@ public class ScoreData implements Comparable { /** * Returns letter grade based on score data, * 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() { 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; } diff --git a/src/itdelatrisu/opsu/UI.java b/src/itdelatrisu/opsu/UI.java index 60108150..ab5e44d1 100644 --- a/src/itdelatrisu/opsu/UI.java +++ b/src/itdelatrisu/opsu/UI.java @@ -173,6 +173,7 @@ public class UI { public static void enter() { backButton.resetHover(); resetBarNotification(); + resetCursorLocations(); resetTooltip(); } @@ -371,10 +372,16 @@ public class UI { GameImage.CURSOR_MIDDLE.destroySkinImage(); GameImage.CURSOR_TRAIL.destroySkinImage(); cursorAngle = 0f; + GameImage.CURSOR.getImage().setRotation(0f); + } + + /** + * Resets all cursor location data. + */ + private static void resetCursorLocations() { lastX = lastY = -1; cursorX.clear(); cursorY.clear(); - GameImage.CURSOR.getImage().setRotation(0f); } /** diff --git a/src/itdelatrisu/opsu/audio/MusicController.java b/src/itdelatrisu/opsu/audio/MusicController.java index 6c84e7ad..797b355c 100644 --- a/src/itdelatrisu/opsu/audio/MusicController.java +++ b/src/itdelatrisu/opsu/audio/MusicController.java @@ -24,8 +24,14 @@ import itdelatrisu.opsu.OsuFile; import itdelatrisu.opsu.OsuParser; import java.io.File; +import java.io.IOException; import java.lang.reflect.Field; 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.openal.AL; @@ -35,6 +41,7 @@ import org.newdawn.slick.MusicListener; import org.newdawn.slick.SlickException; import org.newdawn.slick.openal.Audio; import org.newdawn.slick.openal.SoundStore; +import org.tritonus.share.sampled.file.TAudioFileFormat; /** * Controller for all music. @@ -252,6 +259,30 @@ public class MusicController { 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. * @param loop whether or not to loop the track diff --git a/src/itdelatrisu/opsu/OsuReader.java b/src/itdelatrisu/opsu/io/OsuReader.java similarity index 99% rename from src/itdelatrisu/opsu/OsuReader.java rename to src/itdelatrisu/opsu/io/OsuReader.java index e5ebd50c..c36a2b88 100644 --- a/src/itdelatrisu/opsu/OsuReader.java +++ b/src/itdelatrisu/opsu/io/OsuReader.java @@ -16,7 +16,7 @@ * along with opsu!. If not, see . */ -package itdelatrisu.opsu; +package itdelatrisu.opsu.io; import java.io.DataInputStream; import java.io.File; diff --git a/src/itdelatrisu/opsu/OsuWriter.java b/src/itdelatrisu/opsu/io/OsuWriter.java similarity index 99% rename from src/itdelatrisu/opsu/OsuWriter.java rename to src/itdelatrisu/opsu/io/OsuWriter.java index 56cd8f21..eca07b3a 100644 --- a/src/itdelatrisu/opsu/OsuWriter.java +++ b/src/itdelatrisu/opsu/io/OsuWriter.java @@ -16,7 +16,7 @@ * along with opsu!. If not, see . */ -package itdelatrisu.opsu; +package itdelatrisu.opsu.io; import java.io.DataOutputStream; import java.io.File; diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java index 218d71f3..a077eadb 100644 --- a/src/itdelatrisu/opsu/objects/Circle.java +++ b/src/itdelatrisu/opsu/objects/Circle.java @@ -178,4 +178,10 @@ public class Circle implements HitObject { return false; } -} \ No newline at end of file + + @Override + public float[] getPointAt(int trackPosition) { return new float[] { x, y }; } + + @Override + public int getEndTime() { return hitObject.getTime(); } +} diff --git a/src/itdelatrisu/opsu/objects/HitObject.java b/src/itdelatrisu/opsu/objects/HitObject.java index 8f2f626a..957219be 100644 --- a/src/itdelatrisu/opsu/objects/HitObject.java +++ b/src/itdelatrisu/opsu/objects/HitObject.java @@ -51,4 +51,17 @@ public interface HitObject { * @return true if a hit result was processed */ 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(); } diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 2827b2df..b45d4dba 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -92,6 +92,9 @@ public class Slider implements HitObject { /** Number of ticks hit and tick intervals so far. */ private int ticksHit = 0, tickIntervals = 1; + /** Container dimensions. */ + private static int containerWidth, containerHeight; + /** * Initializes the Slider data type with images and dimensions. * @param container the game container @@ -99,6 +102,9 @@ public class Slider implements HitObject { * @param osu the associated OsuFile object */ public static void init(GameContainer container, float circleSize, OsuFile osu) { + containerWidth = container.getWidth(); + containerHeight = container.getHeight(); + int diameter = (int) (104 - (circleSize * 8)); diameter = (int) (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480) @@ -188,7 +194,7 @@ public class Slider implements HitObject { color.a = oldAlpha; // repeats - for(int tcurRepeat = currentRepeats; tcurRepeat<=currentRepeats+1; tcurRepeat++){ + for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) { if (hitObject.getRepeatCount() - 1 > tcurRepeat) { Image arrow = GameImage.REVERSEARROW.getImage(); if (tcurRepeat != currentRepeats) { @@ -233,8 +239,18 @@ public class Slider implements HitObject { sliderBallFrame.drawCentered(c[0], c[1]); // follow circle - if (followCircleActive) + if (followCircleActive) { 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; } + @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. * @param trackPosition the current track position @@ -456,4 +490,4 @@ public class Slider implements HitObject { return (floor % 2 == 0) ? t - floor : floor + 1 - t; } } -} \ No newline at end of file +} diff --git a/src/itdelatrisu/opsu/objects/Spinner.java b/src/itdelatrisu/opsu/objects/Spinner.java index a176f1fc..0388147d 100644 --- a/src/itdelatrisu/opsu/objects/Spinner.java +++ b/src/itdelatrisu/opsu/objects/Spinner.java @@ -50,8 +50,15 @@ public class Spinner implements HitObject { /** The amount of time, in milliseconds, to fade in the spinner. */ 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. */ - 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. */ private OsuHitObject hitObject; @@ -174,8 +181,7 @@ public class Spinner implements HitObject { // TODO: verify ratios int result; float ratio = rotations / rotationsNeeded; - if (ratio >= 1.0f || - GameMod.AUTO.isActive() || GameMod.SPUN_OUT.isActive()) { + if (ratio >= 1.0f || GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive() || GameMod.SPUN_OUT.isActive()) { result = GameData.HIT_300; SoundController.playSound(SoundEffect.SPINNEROSU); } else if (ratio >= 0.9f) @@ -210,14 +216,12 @@ public class Spinner implements HitObject { // http://osu.ppy.sh/wiki/FAQ#Spinners float angle; if (GameMod.AUTO.isActive()) { - // "auto" mod (fast: 477rpm) lastAngle = 0; - angle = delta / 20f; // angle = 477/60f * delta/1000f * TWO_PI; + angle = delta * AUTO_MULTIPLIER; isSpinning = true; - } else if (GameMod.SPUN_OUT.isActive()) { - // "spun out" mod (slow: 287rpm) + } else if (GameMod.SPUN_OUT.isActive() || GameMod.AUTOPILOT.isActive()) { lastAngle = 0; - angle = delta / 33.25f; // angle = 287/60f * delta/1000f * TWO_PI; + angle = delta * SPUN_OUT_MULTIPLIER; isSpinning = true; } else { angle = (float) Math.atan2(mouseY - (height / 2), mouseX - (width / 2)); @@ -260,6 +264,31 @@ public class Spinner implements HitObject { 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. * @param angle the angle to rotate (in radians) diff --git a/src/itdelatrisu/opsu/replay/Replay.java b/src/itdelatrisu/opsu/replay/Replay.java index 9182bc80..e47c5a05 100644 --- a/src/itdelatrisu/opsu/replay/Replay.java +++ b/src/itdelatrisu/opsu/replay/Replay.java @@ -20,9 +20,9 @@ package itdelatrisu.opsu.replay; import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.Options; -import itdelatrisu.opsu.OsuReader; -import itdelatrisu.opsu.OsuWriter; import itdelatrisu.opsu.Utils; +import itdelatrisu.opsu.io.OsuReader; +import itdelatrisu.opsu.io.OsuWriter; import java.io.ByteArrayOutputStream; import java.io.File; diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 92d38117..2c0d16ce 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -50,7 +50,6 @@ import java.io.FileNotFoundException; import java.io.PrintWriter; import java.util.LinkedList; import java.util.Stack; -import java.util.concurrent.TimeUnit; import org.lwjgl.input.Keyboard; import org.lwjgl.opengl.Display; @@ -189,6 +188,21 @@ public class Game extends BasicGameState { /** The list of current replay frames (for recording replays). */ private LinkedList 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 private GameContainer container; private StateBasedGame game; @@ -209,6 +223,11 @@ public class Game extends BasicGameState { int width = container.getWidth(); int height = container.getHeight(); + // create offscreen graphics + offscreen = new Image(width, height); + gOffscreen = offscreen.getGraphics(); + gOffscreen.setBackground(Color.black); + // create the associated GameData object data = new GameData(width, height); } @@ -218,9 +237,15 @@ public class Game extends BasicGameState { throws SlickException { int width = container.getWidth(); int height = container.getHeight(); + g.setBackground(Color.black); + + // "flashlight" mod: initialize offscreen graphics + if (GameMod.FLASHLIGHT.isActive()) { + gOffscreen.clear(); + Graphics.setCurrent(gOffscreen); + } // background - g.setBackground(Color.black); float dimLevel = Options.getBackgroundDim(); if (Options.isDefaultPlayfieldForced() || !osu.drawBG(width, height, dimLevel, false)) { Image playfield = GameImage.PLAYFIELD.getImage(); @@ -229,6 +254,9 @@ public class Game extends BasicGameState { playfield.setAlpha(1f); } + if (GameMod.FLASHLIGHT.isActive()) + Graphics.setCurrent(g); + int trackPosition = MusicController.getPosition(); if (pauseTime > -1) // returning from pause screen trackPosition = pauseTime; @@ -237,160 +265,234 @@ public class Game extends BasicGameState { int firstObjectTime = osu.objects[0].getTime(); int timeDiff = firstObjectTime - trackPosition; - // checkpoint - if (checkpointLoaded) { - int checkpoint = Options.getCheckpoint(); - String checkpointText = String.format( - "Playing from checkpoint at %02d:%02d.", - TimeUnit.MILLISECONDS.toMinutes(checkpoint), - TimeUnit.MILLISECONDS.toSeconds(checkpoint) - - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(checkpoint)) - ); - Utils.FONT_MEDIUM.drawString( - (width - Utils.FONT_MEDIUM.getWidth(checkpointText)) / 2, - height - 15 - Utils.FONT_MEDIUM.getLineHeight(), - checkpointText, Color.white - ); + // "auto" and "autopilot" mods: move cursor automatically + // TODO: this should really be in update(), not render() + autoMouseX = width / 2; + autoMouseY = height / 2; + autoMousePressed = false; + if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) { + float[] autoXY = null; + if (isLeadIn()) { + // lead-in + float progress = Math.max((float) (leadInTime - osu.audioLeadIn) / approachTime, 0f); + autoMouseY = (int) (height / (2f - progress)); + } else if (objectIndex == 0 && trackPosition < firstObjectTime) { + // 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 - if (osu.breaks != null && breakIndex < osu.breaks.size()) { - if (breakTime > 0) { - int endTime = osu.breaks.get(breakIndex); - int breakLength = endTime - breakTime; + if (osu.breaks != null && breakIndex < osu.breaks.size() && breakTime > 0) { + int endTime = osu.breaks.get(breakIndex); + int breakLength = endTime - breakTime; - // letterbox effect (black bars on top/bottom) - if (osu.letterboxInBreaks && breakLength >= 4000) { - g.setColor(Color.black); - g.fillRect(0, 0, width, height * 0.125f); - g.fillRect(0, height * 0.875f, width, height * 0.125f); - } - - data.drawGameElements(g, true, objectIndex == 0); - - if (breakLength >= 8000 && - trackPosition - breakTime > 2000 && - trackPosition - breakTime < 5000) { - // show break start - if (data.getHealth() >= 50) { - GameImage.SECTION_PASS.getImage().drawCentered(width / 2f, height / 2f); - if (!breakSound) { - SoundController.playSound(SoundEffect.SECTIONPASS); - breakSound = true; - } - } else { - GameImage.SECTION_FAIL.getImage().drawCentered(width / 2f, height / 2f); - if (!breakSound) { - SoundController.playSound(SoundEffect.SECTIONFAIL); - breakSound = true; - } - } - } else if (breakLength >= 4000) { - // show break end (flash twice for 500ms) - int endTimeDiff = endTime - trackPosition; - if ((endTimeDiff > 1500 && endTimeDiff < 2000) || - (endTimeDiff > 500 && endTimeDiff < 1000)) { - Image arrow = GameImage.WARNINGARROW.getImage(); - arrow.setRotation(0); - arrow.draw(width * 0.15f, height * 0.15f); - arrow.draw(width * 0.15f, height * 0.75f); - arrow.setRotation(180); - arrow.draw(width * 0.75f, height * 0.15f); - 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; + // letterbox effect (black bars on top/bottom) + if (osu.letterboxInBreaks && breakLength >= 4000) { + g.setColor(Color.black); + g.fillRect(0, 0, width, height * 0.125f); + g.fillRect(0, height * 0.875f, width, height * 0.125f); } - } - // game elements - data.drawGameElements(g, false, objectIndex == 0); + data.drawGameElements(g, true, objectIndex == 0); - // skip beginning - if (objectIndex == 0 && - trackPosition < osu.objects[0].getTime() - SKIP_OFFSET) - skipButton.draw(); - - // show retries - if (retries >= 2 && timeDiff >= -1000) { - int retryHeight = Math.max( - GameImage.SCOREBAR_BG.getImage().getHeight(), - GameImage.SCOREBAR_KI.getImage().getHeight() - ); - float oldAlpha = Utils.COLOR_WHITE_FADE.a; - if (timeDiff < -500) - Utils.COLOR_WHITE_FADE.a = (1000 + timeDiff) / 500f; - Utils.FONT_MEDIUM.drawString( - 2 + (width / 100), retryHeight, - String.format("%d retries and counting...", retries), - Utils.COLOR_WHITE_FADE - ); - Utils.COLOR_WHITE_FADE.a = oldAlpha; - } - - if (isLeadIn()) - trackPosition = (leadInTime - Options.getMusicOffset()) * -1; // render approach circles during song lead-in - - // countdown - if (osu.countdown > 0) { // TODO: implement half/double rate settings - timeDiff = firstObjectTime - trackPosition; - if (timeDiff >= 500 && timeDiff < 3000) { - if (timeDiff >= 1500) { - GameImage.COUNTDOWN_READY.getImage().drawCentered(width / 2, height / 2); - if (!countdownReadySound) { - SoundController.playSound(SoundEffect.READY); - countdownReadySound = true; + if (breakLength >= 8000 && + trackPosition - breakTime > 2000 && + trackPosition - breakTime < 5000) { + // show break start + if (data.getHealth() >= 50) { + GameImage.SECTION_PASS.getImage().drawCentered(width / 2f, height / 2f); + if (!breakSound) { + SoundController.playSound(SoundEffect.SECTIONPASS); + breakSound = true; + } + } else { + GameImage.SECTION_FAIL.getImage().drawCentered(width / 2f, height / 2f); + if (!breakSound) { + SoundController.playSound(SoundEffect.SECTIONFAIL); + breakSound = true; } } - if (timeDiff < 2000) { - GameImage.COUNTDOWN_3.getImage().draw(0, 0); - if (!countdown3Sound) { - SoundController.playSound(SoundEffect.COUNT3); - countdown3Sound = true; - } - } - if (timeDiff < 1500) { - GameImage.COUNTDOWN_2.getImage().draw(width - GameImage.COUNTDOWN_2.getImage().getWidth(), 0); - if (!countdown2Sound) { - SoundController.playSound(SoundEffect.COUNT2); - countdown2Sound = true; - } - } - if (timeDiff < 1000) { - GameImage.COUNTDOWN_1.getImage().drawCentered(width / 2, height / 2); - if (!countdown1Sound) { - SoundController.playSound(SoundEffect.COUNT1); - countdown1Sound = true; - } - } - } else if (timeDiff >= -500 && timeDiff < 500) { - Image go = GameImage.COUNTDOWN_GO.getImage(); - go.setAlpha((timeDiff < 0) ? 1 - (timeDiff / -1000f) : 1); - go.drawCentered(width / 2, height / 2); - if (!countdownGoSound) { - SoundController.playSound(SoundEffect.GO); - countdownGoSound = true; + } else if (breakLength >= 4000) { + // show break end (flash twice for 500ms) + int endTimeDiff = endTime - trackPosition; + if ((endTimeDiff > 1500 && endTimeDiff < 2000) || + (endTimeDiff > 500 && endTimeDiff < 1000)) { + Image arrow = GameImage.WARNINGARROW.getImage(); + arrow.setRotation(0); + arrow.draw(width * 0.15f, height * 0.15f); + arrow.draw(width * 0.15f, height * 0.75f); + arrow.setRotation(180); + arrow.draw(width * 0.75f, height * 0.15f); + arrow.draw(width * 0.75f, height * 0.75f); } } } - // draw hit objects in reverse order, or else overlapping objects are unreadable - Stack stack = new Stack(); - for (int i = objectIndex; i < hitObjects.length && osu.objects[i].getTime() < trackPosition + approachTime; i++) - stack.add(i); + // non-break + else { + // game elements + data.drawGameElements(g, false, objectIndex == 0); + + // skip beginning + if (objectIndex == 0 && + trackPosition < osu.objects[0].getTime() - SKIP_OFFSET) + skipButton.draw(); - while (!stack.isEmpty()) - hitObjects[stack.pop()].draw(g, trackPosition); + // show retries + if (retries >= 2 && timeDiff >= -1000) { + int retryHeight = Math.max( + GameImage.SCOREBAR_BG.getImage().getHeight(), + GameImage.SCOREBAR_KI.getImage().getHeight() + ); + float oldAlpha = Utils.COLOR_WHITE_FADE.a; + if (timeDiff < -500) + Utils.COLOR_WHITE_FADE.a = (1000 + timeDiff) / 500f; + Utils.FONT_MEDIUM.drawString( + 2 + (width / 100), retryHeight, + String.format("%d retries and counting...", retries), + Utils.COLOR_WHITE_FADE + ); + Utils.COLOR_WHITE_FADE.a = oldAlpha; + } - // draw OsuHitObjectResult objects - data.drawHitResults(trackPosition); + if (isLeadIn()) + trackPosition = (leadInTime - Options.getMusicOffset()) * -1; // render approach circles during song lead-in + + // countdown + if (osu.countdown > 0) { // TODO: implement half/double rate settings + timeDiff = firstObjectTime - trackPosition; + if (timeDiff >= 500 && timeDiff < 3000) { + if (timeDiff >= 1500) { + GameImage.COUNTDOWN_READY.getImage().drawCentered(width / 2, height / 2); + if (!countdownReadySound) { + SoundController.playSound(SoundEffect.READY); + countdownReadySound = true; + } + } + if (timeDiff < 2000) { + GameImage.COUNTDOWN_3.getImage().draw(0, 0); + if (!countdown3Sound) { + SoundController.playSound(SoundEffect.COUNT3); + countdown3Sound = true; + } + } + if (timeDiff < 1500) { + GameImage.COUNTDOWN_2.getImage().draw(width - GameImage.COUNTDOWN_2.getImage().getWidth(), 0); + if (!countdown2Sound) { + SoundController.playSound(SoundEffect.COUNT2); + countdown2Sound = true; + } + } + if (timeDiff < 1000) { + GameImage.COUNTDOWN_1.getImage().drawCentered(width / 2, height / 2); + if (!countdown1Sound) { + SoundController.playSound(SoundEffect.COUNT1); + countdown1Sound = true; + } + } + } else if (timeDiff >= -500 && timeDiff < 500) { + Image go = GameImage.COUNTDOWN_GO.getImage(); + go.setAlpha((timeDiff < 0) ? 1 - (timeDiff / -1000f) : 1); + go.drawCentered(width / 2, height / 2); + if (!countdownGoSound) { + SoundController.playSound(SoundEffect.GO); + countdownGoSound = true; + } + } + } + + // draw hit objects + if (!GameMod.FLASHLIGHT.isActive()) + drawHitObjects(g, trackPosition); + } if (GameMod.AUTO.isActive()) GameImage.UNRANKED.getImage().drawCentered(width / 2, height * 0.077f); @@ -411,7 +513,11 @@ public class Game extends BasicGameState { 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); else UI.draw(g, replayX, replayY, replayKeyPressed); @@ -429,7 +535,11 @@ public class Game extends BasicGameState { return; } 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(); mouseY = input.getMouseY(); frameAndRun(mouseX, mouseY, lastKeysPressed, trackPosition); @@ -455,6 +565,61 @@ public class Game extends BasicGameState { // addReplayFrame(mouseX, mouseY, keysPressed, trackPosition); 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 if (pauseTime > -1) { @@ -505,6 +670,7 @@ public class Game extends BasicGameState { // go to ranking screen else { + boolean unranked = (GameMod.AUTO.isActive() || GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive()); ((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data); if (isReplay) data.setReplay(replay); @@ -515,13 +681,13 @@ public class Game extends BasicGameState { replayFrames.addFirst(ReplayFrame.getStartFrame(replaySkipTime)); replayFrames.addFirst(ReplayFrame.getStartFrame(0)); Replay r = data.getReplay(replayFrames.toArray(new ReplayFrame[replayFrames.size()]), osu); - if (r != null) + if (r != null && !unranked) r.save(); } ScoreData score = data.getScoreData(osu); // add score to database - if (!GameMod.AUTO.isActive() && !GameMod.RELAX.isActive() && !GameMod.AUTOPILOT.isActive() && !isReplay) + if (!unranked && !isReplay) ScoreDB.addScore(score); 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()) return; + // "autopilot" mod: ignore actual cursor coordinates + int cx, cy; + if (GameMod.AUTOPILOT.isActive()) { + cx = autoMouseX; + cy = autoMouseY; + } else { + cx = x; + cy = y; + } + // circles - if (hitObject.isCircle() && hitObjects[objectIndex].mousePressed(x, y, trackPosition)) + if (hitObject.isCircle() && hitObjects[objectIndex].mousePressed(cx, cy, trackPosition)) objectIndex++; // circle hit // sliders else if (hitObject.isSlider()) - hitObjects[objectIndex].mousePressed(x, y, trackPosition); + hitObjects[objectIndex].mousePressed(cx, cy, trackPosition); } @Override @@ -837,9 +1013,7 @@ public class Game extends BasicGameState { public void keyReleased(int key, char c) { if (!isReplay && (key == Options.getGameKeyLeft() || key == Options.getGameKeyRight())) { lastKeysPressed &= ~((key == Options.getGameKeyLeft()) ? ReplayFrame.KEY_K1 : ReplayFrame.KEY_K2); - int mouseX = input.getMouseX(); - int mouseY = input.getMouseY(); - frameAndRun(mouseX, mouseY, lastKeysPressed, MusicController.getPosition()); + frameAndRun(input.getMouseX(), input.getMouseY(), lastKeysPressed, MusicController.getPosition()); } } @@ -920,11 +1094,13 @@ public class Game extends BasicGameState { } } - // load replay frames - if (isReplay) { - // unhide cursor + // unhide cursor for "auto" mod and replays + if (GameMod.AUTO.isActive() || isReplay) UI.showCursor(); + lastReplayTime = 0; + // load replay frames + if (isReplay) { // load mods previousMods = GameMod.getModState(); GameMod.loadModState(replay.mods); @@ -953,7 +1129,6 @@ public class Game extends BasicGameState { // initialize replay-recording structures else { - lastReplayTime = 0; lastKeysPressed = ReplayFrame.KEY_NONE; replaySkipTime = -1; replayFrames = new LinkedList(); @@ -972,13 +1147,34 @@ public class Game extends BasicGameState { throws SlickException { // container.setMouseGrabbed(false); + // re-hide cursor + if (GameMod.AUTO.isActive() || isReplay) + UI.hideCursor(); + // replays if (isReplay) { 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 stack = new Stack(); + 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. * @param osu the OsuFile to load @@ -1016,6 +1212,9 @@ public class Game extends BasicGameState { deaths = 0; deathTime = -1; replayFrames = null; + autoMouseX = 0; + autoMouseY = 0; + autoMousePressed = false; System.gc(); } @@ -1216,16 +1415,31 @@ public class Game extends BasicGameState { private ReplayFrame addReplayFrame(int x, int y, int keys, int time) { int timeDiff = time - lastReplayTime; lastReplayTime = time; - int cx = unscaleX(x); - int cy = unscaleY(y); + int cx = (int) ((x - OsuHitObject.getXOffset()) / OsuHitObject.getXMultiplier()); + int cy = (int) ((y - OsuHitObject.getYOffset()) / OsuHitObject.getYMultiplier()); ReplayFrame tFrame = new ReplayFrame(timeDiff, time, cx, cy, keys); - replayFrames.add(tFrame); + if(replayFrames != null) + replayFrames.add(tFrame); return tFrame; } - private int unscaleX(int x){ - return (int) ((x - OsuHitObject.getXOffset()) / OsuHitObject.getXMultiplier()); - } - private int unscaleY(int y){ - return (int) ((y - OsuHitObject.getYOffset()) / OsuHitObject.getYMultiplier()); + + /** + * Returns the point at the t value between a start and end point. + * @param startX the starting x coordinate + * @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; } } diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index 7884b745..29440698 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -239,7 +239,7 @@ public class MainMenu extends BasicGameState { g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight, 4); g.setColor(Color.white); 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); } @@ -430,8 +430,7 @@ public class MainMenu extends BasicGameState { if (MusicController.isPlaying()) { if (musicPositionBarContains(x, y)) { float pos = (x - musicBarX) / musicBarWidth; - OsuFile osu = MusicController.getOsuFile(); - MusicController.setPosition((int) (pos * osu.endTime)); + MusicController.setPosition((int) (pos * MusicController.getDuration())); return; } }