diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index 1ec253c4..c1487003 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -20,6 +20,7 @@ package itdelatrisu.opsu; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.beatmap.Beatmap; +import itdelatrisu.opsu.beatmap.TimingPoint; import itdelatrisu.opsu.skins.Skin; import itdelatrisu.opsu.skins.SkinLoader; import itdelatrisu.opsu.ui.Fonts; @@ -34,6 +35,7 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.net.URI; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Locale; @@ -1340,6 +1342,9 @@ public class Options { beatmap.audioFilename = new File(tokens[0]); beatmap.title = tokens[1]; beatmap.artist = tokens[2]; + // add a timingpoint so the logo can be pulsed in the mainmenu + beatmap.timingPoints = new ArrayList<>(1); + beatmap.timingPoints.add(new TimingPoint("-44,631.578947368421,4,1,0,100,1,0")); try { beatmap.endTime = Integer.parseInt(tokens[3]); } catch (NumberFormatException e) { diff --git a/src/itdelatrisu/opsu/audio/MusicController.java b/src/itdelatrisu/opsu/audio/MusicController.java index 5ecfa76d..56bc9de3 100644 --- a/src/itdelatrisu/opsu/audio/MusicController.java +++ b/src/itdelatrisu/opsu/audio/MusicController.java @@ -22,6 +22,7 @@ import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.Options; import itdelatrisu.opsu.beatmap.Beatmap; import itdelatrisu.opsu.beatmap.BeatmapParser; +import itdelatrisu.opsu.beatmap.TimingPoint; import itdelatrisu.opsu.ui.UI; import java.io.File; @@ -183,6 +184,40 @@ public class MusicController { */ public static Beatmap getBeatmap() { return lastBeatmap; } + /** + * Gets the progress of the current beat. + * @return null if music is paused or no timingpoints are available, or progress as a value in + * [0, 1], where 0 means a beat just happened and 1 means the next beat is coming right now. + */ + public static Float getBeatProgress() { + if (!isPlaying() || getBeatmap() == null) { + return null; + } + Beatmap map = getBeatmap(); + if (map.timingPoints == null) { + return null; + } + int trackposition = getPosition(); + TimingPoint p = null; + double beatlen = 0d; + int time = 0; + for (TimingPoint pts : map.timingPoints) { + if (pts.getTime() > getPosition()) { + break; + } + p = pts; + if (!p.isInherited() && p.getBeatLength() > 0) { + beatlen = p.getBeatLength(); + time = p.getTime(); + } + } + if (p == null) { + return null; + } + double beatLength = beatlen * 100d; + return (float) ((((trackposition - time) * 100) % beatLength) / beatLength); + } + /** * Returns true if the current track is playing. */ diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapParser.java b/src/itdelatrisu/opsu/beatmap/BeatmapParser.java index c7004703..37bbb60e 100644 --- a/src/itdelatrisu/opsu/beatmap/BeatmapParser.java +++ b/src/itdelatrisu/opsu/beatmap/BeatmapParser.java @@ -212,6 +212,45 @@ public class BeatmapParser { return lastNode; } + /** + * Parses the timingpoints for a beatmap. + * @param map the map to parse timingpoints for + */ + public static void parseOnlyTimingPoints(Beatmap map) { + if (map == null || map.getFile() == null || !map.getFile().exists()) { + return; + } + if (map.timingPoints == null) { + map.timingPoints = new ArrayList(); + } + try ( + InputStream bis = new BufferedInputStream(new FileInputStream(map.getFile())); + BufferedReader in = new BufferedReader(new InputStreamReader(bis, "UTF-8")); + ) { + String line; + boolean found = false; + while((line = in.readLine()) != null) { + line = line.trim(); + if(!isValidLine(line)) { + continue; + } + if ("[TimingPoints]".equals(line)) { + found = true; + continue; + } + if (found) { + if (line.startsWith("[")) { + break; + } + parseSectionTimingPoints(map, line); + } + } + map.timingPoints.trimToSize(); + } catch (IOException e) { + ErrorHandler.error(String.format("Failed to read file '%s'.", map.getFile().getAbsolutePath()), e, false); + } + } + /** * Parses a beatmap. * @param file the file to parse @@ -493,21 +532,7 @@ public class BeatmapParser { break; try { - // parse timing point - TimingPoint timingPoint = new TimingPoint(line); - - // calculate BPM - if (!timingPoint.isInherited()) { - int bpm = Math.round(60000 / timingPoint.getBeatLength()); - if (beatmap.bpmMin == 0) - beatmap.bpmMin = beatmap.bpmMax = bpm; - else if (bpm < beatmap.bpmMin) - beatmap.bpmMin = bpm; - else if (bpm > beatmap.bpmMax) - beatmap.bpmMax = bpm; - } - - beatmap.timingPoints.add(timingPoint); + parseSectionTimingPoints(beatmap, line); } catch (Exception e) { Log.warn(String.format("Failed to read timing point '%s' for file '%s'.", line, file.getAbsolutePath()), e); @@ -624,6 +649,26 @@ public class BeatmapParser { return beatmap; } + /** + * Parses a timing point in the timingpoints section of a beatmap file + * @param beatmap the beatmap to add the timingpoint to + * @param line the line with timingpoint to parse + */ + private static void parseSectionTimingPoints(Beatmap beatmap, String line) { + TimingPoint timingPoint = new TimingPoint(line); + if(!timingPoint.isInherited()) { + int bpm = Math.round(60000 / timingPoint.getBeatLength()); + if( beatmap.bpmMin == 0 ) { + beatmap.bpmMin = beatmap.bpmMax = bpm; + } else if( bpm < beatmap.bpmMin ) { + beatmap.bpmMin = bpm; + } else if( bpm > beatmap.bpmMax ) { + beatmap.bpmMax = bpm; + } + } + beatmap.timingPoints.add(timingPoint); + } + /** * Parses all hit objects in a beatmap. * @param beatmap the beatmap to parse diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index 8eb78819..cd0ea0c0 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -256,7 +256,18 @@ public class MainMenu extends BasicGameState { playButton.draw(); exitButton.draw(); } - logo.draw(); + + // logo + Float position = MusicController.getBeatProgress(); + if (position == null) { + position = System.currentTimeMillis() % 1000 / 1000f; + } + float scale = 1f + position * 0.05f; + logo.draw(Color.white, scale); + Image ghostLogo = GameImage.MENU_LOGO.getImage().getScaledCopy(logo.getCurrentScale() / scale * 1.05f); + float scaleposmodx = ghostLogo.getWidth() / 2; + float scaleposmody = ghostLogo.getHeight() / 2; + ghostLogo.draw(logo.getX() - scaleposmodx, logo.getY() - scaleposmody, Colors.GHOST_LOGO); // draw music buttons if (MusicController.isPlaying()) diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index 770cb3c8..c4b56e3d 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -259,6 +259,15 @@ public class SongMenu extends BasicGameState { /** Header and footer end and start y coordinates, respectively. */ private float headerY, footerY; + /** Footer pulsing logo button. */ + private MenuButton footerLogoButton; + + /** Size of the footer pulsing logo. */ + private float footerLogoSize; + + /** Whether the cursor hovers over the footer logo. */ + private boolean bottomLogoHovered; + /** Time, in milliseconds, for fading the search bar. */ private int searchTransitionTimer = SEARCH_TRANSITION_TIME; @@ -335,6 +344,15 @@ public class SongMenu extends BasicGameState { Fonts.SMALL.getLineHeight(); footerY = height - GameImage.SELECTION_MODS.getImage().getHeight(); + // logo coordinates + float footerHeight = height - footerY; + footerLogoSize = footerHeight * 3.25f; + Image logo = GameImage.MENU_LOGO.getImage(); + logo = logo.getScaledCopy(footerLogoSize / logo.getWidth()); + footerLogoButton = new MenuButton(logo, width - footerHeight * 0.8f, height - footerHeight * 0.65f); + footerLogoButton.setHoverAnimationDuration(1); + footerLogoButton.setHoverExpand(1.2f); + // initialize sorts for (BeatmapSortOrder sort : BeatmapSortOrder.values()) sort.init(width, headerY - SongMenu.DIVIDER_LINE_WIDTH / 2); @@ -498,6 +516,26 @@ public class SongMenu extends BasicGameState { g.drawLine(0, footerY, width, footerY); g.resetLineWidth(); + // opsu logo in bottom bar + Float position = MusicController.getBeatProgress(); + if (position == null) { + position = System.currentTimeMillis() % 1000 / 1000f; + } + if (bottomLogoHovered) { + footerLogoButton.draw(); + } else { + float expand = position * 0.15f; + footerLogoButton.draw(Color.white, 1f - expand); + Image ghostLogo = GameImage.MENU_LOGO.getImage(); + ghostLogo = ghostLogo.getScaledCopy((1f + expand) * footerLogoSize / ghostLogo.getWidth()); + float scaleposmodx = ghostLogo.getWidth() / 2; + float scaleposmody = ghostLogo.getHeight() / 2; + float a = Colors.GHOST_LOGO.a; + Colors.GHOST_LOGO.a *= (1f - position); + ghostLogo.draw(footerLogoButton.getX() - scaleposmodx, footerLogoButton.getY() - scaleposmody, Colors.GHOST_LOGO); + Colors.GHOST_LOGO.a = a; + } + // header if (focusNode != null) { // music/loader icon @@ -753,7 +791,18 @@ public class SongMenu extends BasicGameState { } updateDrawnSongPosition(); - // mouse hover + // mouse hover (logo) + if (footerLogoButton.contains(mouseX, mouseY, 0.25f)) { + footerLogoButton.hoverUpdate(delta, true); + bottomLogoHovered = true; + // reset beatmap node hover + hoverIndex = null; + return; + } + footerLogoButton.hoverUpdate(delta, false); + bottomLogoHovered = false; + + // mouse hover (beatmap nodes) BeatmapSetNode node = getNodeAtPosition(mouseX, mouseY); if (node != null) { if (node == hoverIndex) @@ -796,6 +845,11 @@ public class SongMenu extends BasicGameState { if (isScrollingToFocusNode) return; + if (bottomLogoHovered) { + startGame(); + return; + } + songScrolling.pressed(); startScorePos.pressed(); } @@ -1390,6 +1444,10 @@ public class SongMenu extends BasicGameState { focusNode = BeatmapSetList.get().getNode(node, beatmapIndex); Beatmap beatmap = focusNode.getSelectedBeatmap(); + if (beatmap.timingPoints == null) { + // parse the timingpoints so we can pulse the main menu logo and bottom right logo in songmenu + BeatmapParser.parseOnlyTimingPoints(beatmap); + } MusicController.play(beatmap, false, preview); // load scores diff --git a/src/itdelatrisu/opsu/ui/Colors.java b/src/itdelatrisu/opsu/ui/Colors.java index 7f00603a..eff7ba57 100644 --- a/src/itdelatrisu/opsu/ui/Colors.java +++ b/src/itdelatrisu/opsu/ui/Colors.java @@ -45,7 +45,8 @@ public class Colors { BLUE_SCOREBOARD = new Color(133, 208, 212), BLACK_BG_NORMAL = new Color(0, 0, 0, 0.25f), BLACK_BG_HOVER = new Color(0, 0, 0, 0.5f), - BLACK_BG_FOCUS = new Color(0, 0, 0, 0.75f); + BLACK_BG_FOCUS = new Color(0, 0, 0, 0.75f), + GHOST_LOGO = new Color(1.0f, 1.0f, 1.0f, 0.25f); // This class should not be instantiated. private Colors() {} diff --git a/src/itdelatrisu/opsu/ui/MenuButton.java b/src/itdelatrisu/opsu/ui/MenuButton.java index 1aa524dc..c6675b46 100644 --- a/src/itdelatrisu/opsu/ui/MenuButton.java +++ b/src/itdelatrisu/opsu/ui/MenuButton.java @@ -98,6 +98,9 @@ public class MenuButton { /** The default max rotation angle of the button. */ private static final float DEFAULT_ANGLE_MAX = 30f; + /** The current scale of the drawn button */ + private float currentScale = 1f; + /** * Creates a new button from an Image. * @param img the image @@ -166,6 +169,11 @@ public class MenuButton { */ public float getY() { return y; } + /** + * Returns the current scale. + */ + public float getCurrentScale() { return currentScale; } + /** * Sets text to draw in the middle of the button. * @param text the text to draw @@ -191,14 +199,21 @@ public class MenuButton { /** * Draws the button. */ - public void draw() { draw(Color.white); } + public void draw() { draw(Color.white, 1f); } + + /** + * Draws the button with a color filter. + * @param filter the color to filter with when drawing + */ + public void draw(Color filter) { draw(filter, 1f); } /** * Draw the button with a color filter. * @param filter the color to filter with when drawing + * @param scaleOverride the scale to use when drawing, works only for normal images */ @SuppressWarnings("deprecation") - public void draw(Color filter) { + public void draw(Color filter, float scaleOverride) { // animations: get current frame Image image = this.img; if (image == null) { @@ -208,6 +223,14 @@ public class MenuButton { // normal images if (imgL == null) { + float scalePosModX = 0; + float scalePosModY = 0; + if (scaleOverride != 1f) { + image = image.getScaledCopy(scaleOverride); + scalePosModX = image.getWidth() / 2 - xRadius; + scalePosModY = image.getHeight() / 2 - yRadius; + } + currentScale = scaleOverride; if (hoverEffect == 0) image.draw(x - xRadius, y - yRadius, filter); else { @@ -217,13 +240,16 @@ public class MenuButton { if (scale.getValue() != 1f) { image = image.getScaledCopy(scale.getValue()); image.setAlpha(oldAlpha); + scalePosModX = image.getWidth() / 2 - xRadius; + scalePosModY = image.getHeight() / 2 - yRadius; + currentScale *= scale.getValue(); } } if ((hoverEffect & EFFECT_FADE) > 0) image.setAlpha(alpha.getValue()); if ((hoverEffect & EFFECT_ROTATE) > 0) image.setRotation(angle.getValue()); - image.draw(x - xRadius, y - yRadius, filter); + image.draw(x - xRadius - scalePosModX, y - yRadius - scalePosModY, filter); if (image == this.img) { image.setAlpha(oldAlpha); image.setRotation(oldAngle);