diff --git a/res/loader.png b/res/loader.png new file mode 100644 index 00000000..11264170 Binary files /dev/null and b/res/loader.png differ diff --git a/src/itdelatrisu/opsu/GameScore.java b/src/itdelatrisu/opsu/GameScore.java index ec4ba962..e1bf76b3 100644 --- a/src/itdelatrisu/opsu/GameScore.java +++ b/src/itdelatrisu/opsu/GameScore.java @@ -478,11 +478,10 @@ public class GameScore { * scorebar, score, score percentage, map progress circle, * mod icons, combo count, combo burst, and grade. * @param g the graphics context - * @param mapLength the length of the beatmap (in ms) * @param breakPeriod if true, will not draw scorebar and combo elements, and will draw grade * @param firstObject true if the first hit object's start time has not yet passed */ - public void drawGameElements(Graphics g, int mapLength, boolean breakPeriod, boolean firstObject) { + public void drawGameElements(Graphics g, boolean breakPeriod, boolean firstObject) { // score drawSymbolString((scoreDisplay < 100000000) ? String.format("%08d", scoreDisplay) : Long.toString(scoreDisplay), width - 2, 0, 1.0f, true); @@ -500,11 +499,19 @@ public class GameScore { float circleDiameter = symbolHeight * 0.75f; g.drawOval(circleX, symbolHeight, circleDiameter, circleDiameter); - int firstObjectTime = MusicController.getOsuFile().objects[0].time; + OsuFile osu = MusicController.getOsuFile(); + int firstObjectTime = osu.objects[0].time; int trackPosition = MusicController.getPosition(); if (trackPosition > firstObjectTime) { + // map progress (white) g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter, - -90, -90 + (int) (360f * (trackPosition - firstObjectTime) / mapLength) + -90, -90 + (int) (360f * (trackPosition - firstObjectTime) / (osu.endTime - firstObjectTime)) + ); + } else { + // lead-in time (yellow) + g.setColor(Utils.COLOR_YELLOW_ALPHA); + g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter, + -90 + (int) (360f * trackPosition / firstObjectTime), -90 ); } diff --git a/src/itdelatrisu/opsu/MusicController.java b/src/itdelatrisu/opsu/MusicController.java index 83370519..370176fa 100644 --- a/src/itdelatrisu/opsu/MusicController.java +++ b/src/itdelatrisu/opsu/MusicController.java @@ -23,7 +23,6 @@ import itdelatrisu.opsu.states.Options; import java.io.File; import java.lang.reflect.Field; import java.nio.IntBuffer; -import java.util.concurrent.TimeUnit; import javazoom.jl.converter.Converter; @@ -249,48 +248,6 @@ public class MusicController { return (trackExists() && player.setPosition(position / 1000f)); } - /** - * Gets the length of the track, in milliseconds. - * Returns 0 if no file is loaded or a track is currently being loaded. - * @author bdk (http://slick.ninjacave.com/forum/viewtopic.php?t=2699) - */ - public static int getTrackLength() { - if (!trackExists() || isTrackLoading()) - return 0; - - float duration = 0f; - try { - // get Music object's (private) Audio object reference - Field sound = player.getClass().getDeclaredField("sound"); - sound.setAccessible(true); - Audio audio = (Audio) (sound.get(player)); - - // access Audio object's (private)'length' field - Field length = audio.getClass().getDeclaredField("length"); - length.setAccessible(true); - duration = (float) (length.get(audio)); - } catch (Exception e) { - Log.debug("Could not get track length."); - return 0; - } - return (int) (duration * 1000); - } - - /** - * Gets the length of the track as a formatted string (M:SS). - * Returns "--" if a track is currently being loaded. - */ - public static String getTrackLengthString() { - if (isTrackLoading()) - return "..."; - - int duration = getTrackLength(); - return String.format("%d:%02d", - TimeUnit.MILLISECONDS.toMinutes(duration), - TimeUnit.MILLISECONDS.toSeconds(duration) - - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(duration))); - } - /** * Stops and releases all sources, clears each of the specified Audio * buffers, destroys the OpenAL context, and resets SoundStore for future use. diff --git a/src/itdelatrisu/opsu/OsuFile.java b/src/itdelatrisu/opsu/OsuFile.java index 44167467..fd5bc546 100644 --- a/src/itdelatrisu/opsu/OsuFile.java +++ b/src/itdelatrisu/opsu/OsuFile.java @@ -96,6 +96,7 @@ public class OsuFile implements Comparable { public int hitObjectCircle = 0; // number of circles public int hitObjectSlider = 0; // number of sliders public int hitObjectSpinner = 0; // number of spinners + public int endTime = -1; // last object end time (in ms) /** * Constructor. diff --git a/src/itdelatrisu/opsu/OsuGroupList.java b/src/itdelatrisu/opsu/OsuGroupList.java index f3a840be..4d4a282b 100644 --- a/src/itdelatrisu/opsu/OsuGroupList.java +++ b/src/itdelatrisu/opsu/OsuGroupList.java @@ -38,13 +38,14 @@ public class OsuGroupList { SORT_ARTIST = 1, SORT_CREATOR = 2, SORT_BPM = 3, - SORT_MAX = 4; // not a sort + SORT_LENGTH = 4, + SORT_MAX = 5; // not a sort /** * Sorting order names (indexed by SORT_* constants). */ public static final String[] SORT_NAMES = { - "Title", "Artist", "Creator", "BPM" + "Title", "Artist", "Creator", "BPM", "Length" }; /** @@ -240,6 +241,9 @@ public class OsuGroupList { case SORT_BPM: Collections.sort(nodes, new OsuGroupNode.BPMOrder()); break; + case SORT_LENGTH: + Collections.sort(nodes, new OsuGroupNode.LengthOrder()); + break; } expandedIndex = -1; diff --git a/src/itdelatrisu/opsu/OsuGroupNode.java b/src/itdelatrisu/opsu/OsuGroupNode.java index bf12f5c5..10df7c0a 100644 --- a/src/itdelatrisu/opsu/OsuGroupNode.java +++ b/src/itdelatrisu/opsu/OsuGroupNode.java @@ -20,6 +20,7 @@ package itdelatrisu.opsu; import java.util.ArrayList; import java.util.Comparator; +import java.util.concurrent.TimeUnit; import org.newdawn.slick.Color; import org.newdawn.slick.Image; @@ -100,6 +101,26 @@ public class OsuGroupNode implements Comparable { } } + /** + * Compares two OsuGroupNode objects by length. + * Uses the longest beatmap in each set for comparison. + */ + public static class LengthOrder implements Comparator { + @Override + public int compare(OsuGroupNode v, OsuGroupNode w) { + int vMax = 0, wMax = 0; + for (OsuFile osu : v.osuFiles) { + if (osu.endTime > vMax) + vMax = osu.endTime; + } + for (OsuFile osu : w.osuFiles) { + if (osu.endTime > wMax) + wMax = osu.endTime; + } + return Integer.compare(vMax, wMax); + } + } + /** * Sets a button background image. */ @@ -162,8 +183,10 @@ public class OsuGroupNode implements Comparable { info[0] = osu.toString(); info[1] = String.format("Mapped by %s", osu.creator); - info[2] = String.format("Length: %s BPM: %s Objects: %d", - MusicController.getTrackLengthString(), + info[2] = String.format("Length: %d:%02d BPM: %s Objects: %d", + TimeUnit.MILLISECONDS.toMinutes(osu.endTime), + TimeUnit.MILLISECONDS.toSeconds(osu.endTime) - + TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(osu.endTime)), (osu.bpmMax <= 0) ? "--" : ((osu.bpmMin == osu.bpmMax) ? osu.bpmMin : String.format("%d-%d", osu.bpmMin, osu.bpmMax)), (osu.hitObjectCircle + osu.hitObjectSlider + osu.hitObjectSpinner)); @@ -232,7 +255,7 @@ public class OsuGroupNode implements Comparable { case "od": osuValue = osu.overallDifficulty; break; case "hp": osuValue = osu.HPDrainRate; break; case "bpm": osuValue = osu.bpmMax; break; -// case "length": /* not implemented */ break; + case "length": osuValue = osu.endTime / 1000; break; default: return false; } diff --git a/src/itdelatrisu/opsu/OsuParser.java b/src/itdelatrisu/opsu/OsuParser.java index c041dfdb..bd04266b 100644 --- a/src/itdelatrisu/opsu/OsuParser.java +++ b/src/itdelatrisu/opsu/OsuParser.java @@ -131,7 +131,7 @@ public class OsuParser { osu.timingPoints = new ArrayList(); String line = in.readLine(); - String tokens[]; + String tokens[] = null; while (line != null) { line = line.trim(); if (!isValidLine(line)) { @@ -386,6 +386,7 @@ public class OsuParser { osu.combo = colors.toArray(new Color[colors.size()]); break; case "[HitObjects]": + int type = -1; while ((line = in.readLine()) != null) { line = line.trim(); if (!isValidLine(line)) @@ -394,7 +395,7 @@ public class OsuParser { break; /* Only type counts parsed at this time. */ tokens = line.split(","); - int type = Integer.parseInt(tokens[3]); + type = Integer.parseInt(tokens[3]); if ((type & OsuHitObject.TYPE_CIRCLE) > 0) osu.hitObjectCircle++; else if ((type & OsuHitObject.TYPE_SLIDER) > 0) @@ -402,6 +403,16 @@ public class OsuParser { else //if ((type & OsuHitObject.TYPE_SPINNER) > 0) osu.hitObjectSpinner++; } + + // map length = last object end time (TODO: end on slider?) + if ((type & OsuHitObject.TYPE_SPINNER) > 0) { + // some 'endTime' fields contain a ':' character (?) + int index = tokens[5].indexOf(':'); + if (index != -1) + tokens[5] = tokens[5].substring(0, index); + osu.endTime = Integer.parseInt(tokens[5]); + } else + osu.endTime = Integer.parseInt(tokens[2]); break; default: line = in.readLine(); diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index a5744378..a5713b4d 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -59,7 +59,8 @@ public class Utils { COLOR_GREEN_OBJECT = new Color(26, 207, 26), COLOR_BLUE_OBJECT = new Color(46, 136, 248), COLOR_RED_OBJECT = new Color(243, 48, 77), - COLOR_ORANGE_OBJECT = new Color(255, 200, 32); + COLOR_ORANGE_OBJECT = new Color(255, 200, 32), + COLOR_YELLOW_ALPHA = new Color(255, 255, 0, 0.4f); /** * The default map colors, used when a map does not provide custom colors. diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 3c910954..e3dad7eb 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -114,11 +114,6 @@ public class Game extends BasicGameState { */ private int[] hitResultOffset; - /** - * Time, in milliseconds, between the first and last hit object. - */ - private int mapLength; - /** * Current break index in breaks ArrayList. */ @@ -274,7 +269,7 @@ public class Game extends BasicGameState { g.fillRect(0, height * 0.875f, width, height * 0.125f); } - score.drawGameElements(g, mapLength, true, objectIndex == 0); + score.drawGameElements(g, true, objectIndex == 0); if (breakLength >= 8000 && trackPosition - breakTime > 2000 && @@ -317,7 +312,7 @@ public class Game extends BasicGameState { } // game elements - score.drawGameElements(g, mapLength, false, objectIndex == 0); + score.drawGameElements(g, false, objectIndex == 0); // skip beginning if (objectIndex == 0 && @@ -624,7 +619,7 @@ public class Game extends BasicGameState { // load checkpoint if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) { int checkpoint = Options.getCheckpoint(); - if (checkpoint == 0 || checkpoint > MusicController.getTrackLength()) + if (checkpoint == 0 || checkpoint > osu.endTime) break; // invalid checkpoint try { restart = RESTART_MANUAL; @@ -728,15 +723,6 @@ public class Game extends BasicGameState { if (restart == RESTART_NEW) { loadImages(); setMapModifiers(); - - // calculate map length (TODO: end on slider?) - OsuHitObject lastObject = osu.objects[osu.objects.length - 1]; - int endTime; - if ((lastObject.type & OsuHitObject.TYPE_SPINNER) > 0) - endTime = lastObject.endTime; - else - endTime = lastObject.time; - mapLength = endTime - osu.objects[0].time; } // initialize object maps diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index d187e9b6..1c7de45f 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -190,7 +190,7 @@ public class MainMenu extends BasicGameState { g.setColor(Color.white); if (!MusicController.isTrackLoading()) g.fillRoundRect(width - 168, 54, - 148f * MusicController.getPosition() / MusicController.getTrackLength(), 5, 4); + 148f * MusicController.getPosition() / osu.endTime, 5, 4); // draw text g.setFont(Utils.FONT_MEDIUM); diff --git a/src/itdelatrisu/opsu/states/MainMenuExit.java b/src/itdelatrisu/opsu/states/MainMenuExit.java index 858505d6..b77275b0 100644 --- a/src/itdelatrisu/opsu/states/MainMenuExit.java +++ b/src/itdelatrisu/opsu/states/MainMenuExit.java @@ -120,9 +120,9 @@ public class MainMenuExit extends BasicGameState { float yesX = yesButton.getX(), noX = noButton.getX(); float center = container.getWidth() / 2f; if (yesX < center) - yesButton.setX(Math.min(yesX + (delta / 6f), center)); + yesButton.setX(Math.min(yesX + (delta / 5f), center)); if (noX > center) - noButton.setX(Math.max(noX - (delta / 6f), center)); + noButton.setX(Math.max(noX - (delta / 5f), center)); } @Override diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index bea220a6..8dc0e60a 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -29,12 +29,14 @@ import itdelatrisu.opsu.SoundController; import itdelatrisu.opsu.Utils; import org.lwjgl.opengl.Display; +import org.newdawn.slick.Animation; import org.newdawn.slick.Color; import org.newdawn.slick.GameContainer; import org.newdawn.slick.Graphics; import org.newdawn.slick.Image; import org.newdawn.slick.Input; import org.newdawn.slick.SlickException; +import org.newdawn.slick.SpriteSheet; import org.newdawn.slick.gui.TextField; import org.newdawn.slick.state.BasicGameState; import org.newdawn.slick.state.StateBasedGame; @@ -128,6 +130,11 @@ public class SongMenu extends BasicGameState { */ private Image musicNote; + /** + * Loader animation. + */ + private Animation loader; + // game-related variables private GameContainer container; private StateBasedGame game; @@ -164,7 +171,7 @@ public class SongMenu extends BasicGameState { Image tab = Utils.getTabImage(); float tabX = buttonX + (tab.getWidth() / 2f); float tabY = (height * 0.15f) - (tab.getHeight() / 2f) - 2f; - float tabOffset = (width - buttonX) / sortTabs.length; + float tabOffset = (width - buttonX - tab.getWidth()) / (sortTabs.length - 1); for (int i = 0; i < sortTabs.length; i++) sortTabs[i] = new GUIMenuButton(tab, tabX + (i * tabOffset), tabY); @@ -178,7 +185,7 @@ public class SongMenu extends BasicGameState { search = new TextField( container, Utils.FONT_DEFAULT, - (int) tabX + searchIcon.getWidth(), (int) ((height * 0.15f) - (tab.getHeight() * 5 / 2f)), + (int) tabX + searchIcon.getWidth(), (int) ((height * 0.15f) - (tab.getHeight() * 2.5f)), (int) (buttonWidth / 2), Utils.FONT_DEFAULT.getHeight() ); search.setBackgroundColor(Color.transparent); @@ -191,8 +198,16 @@ public class SongMenu extends BasicGameState { Image optionsIcon = new Image("options.png").getScaledCopy(iconScale); optionsButton = new GUIMenuButton(optionsIcon, search.getX() - (optionsIcon.getWidth() * 1.5f), search.getY()); + // music note int musicNoteDim = (int) (Utils.FONT_LARGE.getHeight() * 0.75f + Utils.FONT_DEFAULT.getHeight()); musicNote = new Image("music-note.png").getScaledCopy(musicNoteDim, musicNoteDim); + + // loader + SpriteSheet spr = new SpriteSheet( + new Image("loader.png").getScaledCopy(musicNoteDim / 48f), + musicNoteDim, musicNoteDim + ); + loader = new Animation(spr, 50); } @Override @@ -218,16 +233,19 @@ public class SongMenu extends BasicGameState { // header if (focusNode != null) { - musicNote.draw(); - int musicNoteWidth = musicNote.getWidth(); - int musicNoteHeight = musicNote.getHeight(); + if (MusicController.isTrackLoading()) + loader.draw(); + else + musicNote.draw(); + int iconWidth = musicNote.getWidth(); + int iconHeight = musicNote.getHeight(); String[] info = focusNode.getInfo(); g.setColor(Color.white); - Utils.FONT_LARGE.drawString(musicNoteWidth + 5, -3, info[0]); + Utils.FONT_LARGE.drawString(iconWidth + 5, -3, info[0]); Utils.FONT_DEFAULT.drawString( - musicNoteWidth + 5, -3 + Utils.FONT_LARGE.getHeight() * 0.75f, info[1]); - int headerY = musicNoteHeight - 3; + iconWidth + 5, -3 + Utils.FONT_LARGE.getHeight() * 0.75f, info[1]); + int headerY = iconHeight - 3; Utils.FONT_BOLD.drawString(5, headerY, info[2]); headerY += Utils.FONT_BOLD.getLineHeight() - 6; Utils.FONT_DEFAULT.drawString(5, headerY, info[3]);