diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index feea0a56..03cb3824 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -139,7 +139,7 @@ public class Options { private static String themeString = "theme.mp3,Rainbows,Kevin MacLeod,219350"; /** The theme song timing point string (for computing beats to pulse the logo) . */ - private static String themeTimingPoint = "-1100,545.454545454545,4,1,0,100,0,0"; + private static String themeTimingPoint = "-3300,545.454545454545,4,1,0,100,0,0"; /** * Returns whether the XDG flag in the manifest (if any) is set to "true". diff --git a/src/itdelatrisu/opsu/audio/MusicController.java b/src/itdelatrisu/opsu/audio/MusicController.java index 82525732..f32043e9 100644 --- a/src/itdelatrisu/opsu/audio/MusicController.java +++ b/src/itdelatrisu/opsu/audio/MusicController.java @@ -195,13 +195,54 @@ public class MusicController { /** * Gets the progress of the current beat. * @return a beat progress value [0,1) where 0 marks the current beat and - * 1 marks the next beat, or {@code null} if no beat information + * 1 marks the next beat, or {@code null} if no timing information * is available (e.g. music paused, no timing points) */ public static Float getBeatProgress() { + if (!updateTimingPoint()) + return null; + + // calculate beat progress + int trackPosition = Math.max(0, getPosition()); + double beatLength = lastTimingPoint.getBeatLength() * 100.0; + int beatTime = lastTimingPoint.getTime(); + return (float) ((((trackPosition - beatTime) * 100.0) % beatLength) / beatLength); + } + + /** + * Gets the progress of the current measure. + * @return a measure progress value [0,1) where 0 marks the start of the measure and + * 1 marks the start of the next measure, or {@code null} if no timing information + * is available (e.g. music paused, no timing points) + */ + public static Float getMeasureProgress() { return getMeasureProgress(1); } + + /** + * Gets the progress of the current measure. + * @param k the meter multiplier + * @return a measure progress value [0,1) where 0 marks the start of the measure and + * 1 marks the start of the next measure, or {@code null} if no timing information + * is available (e.g. music paused, no timing points) + */ + public static Float getMeasureProgress(int k) { + if (!updateTimingPoint()) + return null; + + // calculate measure progress + int trackPosition = Math.max(0, getPosition()); + double measureLength = lastTimingPoint.getBeatLength() * lastTimingPoint.getMeter() * k * 100.0; + int beatTime = lastTimingPoint.getTime(); + return (float) ((((trackPosition - beatTime) * 100.0) % measureLength) / measureLength); + } + + /** + * Updates the timing point information for the current track position. + * @return {@code false} if timing point information is not available, {@code true} otherwise + */ + private static boolean updateTimingPoint() { Beatmap map = getBeatmap(); if (!isPlaying() || map == null || map.timingPoints == null || map.timingPoints.isEmpty()) - return null; + return false; // initialization if (timingPointIndex == 0 && lastTimingPoint == null && !map.timingPoints.isEmpty()) { @@ -221,12 +262,9 @@ public class MusicController { lastTimingPoint = timingPoint; } if (lastTimingPoint == null) - return null; // no timing info + return false; // no timing info - // calculate beat progress - double beatLength = lastTimingPoint.getBeatLength() * 100.0; - int beatTime = lastTimingPoint.getTime(); - return (float) ((((trackPosition - beatTime) * 100.0) % beatLength) / beatLength); + return true; } /** @@ -401,7 +439,7 @@ public class MusicController { public static void playThemeSong() { Beatmap beatmap = Options.getThemeBeatmap(); if (beatmap != null) { - play(beatmap, true, false); + play(beatmap, false, false); themePlaying = true; } } diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index 0f938212..570a69f2 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -35,6 +35,7 @@ import itdelatrisu.opsu.ui.Colors; import itdelatrisu.opsu.ui.Fonts; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.MenuButton.Expand; +import itdelatrisu.opsu.ui.StarFountain; import itdelatrisu.opsu.ui.UI; import itdelatrisu.opsu.ui.animations.AnimatedValue; import itdelatrisu.opsu.ui.animations.AnimationEquation; @@ -53,8 +54,8 @@ 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.state.transition.FadeInTransition; import org.newdawn.slick.state.transition.EasedFadeOutTransition; +import org.newdawn.slick.state.transition.FadeInTransition; /** * "Main Menu" state. @@ -116,6 +117,12 @@ public class MainMenu extends BasicGameState { /** Music position bar coordinates and dimensions. */ private float musicBarX, musicBarY, musicBarWidth, musicBarHeight; + /** Last measure progress value. */ + private float lastMeasureProgress = 0f; + + /** The star fountain. */ + private StarFountain starFountain; + // game-related variables private GameContainer container; private StateBasedGame game; @@ -214,6 +221,9 @@ public class MainMenu extends BasicGameState { restartButton.setHoverAnimationEquation(AnimationEquation.LINEAR); restartButton.setHoverRotate(360); + // initialize star fountain + starFountain = new StarFountain(width, height); + // logo animations float centerOffsetX = width / 5f; logoOpen = new AnimatedValue(400, 0, centerOffsetX, AnimationEquation.OUT_QUAD); @@ -248,6 +258,9 @@ public class MainMenu extends BasicGameState { g.fillRect(0, height * 8 / 9f, width, height / 9f); Colors.BLACK_ALPHA.a = oldAlpha; + // draw star fountain + starFountain.draw(); + // draw downloads button downloadsButton.draw(); @@ -329,7 +342,7 @@ public class MainMenu extends BasicGameState { throws SlickException { UI.update(delta); if (MusicController.trackEnded()) - nextTrack(); // end of track: go to next track + nextTrack(false); // end of track: go to next track int mouseX = input.getMouseX(), mouseY = input.getMouseY(); logo.hoverUpdate(delta, mouseX, mouseY, 0.25f); playButton.hoverUpdate(delta, mouseX, mouseY, 0.25f); @@ -349,6 +362,7 @@ public class MainMenu extends BasicGameState { noHoverUpdate |= contains; musicNext.hoverUpdate(delta, !noHoverUpdate && musicNext.contains(mouseX, mouseY)); musicPrevious.hoverUpdate(delta, !noHoverUpdate && musicPrevious.contains(mouseX, mouseY)); + starFountain.update(delta); // window focus change: increase/decrease theme song volume if (MusicController.isThemePlaying() && @@ -360,6 +374,14 @@ public class MainMenu extends BasicGameState { if (!(Options.isDynamicBackgroundEnabled() && beatmap != null && beatmap.isBackgroundLoading())) bgAlpha.update(delta); + // check measure progress + Float measureProgress = MusicController.getMeasureProgress(2); + if (measureProgress != null) { + if (measureProgress < lastMeasureProgress) + starFountain.burst(true); + lastMeasureProgress = measureProgress; + } + // buttons int centerX = container.getWidth() / 2; float currentLogoButtonAlpha; @@ -432,6 +454,10 @@ public class MainMenu extends BasicGameState { } } + // reset measure info + lastMeasureProgress = 0f; + starFountain.clear(); + // reset button hover states if mouse is not currently hovering over the button int mouseX = input.getMouseX(), mouseY = input.getMouseY(); if (!logo.contains(mouseX, mouseY, 0.25f)) @@ -489,7 +515,7 @@ public class MainMenu extends BasicGameState { } return; } else if (musicNext.contains(x, y)) { - nextTrack(); + nextTrack(true); UI.sendBarNotification(">> Next"); return; } else if (musicPrevious.contains(x, y)) { @@ -598,7 +624,7 @@ public class MainMenu extends BasicGameState { game.enterState(Opsu.STATE_DOWNLOADSMENU, new EasedFadeOutTransition(), new FadeInTransition()); break; case Input.KEY_R: - nextTrack(); + nextTrack(true); break; case Input.KEY_UP: UI.changeVolume(1); @@ -656,9 +682,16 @@ public class MainMenu extends BasicGameState { /** * Plays the next track, and adds the previous one to the stack. + * @param user {@code true} if this was user-initiated, false otherwise (track end) */ - private void nextTrack() { + private void nextTrack(boolean user) { boolean isTheme = MusicController.isThemePlaying(); + if (isTheme && !user) { + // theme was playing, restart + // NOTE: not looping due to inaccurate track positions after loop + MusicController.playAt(0, false); + return; + } SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU); BeatmapSetNode node = menu.setFocus(BeatmapSetList.get().getRandomNode(), -1, true, false); boolean sameAudio = false; diff --git a/src/itdelatrisu/opsu/ui/StarFountain.java b/src/itdelatrisu/opsu/ui/StarFountain.java new file mode 100644 index 00000000..6a126c33 --- /dev/null +++ b/src/itdelatrisu/opsu/ui/StarFountain.java @@ -0,0 +1,85 @@ +package itdelatrisu.opsu.ui; + +import itdelatrisu.opsu.GameImage; +import itdelatrisu.opsu.ui.animations.AnimatedValue; +import itdelatrisu.opsu.ui.animations.AnimationEquation; + +import org.newdawn.slick.Image; + +/** + * Star fountain consisting of two star streams. + */ +public class StarFountain { + /** The (approximate) number of stars in each burst. */ + private static final int BURST_SIZE = 80; + + /** Star streams. */ + private final StarStream left, right; + + /** Burst progress. */ + private final AnimatedValue burstProgress = new AnimatedValue(800, 0, 1, AnimationEquation.LINEAR); + + /** + * Initializes the star fountain. + * @param containerWidth the container width + * @param containerHeight the container height + */ + public StarFountain(int containerWidth, int containerHeight) { + Image img = GameImage.STAR2.getImage(); + float xDir = containerWidth * 0.4f, yDir = containerHeight * 0.75f; + this.left = new StarStream(-img.getWidth(), containerHeight, xDir, -yDir, 0); + this.right = new StarStream(containerWidth, containerHeight, -xDir, -yDir, 0); + setStreamProperties(left); + setStreamProperties(right); + } + + /** + * Sets attributes for the given star stream. + */ + private void setStreamProperties(StarStream stream) { + stream.setDirectionSpread(60f); + stream.setDurationSpread(1100, 200); + } + + /** + * Draws the star fountain. + */ + public void draw() { + left.draw(); + right.draw(); + } + + /** + * Updates the stars in the fountain by a delta interval. + * @param delta the delta interval since the last call + */ + public void update(int delta) { + left.update(delta); + right.update(delta); + if (burstProgress.update(delta)) { + int size = Math.round((float) delta / burstProgress.getDuration() * BURST_SIZE); + left.burst(size); + right.burst(size); + } + } + + /** + * Creates a burst of stars to be processed during the next {@link #update(int)} call. + * @param wait if {@code true}, will not burst if a previous burst is in progress + */ + public void burst(boolean wait) { + if (wait && (burstProgress.getTime() < burstProgress.getDuration() || !left.isEmpty() || !right.isEmpty())) + return; + + burstProgress.setTime(0); + } + + /** + * Clears the stars currently in the fountain. + */ + public void clear() { + left.clear(); + right.clear(); + burstProgress.setTime(burstProgress.getDuration()); + } +} diff --git a/src/itdelatrisu/opsu/ui/StarStream.java b/src/itdelatrisu/opsu/ui/StarStream.java index 6b43e2af..7a5701a9 100644 --- a/src/itdelatrisu/opsu/ui/StarStream.java +++ b/src/itdelatrisu/opsu/ui/StarStream.java @@ -32,7 +32,7 @@ import java.util.Random; import org.newdawn.slick.Image; /** - * Horizontal star stream. + * Star stream. */ public class StarStream { /** The origin of the star stream. */ @@ -196,7 +196,7 @@ public class StarStream { Vec2f offset = position.cpy().add(direction.cpy().nor().normalize().scale((float) getGaussian(0, positionSpread))); Vec2f dir = direction.cpy().scale(distanceRatio).add((float) getGaussian(0, directionSpread), (float) getGaussian(0, directionSpread)); int angle = (int) getGaussian(0, 22.5); - int duration = (int) (distanceRatio * getGaussian(durationBase, durationSpread)); + int duration = Math.max(0, (int) (distanceRatio * getGaussian(durationBase, durationSpread))); AnimationEquation eqn = random.nextBoolean() ? AnimationEquation.IN_OUT_QUAD : AnimationEquation.OUT_QUAD; return new Star(offset, dir, angle, duration, eqn); @@ -216,6 +216,11 @@ public class StarStream { */ public void clear() { stars.clear(); } + /** + * Returns whether there are any stars currently in this stream. + */ + public boolean isEmpty() { return stars.isEmpty(); } + /** * Returns the next pseudorandom, Gaussian ("normally") distributed {@code double} value * with the given mean and standard deviation.