From 40e67cedc9334f18f2a0755eb1eb1b9caea45443 Mon Sep 17 00:00:00 2001 From: fd Date: Sun, 5 Apr 2015 11:24:05 -0400 Subject: [PATCH] Spinner Test2 Improves Score accuracy (still mostly wrong) inital replay seek --- src/itdelatrisu/opsu/GameData.java | 53 +++++++- src/itdelatrisu/opsu/Options.java | 6 +- src/itdelatrisu/opsu/OsuFile.java | 1 + src/itdelatrisu/opsu/OsuGroupList.java | 2 +- src/itdelatrisu/opsu/ScoreData.java | 1 + src/itdelatrisu/opsu/UI.java | 3 +- src/itdelatrisu/opsu/db/ScoreDB.java | 9 +- src/itdelatrisu/opsu/io/OsuWriter.java | 3 +- src/itdelatrisu/opsu/objects/Circle.java | 35 +++-- src/itdelatrisu/opsu/objects/DummyObject.java | 5 + src/itdelatrisu/opsu/objects/HitObject.java | 5 + src/itdelatrisu/opsu/objects/Slider.java | 105 ++++++++++++--- src/itdelatrisu/opsu/objects/Spinner.java | 120 ++++++++++++------ src/itdelatrisu/opsu/replay/Replay.java | 12 +- .../opsu/replay/ReplayImporter.java | 13 +- src/itdelatrisu/opsu/states/Game.java | 61 ++++++++- src/itdelatrisu/opsu/states/Splash.java | 6 +- 17 files changed, 348 insertions(+), 92 deletions(-) diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index 16c99e99..666b15ed 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -267,6 +267,14 @@ public class GameData { /** Beatmap OverallDifficulty value. (0:easy ~ 10:hard) */ private float difficulty = 5f; + /** Beatmap ApproachRate value. (0:easy ~ 10:hard) */ + private float approachRate = 5f; + + /** Beatmap CircleSize value. (2:big ~ 7:small) */ + private float circleSize = 5f; + + private int difficultyMultiplier = -1; + /** Default text symbol images. */ private Image[] defaultSymbols; @@ -432,6 +440,18 @@ public class GameData { public void setDifficulty(float difficulty) { this.difficulty = difficulty; } public float getDifficulty() { return difficulty; } + /** + * Sets or returns the approach rate. + */ + public void setApproachRate(float approachRate) { this.approachRate = approachRate; } + public float getApproachRate() { return approachRate; } + + /** + * Sets or returns the approach rate. + */ + public void setCircleSize(float circleSize) { this.circleSize = circleSize; } + public float getCircleSize() { return circleSize; } + /** * Sets the array of hit result offsets. */ @@ -1118,6 +1138,10 @@ public class GameData { hitResultList.add(new OsuHitObjectResult(time, result, x, y, null, false)); } } + public void sliderFinalResult(int time, int hitSlider30, float x, float y, + OsuHitObject hitObject, int currentRepeats) { + score += 30; + } /** * Handles a hit result. @@ -1141,15 +1165,18 @@ public class GameData { changeHealth(5f); break; case HIT_100: + System.out.println("100! "+hitObject+" "+hitObject.getTime()+" "+hitObject.getTypeName()); hitValue = 100; changeHealth(2f); comboEnd |= 1; break; case HIT_50: + System.out.println("50! "+hitObject+" "+hitObject.getTime()+" "+hitObject.getTypeName()); hitValue = 50; comboEnd |= 2; break; case HIT_MISS: + System.out.println("miss! "+hitObject+" "+hitObject.getTime()+" "+hitObject.getTypeName()); hitValue = 0; changeHealth(-10f); comboEnd |= 2; @@ -1164,6 +1191,7 @@ public class GameData { hitObject.getSampleSet(repeat), hitObject.getAdditionSampleSet(repeat)); /** + * https://osu.ppy.sh/wiki/Score * [SCORE FORMULA] * Score = Hit Value + Hit Value * (Combo * Difficulty * Mod) / 25 * - Hit Value: hit result (50, 100, 300), slider ticks, spinner bonus @@ -1171,7 +1199,11 @@ public class GameData { * - Difficulty: the beatmap difficulty * - Mod: mod multipliers */ - score += (hitValue + (hitValue * (Math.max(combo - 1, 0) * difficulty * GameMod.getScoreMultiplier()) / 25)); + int comboMulti = Math.max(combo - 1, 0); + if(hitObject.isSlider()){ + comboMulti += 1; + } + score += (hitValue + (hitValue * (comboMulti * getDifficultyMultiplier() * GameMod.getScoreMultiplier()) / 25)); incrementComboStreak(); } hitResultCount[result]++; @@ -1205,6 +1237,23 @@ public class GameData { hitResultList.add(new OsuHitObjectResult(time, result, x, y, color, hitObject.isSpinner())); } + private int getDifficultyMultiplier() { + return difficultyMultiplier; + } + + public void calculateDifficultyMultiplier() { + //https://osu.ppy.sh/wiki/Score#How_to_calculate_the_Difficulty_multiplier + //TODO THE LIES ( difficultyMultiplier ) + /* + 924 3x1/4 beat notes 0.14stars + 924 3x1beat 0.28stars + 912 3x1beat wth 1 extra note 10 sec away 0.29stars + + seems to be based on hitobject density? (Total Objects/Time) + */ + + difficultyMultiplier = (int)((circleSize + difficulty + drainRate) / 6) + 2; + } /** * Returns a ScoreData object encapsulating all game data. * If score data already exists, the existing object will be returned @@ -1235,7 +1284,7 @@ public class GameData { scoreData.perfect = (comboMax == fullObjectCount); scoreData.mods = GameMod.getModState(); scoreData.replayString = (replay == null) ? null : replay.getReplayFilename(); - scoreData.playerName = "OpsuPlayer"; //TODO? + scoreData.playerName = "OpsuPlayer"; //TODO GameDataPlayerName? return scoreData; } diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index 16f71c4f..00a0f901 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -452,7 +452,7 @@ public class Options { private static Resolution resolution = Resolution.RES_1024_768; /** Frame limiters. */ - private static final int[] targetFPS = { 60, 120, 240 }; + private static final int[] targetFPS = { 60, 120, 240, 30, 20, 15, 12 }; /** Index in targetFPS[] array. */ private static int targetFPSindex = 0; @@ -830,8 +830,8 @@ public class Options { /** * Returns the replay import directory. - * If invalid, this will create and return a "SongPacks" directory. - * @return the OSZ archive directory + * If invalid, this will create and return a "ReplayImport" directory. + * @return the replay import directory */ public static File getReplayImportDir() { if (replayImportDir != null && replayImportDir.isDirectory()) diff --git a/src/itdelatrisu/opsu/OsuFile.java b/src/itdelatrisu/opsu/OsuFile.java index faa8bfeb..9e01abc3 100644 --- a/src/itdelatrisu/opsu/OsuFile.java +++ b/src/itdelatrisu/opsu/OsuFile.java @@ -180,6 +180,7 @@ public class OsuFile implements Comparable { /** Combo colors (max 8). */ public Color[] combo; + /** md5 hash of this file */ public String md5Hash; /** diff --git a/src/itdelatrisu/opsu/OsuGroupList.java b/src/itdelatrisu/opsu/OsuGroupList.java index ad5bdeb9..b7bbbe50 100644 --- a/src/itdelatrisu/opsu/OsuGroupList.java +++ b/src/itdelatrisu/opsu/OsuGroupList.java @@ -57,7 +57,7 @@ public class OsuGroupList { /** Set of all beatmap set IDs for the parsed beatmaps. */ private HashSet MSIDdb; - /** Set of all beatmap set IDs for the parsed beatmaps. */ + /** Map of all hash to OsuFile . */ public HashMap beatmapHashesToFile; diff --git a/src/itdelatrisu/opsu/ScoreData.java b/src/itdelatrisu/opsu/ScoreData.java index 82ba6875..7abb055d 100644 --- a/src/itdelatrisu/opsu/ScoreData.java +++ b/src/itdelatrisu/opsu/ScoreData.java @@ -77,6 +77,7 @@ public class ScoreData implements Comparable { /** The tooltip string. */ private String tooltip; + /** The players Name. */ public String playerName; /** Drawing values. */ diff --git a/src/itdelatrisu/opsu/UI.java b/src/itdelatrisu/opsu/UI.java index b5a2e95c..bbffec46 100644 --- a/src/itdelatrisu/opsu/UI.java +++ b/src/itdelatrisu/opsu/UI.java @@ -244,6 +244,7 @@ public class UI { int FPSmod = (Options.getTargetFPS() / 60); // TODO: use an image buffer + /* if (newStyle) { // new style: add all points between cursor movements if (lastX < 0) { @@ -264,7 +265,7 @@ public class UI { int max = 10 * FPSmod; if (cursorX.size() > max) removeCount = cursorX.size() - max; - } + }*/ // remove points from the lists for (int i = 0; i < removeCount && !cursorX.isEmpty(); i++) { diff --git a/src/itdelatrisu/opsu/db/ScoreDB.java b/src/itdelatrisu/opsu/db/ScoreDB.java index 679bd435..73869809 100644 --- a/src/itdelatrisu/opsu/db/ScoreDB.java +++ b/src/itdelatrisu/opsu/db/ScoreDB.java @@ -97,6 +97,11 @@ public class ScoreDB { // prepare sql statements try { + + //TODO timestamp as primary key should prevent importing the same replay multiple times + //but if for some magical reason two different replays has the same time stamp + //it will fail, such as copying replays from another drive? which will reset + //the last modified of the file. insertStmt = connection.prepareStatement( "INSERT OR IGNORE INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" ); @@ -116,7 +121,8 @@ public class ScoreDB { "DELETE FROM scores WHERE " + "timestamp = ? AND MID = ? AND MSID = ? AND title = ? AND artist = ? AND " + "creator = ? AND version = ? AND hit300 = ? AND hit100 = ? AND hit50 = ? AND " + - "geki = ? AND katu = ? AND miss = ? AND score = ? AND combo = ? AND perfect = ? AND mods = ?" + "geki = ? AND katu = ? AND miss = ? AND score = ? AND combo = ? AND perfect = ? AND mods = ? AND " + + "replay = ? AND playerName = ?" ); } catch (SQLException e) { ErrorHandler.error("Failed to prepare score statements.", e, true); @@ -286,6 +292,7 @@ public class ScoreDB { stmt.setInt(15, data.combo); stmt.setBoolean(16, data.perfect); stmt.setInt(17, data.mods); + stmt.setString(18, data.replayString); stmt.setString(19, data.playerName); } diff --git a/src/itdelatrisu/opsu/io/OsuWriter.java b/src/itdelatrisu/opsu/io/OsuWriter.java index eca07b3a..91b2efce 100644 --- a/src/itdelatrisu/opsu/io/OsuWriter.java +++ b/src/itdelatrisu/opsu/io/OsuWriter.java @@ -18,6 +18,7 @@ package itdelatrisu.opsu.io; +import java.io.BufferedOutputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileNotFoundException; @@ -51,7 +52,7 @@ public class OsuWriter { * @param dest the output stream to write to */ public OsuWriter(OutputStream dest) { - this.writer = new DataOutputStream(dest); + this.writer = new DataOutputStream(new BufferedOutputStream(dest)); } /** diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java index 6380078c..85f367da 100644 --- a/src/itdelatrisu/opsu/objects/Circle.java +++ b/src/itdelatrisu/opsu/objects/Circle.java @@ -36,6 +36,8 @@ public class Circle implements HitObject { /** The amount of time, in milliseconds, to fade in the circle. */ private static final int FADE_IN_TIME = 375; + private static float diameter; + /** The associated OsuHitObject. */ private OsuHitObject hitObject; @@ -54,17 +56,19 @@ public class Circle implements HitObject { /** Whether or not the circle result ends the combo streak. */ private boolean comboEnd; + /** * Initializes the Circle data type with map modifiers, images, and dimensions. * @param container the game container * @param circleSize the map's circleSize value */ public static void init(GameContainer container, float circleSize) { - int diameter = (int) (104 - (circleSize * 8)); - diameter = (int) (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480) - GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameter, diameter)); - GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameter, diameter)); - GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameter, diameter)); + diameter = (108 - (circleSize * 8)); + diameter = (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480) + int diameterInt = (int)diameter; + GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt)); + GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt)); + GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt)); } /** @@ -113,16 +117,19 @@ public class Circle implements HitObject { private int hitResult(int time) { int timeDiff = Math.abs(time); + int[] hitResultOffset = game.getHitResultOffsets(); int result = -1; - if (timeDiff < hitResultOffset[GameData.HIT_300]) + if (timeDiff <= hitResultOffset[GameData.HIT_300]) result = GameData.HIT_300; - else if (timeDiff < hitResultOffset[GameData.HIT_100]) + else if (timeDiff <= hitResultOffset[GameData.HIT_100]) result = GameData.HIT_100; - else if (timeDiff < hitResultOffset[GameData.HIT_50]) + else if (timeDiff <= hitResultOffset[GameData.HIT_50]) result = GameData.HIT_50; - else if (timeDiff < hitResultOffset[GameData.HIT_MISS]) + else if (timeDiff <= hitResultOffset[GameData.HIT_MISS]){ result = GameData.HIT_MISS; + System.out.println(timeDiff); + } //else not a hit return result; @@ -131,8 +138,7 @@ public class Circle implements HitObject { @Override public boolean mousePressed(int x, int y, int trackPosition) { double distance = Math.hypot(this.x - x, this.y - y); - int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2; - if (distance < circleRadius) { + if (distance < diameter/2) { int timeDiff = trackPosition - hitObject.getTime(); int result = hitResult(timeDiff); @@ -152,7 +158,7 @@ public class Circle implements HitObject { int[] hitResultOffset = game.getHitResultOffsets(); boolean isAutoMod = GameMod.AUTO.isActive(); - if (overlap || trackPosition > time + hitResultOffset[GameData.HIT_50]) { + if (trackPosition > time + hitResultOffset[GameData.HIT_50]) { if (isAutoMod) // "auto" mod: catch any missed notes due to lag data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, 0); @@ -187,4 +193,9 @@ public class Circle implements HitObject { this.x = hitObject.getScaledX(); this.y = hitObject.getScaledY(); } + + @Override + public void reset() { + + } } diff --git a/src/itdelatrisu/opsu/objects/DummyObject.java b/src/itdelatrisu/opsu/objects/DummyObject.java index 321d7dda..4f727345 100644 --- a/src/itdelatrisu/opsu/objects/DummyObject.java +++ b/src/itdelatrisu/opsu/objects/DummyObject.java @@ -63,4 +63,9 @@ public class DummyObject implements HitObject { this.x = hitObject.getScaledX(); this.y = hitObject.getScaledY(); } + + @Override + public void reset() { + + } } diff --git a/src/itdelatrisu/opsu/objects/HitObject.java b/src/itdelatrisu/opsu/objects/HitObject.java index b87247be..0be6c3e0 100644 --- a/src/itdelatrisu/opsu/objects/HitObject.java +++ b/src/itdelatrisu/opsu/objects/HitObject.java @@ -69,4 +69,9 @@ public interface HitObject { * Updates the position of the hit object. */ public void updatePosition(); + + /** + * Resets the hit object so that it can be reused. + */ + public void reset(); } diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 646e8c12..fb88aa11 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -46,6 +46,10 @@ public class Slider implements HitObject { /** Rate at which slider ticks are placed. */ private static float sliderTickRate = 1.0f; + + private static float followRadius; + + private static float diameter; /** The amount of time, in milliseconds, to fade in the slider. */ private static final int FADE_IN_TIME = 375; @@ -97,6 +101,8 @@ public class Slider implements HitObject { /** Container dimensions. */ private static int containerWidth, containerHeight; + + /** * Initializes the Slider data type with images and dimensions. @@ -107,10 +113,13 @@ public class Slider implements HitObject { 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) - + + diameter = (108 - (circleSize * 8)); + diameter = (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480) + int diameterInt = (int)diameter; + + followRadius = diameter / 2 * 3f; + // slider ball if (GameImage.SLIDER_BALL.hasSkinImages() || (!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null)) @@ -118,11 +127,11 @@ public class Slider implements HitObject { else sliderBallImages = new Image[]{ GameImage.SLIDER_BALL.getImage() }; for (int i = 0; i < sliderBallImages.length; i++) - sliderBallImages[i] = sliderBallImages[i].getScaledCopy(diameter * 118 / 128, diameter * 118 / 128); + sliderBallImages[i] = sliderBallImages[i].getScaledCopy(diameterInt * 118 / 128, diameterInt * 118 / 128); - GameImage.SLIDER_FOLLOWCIRCLE.setImage(GameImage.SLIDER_FOLLOWCIRCLE.getImage().getScaledCopy(diameter * 259 / 128, diameter * 259 / 128)); - GameImage.REVERSEARROW.setImage(GameImage.REVERSEARROW.getImage().getScaledCopy(diameter, diameter)); - GameImage.SLIDER_TICK.setImage(GameImage.SLIDER_TICK.getImage().getScaledCopy(diameter / 4, diameter / 4)); + GameImage.SLIDER_FOLLOWCIRCLE.setImage(GameImage.SLIDER_FOLLOWCIRCLE.getImage().getScaledCopy(diameterInt * 259 / 128, diameterInt * 259 / 128)); + GameImage.REVERSEARROW.setImage(GameImage.REVERSEARROW.getImage().getScaledCopy(diameterInt, diameterInt)); + GameImage.SLIDER_TICK.setImage(GameImage.SLIDER_TICK.getImage().getScaledCopy(diameterInt / 4, diameterInt / 4)); sliderMultiplier = osu.sliderMultiplier; sliderTickRate = osu.sliderTickRate; @@ -266,6 +275,55 @@ public class Slider implements HitObject { * @return the hit result (GameData.HIT_* constants) */ private int hitResult() { + /* + time scoredelta score-hit-initial-tick= unaccounted + (1/4 - 1) 396 - 300 - 30 46 + (1+1/4 - 2) 442 - 300 - 30 - 10 + (2+1/4 - 3) 488 - 300 - 30 - 2*10 896 (408)5x + (3+1/4 - 4) 534 - 300 - 30 - 3*10 + (4+1/4 - 5) 580 - 300 - 30 - 4*10 + (5+1/4 - 6) 626 - 300 - 30 - 5*10 + (6+1/4 - 7) 672 - 300 - 30 - 6*10 + + difficultyMulti = 3 (+36 per combo) + + score = + (t)ticks(10) * nticks + + (h)hitValue + (c)combo (hitValue/25 * difficultyMultiplier*(combo-1)) + (i)initialHit (30) + + (f)finalHit(30) + + + s t h c i f + 626 - 10*5 - 300 - 276(-216 - 30 - 30) (all)(7x) + 240 - 10*5 - 100 - 90 (-60 <- 30>) (no final or initial)(6x) + + 218 - 10*4 - 100 - 78 (-36 - 30) (4 tick no initial)(5x) + 196 - 10*3 - 100 - 66 (-24 - 30 ) (3 tick no initial)(4x) + 112 - 10*2 - 50 - 42 (-12 - 30 ) (2 tick no initial)(3x) + 96 - 10 - 50 - 36 ( -6 - 30 ) (1 tick no initial)(2x) + + 206 - 10*4 - 100 - 66 (-36 - 30 ) (4 tick no initial)(4x) + 184 - 10*3 - 100 - 54 (-24 - 30 ) (3 tick no initial)(3x) + 90 - 10 - 50 - 30 ( - 30 ) (1 tick no initial)(0x) + + 194 - 10*4 - 100 - 54 (-24 - 30 ) (4 tick no initial)(3x) + + 170 - 10*4 - 100 - 30 ( - 30 ) (4 tick no final)(0x) + 160 - 10*3 - 100 - 30 ( - 30 ) (3 tick no final)(0x) + 100 - 10*2 - 50 - 30 ( - 30 ) (2 tick no final)(0x) + + 198 - 10*5 - 100 - 48 (-36 ) (no initial and final)(5x) + 110 - 50 - ( - 30 - 30 ) (final and initial no tick)(0x) + 80 - 50 - ( <- 30> ) (only final or initial)(0x) + + 140 - 10*4 - 100 - 0 (4 ticks only)(0x) + 80 - 10*3 - 50 - 0 (3 tick only)(0x) + 70 - 10*2 - 50 - 0 (2 tick only)(0x) + 60 - 10 - 50 - 0 (1 tick only)(0x) + + + */ float tickRatio = (float) ticksHit / tickIntervals; int result; @@ -277,7 +335,7 @@ public class Slider implements HitObject { result = GameData.HIT_50; else result = GameData.HIT_MISS; - + if (currentRepeats % 2 == 0) { // last circle float[] lastPos = curve.pointAt(1); data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result, @@ -296,8 +354,7 @@ public class Slider implements HitObject { return false; double distance = Math.hypot(this.x - x, this.y - y); - int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2; - if (distance < circleRadius) { + if (distance < diameter / 2) { int timeDiff = Math.abs(trackPosition - hitObject.getTime()); int[] hitResultOffset = game.getHitResultOffsets(); @@ -353,26 +410,29 @@ public class Slider implements HitObject { } // end of slider - if (overlap || trackPosition > hitObject.getTime() + sliderTimeTotal) { + if (trackPosition > hitObject.getTime() + sliderTimeTotal) { tickIntervals++; // check if cursor pressed and within end circle if (keyPressed || GameMod.RELAX.isActive()) { float[] c = curve.pointAt(getT(trackPosition, false)); double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY); - int followCircleRadius = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getWidth() / 2; - if (distance < followCircleRadius) + if (distance < followRadius) //TODO IDK Magic numbers sliderClickedFinal = true; } // final circle hit - if (sliderClickedFinal) + if (sliderClickedFinal){ ticksHit++; + data.sliderFinalResult(hitObject.getTime(), GameData.HIT_SLIDER30, this.x, this.y, hitObject, currentRepeats); + } // "auto" mod: always send a perfect hit result if (isAutoMod) ticksHit = tickIntervals; + //TODO missing the final shouldn't increment the combo + // calculate and send slider result hitResult(); return true; @@ -405,8 +465,7 @@ public class Slider implements HitObject { // holding slider... float[] c = curve.pointAt(getT(trackPosition, false)); double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY); - int followCircleRadius = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getWidth() / 2; - if (((keyPressed || GameMod.RELAX.isActive()) && distance < followCircleRadius) || isAutoMod) { + if (((keyPressed || GameMod.RELAX.isActive()) && distance < followRadius) || isAutoMod) { // mouse pressed and within follow circle followCircleActive = true; data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER); @@ -487,4 +546,16 @@ public class Slider implements HitObject { return (floor % 2 == 0) ? t - floor : floor + 1 - t; } } + + @Override + public void reset() { + sliderClickedInitial = false; + sliderClickedFinal = false; + followCircleActive = false; + currentRepeats = 0; + tickIndex = 0; + ticksHit = 0; + tickIntervals = 1; + } + } diff --git a/src/itdelatrisu/opsu/objects/Spinner.java b/src/itdelatrisu/opsu/objects/Spinner.java index b82e7446..acc8c28d 100644 --- a/src/itdelatrisu/opsu/objects/Spinner.java +++ b/src/itdelatrisu/opsu/objects/Spinner.java @@ -40,12 +40,13 @@ public class Spinner implements HitObject { private static int width, height; /** The number of rotation velocities to store. */ - // note: currently takes about 200ms to spin up (4 * 50) - private static final int MAX_ROTATION_VELOCITIES = 50; + // note: currently takes about 200ms to spin up (1000/60 * 12) + private static final int MAX_ROTATION_VELOCITIES = 12; /** The amount of time, in milliseconds, before another velocity is stored. */ - private static final int DELTA_UPDATE_TIME = 16; + private static final float DELTA_UPDATE_TIME = 1000 / 60f; + /** The amount of time, in milliseconds, to fade in the spinner. */ private static final int FADE_IN_TIME = 500; @@ -59,6 +60,8 @@ public class Spinner implements HitObject { TWO_PI = (float) (Math.PI * 2), HALF_PI = (float) (Math.PI / 2); + private static final float MAX_ANG_DIFF = DELTA_UPDATE_TIME * 477 / 60 / 1000 * TWO_PI; // ~95.3 + /** The associated OsuHitObject. */ private OsuHitObject hitObject; @@ -78,19 +81,23 @@ public class Spinner implements HitObject { private float rotationsNeeded; /** The remaining amount of time that was not used. */ - private int deltaOverflow; + private float deltaOverflow; /** The sum of all the velocities in storedVelocities. */ - private float sumVelocity = 0f; + private float sumDeltaAngle = 0f; /** Array holding the most recent rotation velocities. */ - private float[] storedVelocities = new float[MAX_ROTATION_VELOCITIES]; + private float[] storedDeltaAngle = new float[MAX_ROTATION_VELOCITIES]; /** True if the mouse cursor is pressed. */ private boolean isSpinning; /** Current index of the stored velocities in rotations/second. */ - private int velocityIndex = 0; + private int deltaAngleIndex = 0; + + private float deltaAngleOverflow = 0; + + private int drawnRPM = 0; /** * Initializes the Spinner data type with images and dimensions. @@ -112,8 +119,9 @@ public class Spinner implements HitObject { this.data = data; // calculate rotations needed - float spinsPerMinute = 100 + (data.getDifficulty() * 15); + float spinsPerMinute = 94 + (data.getDifficulty() * 15); rotationsNeeded = spinsPerMinute * (hitObject.getEndTime() - hitObject.getTime()) / 60000f; + System.out.println("rotationsNeeded "+rotationsNeeded); } @Override @@ -135,12 +143,11 @@ public class Spinner implements HitObject { Utils.COLOR_BLACK_ALPHA.a = oldAlpha; // rpm - int rpm = Math.abs(Math.round(sumVelocity / storedVelocities.length * 60)); Image rpmImg = GameImage.SPINNER_RPM.getImage(); rpmImg.setAlpha(alpha); rpmImg.drawCentered(width / 2f, height - rpmImg.getHeight() / 2f); if (timeDiff < 0) - data.drawSymbolString(Integer.toString(rpm), (width + rpmImg.getWidth() * 0.95f) / 2f, + data.drawSymbolString(Integer.toString(drawnRPM), (width + rpmImg.getWidth() * 0.95f) / 2f, height - data.getScoreSymbolImage('0').getHeight() * 1.025f, 1f, 1f, true); // spinner meter (subimage) @@ -196,7 +203,11 @@ public class Spinner implements HitObject { } @Override - public boolean mousePressed(int x, int y, int trackPosition) { return false; } // not used + public boolean mousePressed(int x, int y, int trackPosition) { + lastAngle = (float) Math.atan2(x - (height / 2), y - (width / 2)); + System.out.println("lastAngle:"+lastAngle); + return false; + } @Override public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed, int trackPosition) { @@ -211,22 +222,25 @@ public class Spinner implements HitObject { if (isSpinning && !(keyPressed || GameMod.RELAX.isActive())) isSpinning = false; + System.out.println("Spinner update "+mouseX+" "+mouseY+" "+deltaOverflow); + // spin automatically + // http://osu.ppy.sh/wiki/FAQ#Spinners + + deltaOverflow += delta; - while (deltaOverflow >= DELTA_UPDATE_TIME) { - // spin automatically - // http://osu.ppy.sh/wiki/FAQ#Spinners - float angle; + + float angle = 0; + if (deltaOverflow >= DELTA_UPDATE_TIME){ if (GameMod.AUTO.isActive()) { lastAngle = 0; - angle = delta * AUTO_MULTIPLIER; + angle = deltaOverflow * AUTO_MULTIPLIER; isSpinning = true; } else if (GameMod.SPUN_OUT.isActive() || GameMod.AUTOPILOT.isActive()) { lastAngle = 0; - angle = delta * SPUN_OUT_MULTIPLIER; + angle = deltaOverflow * SPUN_OUT_MULTIPLIER; isSpinning = true; } else { angle = (float) Math.atan2(mouseY - (height / 2), mouseX - (width / 2)); - // set initial angle to current mouse position to skip first click if (!isSpinning && (keyPressed || GameMod.RELAX.isActive())) { lastAngle = angle; @@ -234,33 +248,45 @@ public class Spinner implements HitObject { return false; } } - // make angleDiff the smallest angle change possible // (i.e. 1/4 rotation instead of 3/4 rotation) float angleDiff = angle - lastAngle; + lastAngle = angle; + if (angleDiff < -Math.PI) angleDiff += TWO_PI; else if (angleDiff > Math.PI) angleDiff -= TWO_PI; - - System.out.println("AngleDiff "+angleDiff); - // spin caused by the cursor - float cursorVelocity = 0; if (isSpinning) - cursorVelocity = Utils.clamp(angleDiff / TWO_PI / delta * 1000, -8f, 8f); - - sumVelocity -= storedVelocities[velocityIndex]; - sumVelocity += cursorVelocity; - storedVelocities[velocityIndex++] = cursorVelocity; - velocityIndex %= storedVelocities.length; - deltaOverflow -= DELTA_UPDATE_TIME; + deltaAngleOverflow += angleDiff; + } + + while (deltaOverflow >= DELTA_UPDATE_TIME) { + System.out.println("Spinner update2 "+mouseX+" "+mouseY+" "+deltaAngleOverflow+" "+deltaOverflow); - float rotationAngle = sumVelocity / storedVelocities.length * TWO_PI * delta / 1000; + // spin caused by the cursor + float deltaAngle = 0; + if (isSpinning){ + deltaAngle = deltaAngleOverflow * DELTA_UPDATE_TIME / deltaOverflow; + deltaAngleOverflow -= deltaAngle; + deltaAngle = Utils.clamp(deltaAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF); + } + sumDeltaAngle -= storedDeltaAngle[deltaAngleIndex]; + sumDeltaAngle += deltaAngle; + storedDeltaAngle[deltaAngleIndex++] = deltaAngle; + deltaAngleIndex %= storedDeltaAngle.length; + deltaOverflow -= DELTA_UPDATE_TIME; + + float rotationAngle = sumDeltaAngle / MAX_ROTATION_VELOCITIES; + rotationAngle = Utils.clamp(rotationAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF);//*0.9650f; + float rotationPerSec = rotationAngle * (1000/DELTA_UPDATE_TIME) / TWO_PI; + + drawnRPM = (int)(Math.abs(rotationPerSec * 60)); + System.out.println("Ang DIFF:"+deltaAngle+" "+rotations+" "+angle+" "+lastAngle+" "+rotationAngle+" "+sumDeltaAngle+" "+MAX_ANG_DIFF); rotate(rotationAngle); if (Math.abs(rotationAngle) > 0.00001f) - data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER); - - lastAngle = angle; + data.changeHealth(DELTA_UPDATE_TIME * GameData.HP_DRAIN_MULTIPLIER); + } return false; } @@ -304,15 +330,37 @@ public class Spinner implements HitObject { // added one whole rotation... if (Math.floor(newRotations) > rotations) { + //TODO seems to give 1100 points per spin but also an extra 100 for some spinners if (newRotations > rotationsNeeded) { // extra rotations data.changeScore(1000); + SoundController.playSound(SoundEffect.SPINNERBONUS); - } else { + } + data.changeScore(100); + SoundController.playSound(SoundEffect.SPINNERSPIN); + + } + //* + if (Math.floor(newRotations + 0.5f) > rotations + 0.5f) { + if (newRotations + 0.5f > rotationsNeeded) { // extra rotations data.changeScore(100); - SoundController.playSound(SoundEffect.SPINNERSPIN); } } + //*/ rotations = newRotations; } + + @Override + public void reset() { + deltaAngleIndex = 0; + sumDeltaAngle = 0; + for(int i=0; i lifeFrameList = new ArrayList(lifeData.length); @@ -369,8 +375,8 @@ public class Replay { @Override public String toString() { - final int LINE_SPLIT = 5; - final int MAX_LINES = LINE_SPLIT * 10; + final int LINE_SPLIT = 1; + final int MAX_LINES = LINE_SPLIT * 99999999; StringBuilder sb = new StringBuilder(); sb.append("File: "); sb.append(file.getName()); sb.append('\n'); diff --git a/src/itdelatrisu/opsu/replay/ReplayImporter.java b/src/itdelatrisu/opsu/replay/ReplayImporter.java index a564aff8..1893f32c 100644 --- a/src/itdelatrisu/opsu/replay/ReplayImporter.java +++ b/src/itdelatrisu/opsu/replay/ReplayImporter.java @@ -12,16 +12,22 @@ import java.io.IOException; public class ReplayImporter { public static void importAllReplaysFromDir(File dir) { - System.out.println(OsuGroupList.get().beatmapHashesToFile); for (File replayToImport : dir.listFiles()) { try { Replay r = new Replay(replayToImport); - r.load(); + r.loadHeader(); OsuFile oFile = OsuGroupList.get().getFileFromBeatmapHash(r.beatmapHash); if(oFile != null){ + File replaydir = Options.getReplayDir(); + if (!replaydir.isDirectory()) { + if (!replaydir.mkdir()) { + ErrorHandler.error("Failed to create replay directory.", null, false); + return; + } + } //ErrorHandler.error("Importing"+replayToImport+" forBeatmap:"+oFile, null, false); ScoreData data = r.getScoreData(oFile); - File moveToFile = new File(Options.getReplayDir(),replayToImport.getName()); + File moveToFile = new File(replaydir, replayToImport.getName()); System.out.println("Moving "+replayToImport+" to "+moveToFile); if( !replayToImport.renameTo(moveToFile) @@ -35,7 +41,6 @@ public class ReplayImporter { //ErrorHandler.error("Could not find beatmap for replay "+replayToImport, null, false); } } catch (IOException e) { - // TODO Auto-generated catch block //e.printStackTrace(); System.out.println(e); } diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 094116f5..4e6a4479 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -217,6 +217,10 @@ public class Game extends BasicGameState { private Input input; private int state; + private int width; + + private int height; + public Game(int state) { this.state = state; } @@ -228,8 +232,8 @@ public class Game extends BasicGameState { this.game = game; input = container.getInput(); - int width = container.getWidth(); - int height = container.getHeight(); + width = container.getWidth(); + height = container.getHeight(); // create offscreen graphics offscreen = new Image(width, height); @@ -597,6 +601,27 @@ public class Game extends BasicGameState { // out of frames, use previous data if (replayIndex >= replay.frames.length) updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed); + + //TODO probably should to disable sounds then reseek to the new position + if(replayIndex-1 >= 1 && replayIndex < replay.frames.length && trackPosition < replay.frames[replayIndex-1].getTime()){ + replayIndex = 0; + while(objectIndex>=0){ + hitObjects[objectIndex].reset(); + objectIndex--; + + } + // load the first timingPoint + if (!osu.timingPoints.isEmpty()) { + OsuTimingPoint timingPoint = osu.timingPoints.get(0); + if (!timingPoint.isInherited()) { + beatLengthBase = beatLength = timingPoint.getBeatLength(); + HitSound.setDefaultSampleSet(timingPoint.getSampleType()); + SoundController.setSampleVolume(timingPoint.getSampleVolume()); + timingPointIndex++; + } + } + resetGameData(); + } // update and run replay frames while (replayIndex < replay.frames.length && trackPosition >= replay.frames[replayIndex].getTime()) { @@ -743,7 +768,7 @@ public class Game extends BasicGameState { while (objectIndex < hitObjects.length && trackPosition > osu.objects[objectIndex].getTime()) { // check if we've already passed the next object's start time boolean overlap = (objectIndex + 1 < hitObjects.length && - trackPosition > osu.objects[objectIndex + 1].getTime() - hitResultOffset[GameData.HIT_300]); + trackPosition > osu.objects[objectIndex + 1].getTime() - hitResultOffset[GameData.HIT_50]); // update hit object and check completion status if (hitObjects[objectIndex].update(overlap, delta, mouseX, mouseY, keyPressed, trackPosition)) @@ -879,6 +904,11 @@ public class Game extends BasicGameState { // only allow skip button if (button != Input.MOUSE_MIDDLE_BUTTON && skipButton.contains(x, y)) skipIntro(); + if(y < 50){ + float pos = (float)x / width * osu.endTime; + System.out.println("Seek to"+pos); + MusicController.setPosition((int)pos); + } return; } @@ -1028,13 +1058,21 @@ public class Game extends BasicGameState { MusicController.setPosition(0); MusicController.pause(); + if (!osu.timingPoints.isEmpty()) { + OsuTimingPoint timingPoint = osu.timingPoints.get(0); + if (!timingPoint.isInherited()) { + beatLengthBase = beatLength = timingPoint.getBeatLength(); + } + } + hitObjects = new HitObject[osu.objects.length]; + // initialize object maps for (int i = 0; i < osu.objects.length; i++) { OsuHitObject hitObject = osu.objects[i]; // is this the last note in the combo? boolean comboEnd = false; - if (i + 1 < osu.objects.length && osu.objects[i + 1].isNewCombo()) + if (i + 1 >= osu.objects.length || osu.objects[i + 1].isNewCombo()) comboEnd = true; Color color = osu.combo[hitObject.getComboIndex()]; @@ -1086,6 +1124,7 @@ public class Game extends BasicGameState { // load replay frames if (isReplay) { + //System.out.println(replay.toString()); // load mods previousMods = GameMod.getModState(); GameMod.loadModState(replay.mods); @@ -1238,7 +1277,6 @@ public class Game extends BasicGameState { * Resets all game data and structures. */ public void resetGameData() { - hitObjects = new HitObject[osu.objects.length]; data.clear(); objectIndex = 0; breakIndex = 0; @@ -1376,15 +1414,25 @@ public class Game extends BasicGameState { // overallDifficulty (hit result time offsets) hitResultOffset = new int[GameData.HIT_MAX]; + //* + float mult = 0.608f; + hitResultOffset[GameData.HIT_300] = (int) ((128 - (overallDifficulty * 9.6))*mult); + hitResultOffset[GameData.HIT_100] = (int) ((224 - (overallDifficulty * 12.8))*mult); + hitResultOffset[GameData.HIT_50] = (int) ((320 - (overallDifficulty * 16))*mult); + hitResultOffset[GameData.HIT_MISS] = (int) ((1000 - (overallDifficulty * 10))*mult); + /*/ hitResultOffset[GameData.HIT_300] = (int) (78 - (overallDifficulty * 6)); hitResultOffset[GameData.HIT_100] = (int) (138 - (overallDifficulty * 8)); hitResultOffset[GameData.HIT_50] = (int) (198 - (overallDifficulty * 10)); hitResultOffset[GameData.HIT_MISS] = (int) (500 - (overallDifficulty * 10)); - + //*/ // HPDrainRate (health change), overallDifficulty (scoring) data.setDrainRate(HPDrainRate); data.setDifficulty(overallDifficulty); + data.setApproachRate(approachRate); + data.setCircleSize(circleSize); data.setHitResultOffset(hitResultOffset); + data.calculateDifficultyMultiplier(); } /** @@ -1491,6 +1539,7 @@ public class Game extends BasicGameState { * @param keys the keys that are pressed */ private void sendGameKeyPress(int keys, int x, int y, int trackPosition) { + System.out.println("Game Key Pressed"+keys+" "+x+" "+y+" "+objectIndex); if (objectIndex >= hitObjects.length) // nothing to do here return; diff --git a/src/itdelatrisu/opsu/states/Splash.java b/src/itdelatrisu/opsu/states/Splash.java index c4cea99e..90ee8fcf 100644 --- a/src/itdelatrisu/opsu/states/Splash.java +++ b/src/itdelatrisu/opsu/states/Splash.java @@ -18,11 +18,9 @@ package itdelatrisu.opsu.states; -import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.Options; -import itdelatrisu.opsu.OsuFile; import itdelatrisu.opsu.OsuGroupList; import itdelatrisu.opsu.OsuParser; import itdelatrisu.opsu.OszUnpacker; @@ -30,11 +28,9 @@ import itdelatrisu.opsu.UI; import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.SoundController; -import itdelatrisu.opsu.replay.Replay; import itdelatrisu.opsu.replay.ReplayImporter; import java.io.File; -import java.io.IOException; import org.newdawn.slick.Color; import org.newdawn.slick.GameContainer; @@ -44,7 +40,6 @@ import org.newdawn.slick.Input; import org.newdawn.slick.SlickException; import org.newdawn.slick.state.BasicGameState; import org.newdawn.slick.state.StateBasedGame; -import org.newdawn.slick.util.Log; /** * "Splash Screen" state. @@ -111,6 +106,7 @@ public class Splash extends BasicGameState { // parse song directory OsuParser.parseAllFiles(beatmapDir); + // import replays ReplayImporter.importAllReplaysFromDir(Options.getReplayImportDir()); // load sounds