diff --git a/.gitignore b/.gitignore index dd8a9712..87058b73 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ .metadata .classpath .project +.externalToolBuilders/ # IntelliJ .idea/ diff --git a/pom.xml b/pom.xml index 2921441f..bfaaef28 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 itdelatrisu opsu - 0.10.0 + 0.10.1 ${maven.build.timestamp} yyyy-MM-dd HH:mm @@ -142,12 +142,12 @@ org.lwjgl.lwjgl lwjgl - 2.9.1 + 2.9.3 org.slick2d slick2d-core - 1.0.0 + 1.0.1 org.jcraft @@ -197,17 +197,22 @@ org.apache.maven maven-artifact - 3.0.3 + 3.3.3 org.apache.commons commons-compress - 1.8 + 1.9 + + + org.tukaani + xz + 1.5 com.github.jponge lzma-java - 1.2 + 1.3 diff --git a/res/bang.png b/res/bang.png deleted file mode 100644 index 6f1e3274..00000000 Binary files a/res/bang.png and /dev/null differ diff --git a/res/download.png b/res/download.png new file mode 100644 index 00000000..86d75d82 Binary files /dev/null and b/res/download.png differ diff --git a/res/options-background.jpg b/res/options-background.jpg deleted file mode 100644 index d590ad6b..00000000 Binary files a/res/options-background.jpg and /dev/null differ diff --git a/res/options-background.png b/res/options-background.png new file mode 100644 index 00000000..10612e32 Binary files /dev/null and b/res/options-background.png differ diff --git a/res/update.png b/res/update.png new file mode 100644 index 00000000..e1db162f Binary files /dev/null and b/res/update.png differ diff --git a/src/fluddokt/opsu/fake/AudioDevicePlayer2.java b/src/fluddokt/opsu/fake/AudioDevicePlayer2.java index ed73ebea..c4928a6a 100644 --- a/src/fluddokt/opsu/fake/AudioDevicePlayer2.java +++ b/src/fluddokt/opsu/fake/AudioDevicePlayer2.java @@ -104,6 +104,7 @@ public void run() { len = Math.min(rbuf.length, -samplePos); for (int i=0; i= 60f) * or {@code Grade.NULL} if no objects have been processed. */ private Grade getGrade() { + boolean silver = (scoreData == null) ? + (GameMod.HIDDEN.isActive() || GameMod.FLASHLIGHT.isActive()) : + (scoreData.mods & (GameMod.HIDDEN.getBit() | GameMod.FLASHLIGHT.getBit())) != 0; return getGrade( hitResultCount[HIT_300], hitResultCount[HIT_100], hitResultCount[HIT_50], hitResultCount[HIT_MISS], - (GameMod.HIDDEN.isActive() || GameMod.FLASHLIGHT.isActive()) + silver ); } diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index 4636aaeb..96a641ae 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -401,10 +401,16 @@ protected Image process_sub(Image img, int w, int h) { return img.getScaledCopy((h / 17f) / img.getHeight()); } }, - BANG ("bang", "png", false, false) { + DOWNLOAD ("download", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return REPOSITORY.process_sub(img, w, h); + return img.getScaledCopy((h / 14f) / img.getHeight()); + } + }, + UPDATE ("update", "png", false, false) { + @Override + protected Image process_sub(Image img, int w, int h) { + return img.getScaledCopy((h / 14f) / img.getHeight()); } }, OPTIONS_BG ("options-background", "png|jpg", false, true) { diff --git a/src/itdelatrisu/opsu/GameMod.java b/src/itdelatrisu/opsu/GameMod.java index a5f7782e..83eb26c0 100644 --- a/src/itdelatrisu/opsu/GameMod.java +++ b/src/itdelatrisu/opsu/GameMod.java @@ -21,6 +21,7 @@ import fluddokt.opsu.fake.*; import itdelatrisu.opsu.ui.MenuButton; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.util.Arrays; import java.util.Collections; @@ -50,7 +51,7 @@ public enum GameMod { "DoubleTime", "Zoooooooooom."), // NIGHTCORE (Category.HARD, 2, GameImage.MOD_NIGHTCORE, "NT", 64, Input.KEY_D, 1.12f, // "Nightcore", "uguuuuuuuu"), - HIDDEN (Category.HARD, 3, GameImage.MOD_HIDDEN, "HD", 8, Input.KEY_F, 1.06f, false, + HIDDEN (Category.HARD, 3, GameImage.MOD_HIDDEN, "HD", 8, Input.KEY_F, 1.06f, "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, "Flashlight", "Restricted view area."), @@ -58,9 +59,9 @@ public enum GameMod { "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, "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, + SPUN_OUT (Category.SPECIAL, 2, GameImage.MOD_SPUN_OUT, "SO", 4096, Input.KEY_C, 0.9f, "SpunOut", "Spinners will be automatically completed."), - AUTO (Category.SPECIAL, 3, GameImage.MOD_AUTO, "", 2048, Input.KEY_B, 1f, + AUTO (Category.SPECIAL, 3, GameImage.MOD_AUTO, "", 2048, Input.KEY_V, 1f, "Autoplay", "Watch a perfect automated play through the song."); /** Mod categories. */ @@ -203,6 +204,8 @@ public static void init(int width, int height) { mod.button = new MenuButton(img, baseX + (offsetX * mod.categoryIndex) + img.getWidth() / 2f, mod.category.getY()); + mod.button.setHoverAnimationDuration(300); + mod.button.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK); mod.button.setHoverExpand(1.2f); mod.button.setHoverRotate(10f); diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index 50d7c0bf..e98d0484 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -41,7 +41,9 @@ //import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; +import java.net.InetAddress; import java.net.ServerSocket; +import java.net.UnknownHostException; /* import org.newdawn.slick.Color; @@ -125,9 +127,16 @@ public void uncaughtException(Thread t, Throwable e) { /* // only allow a single instance try { - SERVER_SOCKET = new ServerSocket(Options.getPort()); + SERVER_SOCKET = new ServerSocket(Options.getPort(), 1, InetAddress.getLocalHost()); + } catch (UnknownHostException e) { + // shouldn't happen } catch (IOException e) { - ErrorHandler.error(String.format("Another program is already running on port %d.", Options.getPort()), e, false); + ErrorHandler.error(String.format( + "opsu! could not be launched for one of these reasons:\n" + + "- An instance of opsu! is already running.\n" + + "- Another program is bound to port %d. " + + "You can change the port opsu! uses by editing the \"Port\" field in the configuration file.", + Options.getPort()), null, false); System.exit(1); } */ @@ -160,6 +169,7 @@ public void uncaughtException(Thread t, Throwable e) { Updater.get().setUpdateInfo(args[0], args[1]); // check for updates + if (!Options.isUpdaterDisabled()) { new Thread() { @Override public void run() { diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index a054d8ec..05064b1c 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -143,8 +143,8 @@ private static File getXDGBaseDir(String env, String fallback) { rootPath = String.format("%s/%s", home, fallback); } File dir = new File(rootPath, "opsu"); - if (!dir.isDirectory()) - dir.mkdir(); + if (!dir.isDirectory() && !dir.mkdir()) + ErrorHandler.error(String.format("Failed to create configuration folder at '%s/opsu'.", rootPath), null, false); return dir; } else return new File("./opsu"); @@ -216,7 +216,7 @@ public enum GameOption { @Override public void read(String s) { int i = Integer.parseInt(s); - if (i > 0 && i < 65535) + if (i > 0 && i <= 65535) port = i; } }, @@ -361,7 +361,9 @@ public void drag(GameContainer container, int d) { public String getValueString() { return String.format("%dms", val); } }, DISABLE_SOUNDS ("Disable All Sound Effects", "DisableSound", "May resolve Linux sound driver issues. Requires a restart.", - (System.getProperty("os.name").toLowerCase().indexOf("linux") > -1)), + //(System.getProperty("os.name").toLowerCase().contains("linux")) + false + ), KEY_LEFT ("Left Game Key", "keyOsuLeft", "Select this option to input a key.") { @Override public String getValueString() { return Keyboard.getKeyName(getGameKeyLeft()); } @@ -457,6 +459,10 @@ public String getValueString() { val - TimeUnit.MINUTES.toSeconds(TimeUnit.SECONDS.toMinutes(val))); } }, + ENABLE_THEME_SONG ("Enable Theme Song", "MenuMusic", "Whether to play the theme song upon starting opsu!", true), + REPLAY_SEEKING ("Replay Seeking", "ReplaySeeking", "Enable a seeking bar on the left side of the screen during replays.", false), + DISABLE_UPDATER ("Disable Automatic Updates", "DisableUpdater", "Disable automatic checking for updates upon starting opsu!.", false), + IN_GAME_PAUSE("In game pause button", "InGamePause", "Shows a in game pasue button.", false), MOBILE_UI_SCALING ("Scales certain UI elements.", "MobileUIScale", "Scales certain UI elements. Requires Restart", (com.badlogic.gdx.Gdx.graphics.getWidth()/com.badlogic.gdx.Gdx.graphics.getPpiX()) <= 6.0f?//screen width less than 6 inches @@ -469,9 +475,12 @@ public String getValueString() { @Override public void read(String s) { int i = (int) (Float.parseFloat(s) * 10f); - if (i >= 0 && i <= 100) + if (i >= 2 && i <= 30) val = i; } + + @Override + public String write() { return String.format(Locale.US, "%.1f", val / 10f); } }, NEW_SLIDER("Enable New Slider", "NewSlider", "Use the new Slider style.",true), @@ -507,8 +516,6 @@ public void click(GameContainer container) { val = 1; } }, - ENABLE_THEME_SONG ("Enable Theme Song", "MenuMusic", "Whether to play the theme song upon starting opsu!", true), - REPLAY_SEEKING ("Replay Seeking", "ReplaySeeking", "Enable a seeking bar on the left side of the screen during replays.", false), DISABLE_CURSOR ("Disable Cursor", "DisableCursor", "", false), ; @@ -936,8 +943,8 @@ public static void setDisplayMode(Container app) { public static boolean isNewCursorEnabled() { return GameOption.NEW_CURSOR.getBooleanValue(); } /** - * Returns whether or not the new cursor type is enabled. - * @return true if enabled + * Returns whether to disable the cursor + * @return true if disabled */ public static boolean isDisableCursor() { return GameOption.DISABLE_CURSOR.getBooleanValue(); } @@ -1054,6 +1061,12 @@ public static void setDisplayMode(Container app) { */ public static boolean isReplaySeekingEnabled() { return GameOption.REPLAY_SEEKING.getBooleanValue(); } + /** + * Returns whether or not automatic checking for updates is disabled. + * @return true if disabled + */ + public static boolean isUpdaterDisabled() { return GameOption.DISABLE_UPDATER.getBooleanValue(); } + /** * Sets the track checkpoint time, if within bounds. * @param time the track position (in ms) @@ -1176,7 +1189,10 @@ public static File getBeatmapDir() { if (beatmapDir.isDirectory()) return beatmapDir; } - beatmapDir.mkdir(); // none found, create new directory + + // none found, create new directory + if (!beatmapDir.mkdir()) + ErrorHandler.error(String.format("Failed to create beatmap directory at '%s'.", beatmapDir.getAbsolutePath()), null, false); return beatmapDir; } @@ -1190,7 +1206,8 @@ public static File getOSZDir() { return oszDir; oszDir = new File(DATA_DIR, "SongPacks/"); - oszDir.mkdir(); + if (!oszDir.isDirectory() && !oszDir.mkdir()) + ErrorHandler.error(String.format("Failed to create song packs directory at '%s'.", oszDir.getAbsolutePath()), null, false); return oszDir; } @@ -1204,7 +1221,8 @@ public static File getReplayImportDir() { return replayImportDir; replayImportDir = new File(DATA_DIR, "ReplayImport/"); - replayImportDir.mkdir(); + if (!replayImportDir.isDirectory() && !replayImportDir.mkdir()) + ErrorHandler.error(String.format("Failed to create replay import directory at '%s'.", replayImportDir.getAbsolutePath()), null, false); return replayImportDir; } @@ -1249,7 +1267,10 @@ public static File getSkinRootDir() { if (skinRootDir.isDirectory()) return skinRootDir; } - skinRootDir.mkdir(); // none found, create new directory + + // none found, create new directory + if (!skinRootDir.mkdir()) + ErrorHandler.error(String.format("Failed to create skins directory at '%s'.", skinRootDir.getAbsolutePath()), null, false); return skinRootDir; } @@ -1408,12 +1429,6 @@ public static void saveOptions() { writer.write(option.write()); writer.newLine(); } - writer.write(String.format("InGamePause = %b", isInGamePauseEnabled())); - writer.newLine(); - writer.write(String.format("MobileUIScale = %.1f", getMobileUIScale())); - writer.newLine(); - writer.write(String.format("SliderQuality = %d", getSliderQuality())); - writer.newLine(); writer.close(); } catch (IOException e) { ErrorHandler.error(String.format("Failed to write to file '%s'.", OPTIONS_FILE.getAbsolutePath()), e, false); diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 5629f9f0..21e185c8 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -249,6 +249,22 @@ else if (val > max) return val; } + /** + * Clamps a value between a lower and upper bound. + * @param val the value to clamp + * @param low the lower bound + * @param high the upper bound + * @return the clamped value + * @author fluddokt + */ + public static int clamp(int val, int low, int high) { + if (val < low) + return low; + if (val > high) + return high; + return val; + } + /** * Clamps a value between a lower and upper bound. * @param val the value to clamp @@ -299,11 +315,9 @@ public static boolean isGameKeyPressed() { public static void takeScreenShot() { // create the screenshot directory File dir = Options.getScreenshotDir(); - if (!dir.isDirectory()) { - if (!dir.mkdir()) { - ErrorHandler.error("Failed to create screenshot directory.", null, false); - return; - } + if (!dir.isDirectory() && !dir.mkdir()) { + ErrorHandler.error(String.format("Failed to create screenshot directory at '%s'.", dir.getAbsolutePath()), null, false); + return; } // create file name diff --git a/src/itdelatrisu/opsu/db/ScoreDB.java b/src/itdelatrisu/opsu/db/ScoreDB.java index c65921b2..c82ec990 100644 --- a/src/itdelatrisu/opsu/db/ScoreDB.java +++ b/src/itdelatrisu/opsu/db/ScoreDB.java @@ -121,7 +121,9 @@ public static void init() { "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 = ? AND " + - "replay = ? AND playerName = ?" + "(replay = ? OR (replay IS NULL AND ? IS NULL)) AND " + + "(playerName = ? OR (playerName IS NULL AND ? IS NULL))" + // TODO: extra playerName checks not needed if name is guaranteed not null ); } catch (SQLException e) { ErrorHandler.error("Failed to prepare score statements.", e, true); @@ -228,6 +230,7 @@ public static void addScore(ScoreData data) { try { setStatementFields(insertStmt, data); insertStmt.setString(18, data.replayString); + insertStmt.setString(19, data.playerName); insertStmt.executeUpdate(); } catch (SQLException e) { ErrorHandler.error("Failed to save score to database.", e, true); @@ -244,6 +247,10 @@ public static void deleteScore(ScoreData data) { try { setStatementFields(deleteScoreStmt, data); + deleteScoreStmt.setString(18, data.replayString); + deleteScoreStmt.setString(19, data.replayString); + deleteScoreStmt.setString(20, data.playerName); + deleteScoreStmt.setString(21, data.playerName); deleteScoreStmt.executeUpdate(); } catch (SQLException e) { ErrorHandler.error("Failed to delete score from database.", e, true); @@ -295,8 +302,6 @@ private static void setStatementFields(PreparedStatement stmt, ScoreData data) 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/downloads/Download.java b/src/itdelatrisu/opsu/downloads/Download.java index 0941884c..564cbdbe 100644 --- a/src/itdelatrisu/opsu/downloads/Download.java +++ b/src/itdelatrisu/opsu/downloads/Download.java @@ -55,6 +55,9 @@ public class Download { /** Read timeout, in ms. */ public static final int READ_TIMEOUT = 10000; + /** Maximum number of HTTP/HTTPS redirects to follow. */ + public static final int MAX_REDIRECTS = 3; + /** Time between download speed and ETA updates, in ms. */ private static final int UPDATE_INTERVAL = 1000; @@ -185,13 +188,57 @@ public void start() { dlThread = new Thread() { @Override public void run() { - // open connection, get content length + // open connection HttpURLConnection conn = null; try { - conn = (HttpURLConnection) url.openConnection(); - conn.setConnectTimeout(CONNECTION_TIMEOUT); - conn.setReadTimeout(READ_TIMEOUT); - conn.setUseCaches(false); + URL downloadURL = url; + int redirectCount = 0; + boolean isRedirect = false; + do { + isRedirect = false; + + conn = (HttpURLConnection) downloadURL.openConnection(); + conn.setConnectTimeout(CONNECTION_TIMEOUT); + conn.setReadTimeout(READ_TIMEOUT); + conn.setUseCaches(false); + + // allow HTTP <--> HTTPS redirects + // http://download.java.net/jdk7u2/docs/technotes/guides/deployment/deployment-guide/upgrade-guide/article-17.html + conn.setInstanceFollowRedirects(false); + conn.setRequestProperty("User-Agent", "Mozilla/5.0..."); + + // check for redirect + int status = conn.getResponseCode(); + if (status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM || + status == HttpURLConnection.HTTP_SEE_OTHER || status == HttpURLConnection.HTTP_USE_PROXY) { + URL base = conn.getURL(); + String location = conn.getHeaderField("Location"); + URL target = null; + if (location != null) + target = new URL(base, location); + conn.disconnect(); + + // check for problems + String error = null; + if (location == null) + error = String.format("Download for URL '%s' is attempting to redirect without a 'location' header.", base.toString()); + else if (!target.getProtocol().equals("http") && !target.getProtocol().equals("https")) + error = String.format("Download for URL '%s' is attempting to redirect to a non-HTTP/HTTPS protocol '%s'.", base.toString(), target.getProtocol()); + else if (redirectCount > MAX_REDIRECTS) + error = String.format("Download for URL '%s' is attempting too many redirects (over %d).", base.toString(), MAX_REDIRECTS); + if (error != null) { + ErrorHandler.error(error, null, false); + throw new IOException(); + } + + // follow redirect + downloadURL = target; + redirectCount++; + isRedirect = true; + } + } while (isRedirect); + + // store content length contentLength = conn.getContentLength(); if(contentLength<0) throw new IOException("Negative content length"); diff --git a/src/itdelatrisu/opsu/downloads/DownloadNode.java b/src/itdelatrisu/opsu/downloads/DownloadNode.java index ee9657a5..ed30ab42 100644 --- a/src/itdelatrisu/opsu/downloads/DownloadNode.java +++ b/src/itdelatrisu/opsu/downloads/DownloadNode.java @@ -258,7 +258,7 @@ public void createDownload(DownloadServer server) { */ File path = new File(Options.getOSZDir(),Integer.toString(beatmapSetID)); String rename = String.format("%d %s - %s.osz", beatmapSetID, artist, title); - this.download = new Download(url, path, rename); + Download download = new Download(url, path, rename); download.setListener(new DownloadListener() { @Override public void completed() { @@ -270,6 +270,7 @@ public void error() { UI.sendBarNotification("Download failed due to a connection error."); } }); + this.download = download; if (Options.useUnicodeMetadata()) // load glyphs Utils.loadGlyphs(Utils.FONT_LARGE, getTitle(), null); } @@ -382,6 +383,7 @@ public void drawResult(Graphics g, int index, boolean hover, boolean focus, bool * @param hover true if the mouse is hovering over this button */ public void drawDownload(Graphics g, int index, int id, boolean hover) { + Download download = this.download; // in case clearDownload() is called asynchronously if (download == null) { ErrorHandler.error("Trying to draw download information for button without Download object.", null, false); return; diff --git a/src/itdelatrisu/opsu/downloads/servers/MnetworkServer.java b/src/itdelatrisu/opsu/downloads/servers/MnetworkServer.java new file mode 100644 index 00000000..625f7015 --- /dev/null +++ b/src/itdelatrisu/opsu/downloads/servers/MnetworkServer.java @@ -0,0 +1,133 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015 Jeffrey Han + * + * opsu! is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * opsu! is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with opsu!. If not, see . + */ + +package itdelatrisu.opsu.downloads.servers; + +import itdelatrisu.opsu.ErrorHandler; +import itdelatrisu.opsu.Utils; +import itdelatrisu.opsu.downloads.DownloadNode; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Download server: http://osu.uu.gl/ + */ +public class MnetworkServer extends DownloadServer { + /** Server name. */ + private static final String SERVER_NAME = "Mnetwork"; + + /** Formatted download URL: {@code beatmapSetID} */ + private static final String DOWNLOAD_URL = "http://osu.uu.gl/s/%d"; + + /** Formatted search URL: {@code query} */ + private static final String SEARCH_URL = "http://osu.uu.gl/d/%s"; + + /** Total result count from the last query. */ + private int totalResults = -1; + + /** Beatmap pattern. */ + private Pattern BEATMAP_PATTERN = Pattern.compile("^(\\d+) ([^-]+) - (.+)\\.osz$"); + + /** Constructor. */ + public MnetworkServer() {} + + @Override + public String getName() { return SERVER_NAME; } + + @Override + public String getDownloadURL(int beatmapSetID) { + return String.format(DOWNLOAD_URL, beatmapSetID); + } + + @Override + public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException { + DownloadNode[] nodes = null; + try { + // read HTML + String queryString = (query.isEmpty()) ? "-" : query; + String search = String.format(SEARCH_URL, URLEncoder.encode(queryString, "UTF-8")); + String html = Utils.readDataFromUrl(new URL(search)); + if (html == null) { + this.totalResults = -1; + return null; + } + + // parse results + // NOTE: Not using a full HTML parser because this is a relatively simple operation. + // FORMAT: + //
+ // {{id}} {{artist}} - {{title}}.osz
+ // BPM: {{bpm}} | Total Time: {{m}}:{{s}}
+ // Genre: {{genre}} | Updated: {{MMM}} {{d}}, {{yyyy}}
+ List nodeList = new ArrayList(); + final String START_TAG = "
", HREF_TAG = " n) continue; + i = html.indexOf('>', i + HREF_TAG.length()); + if (i == -1 || i >= n) continue; + j = html.indexOf('<', i + 1); + if (j == -1 || j > n) continue; + String beatmap = html.substring(i + 1, j).trim(); + + // find date + i = html.indexOf(UPDATED, j); + if (i == -1 || i >= n) continue; + j = html.indexOf('<', i + UPDATED.length()); + if (j == -1 || j > n) continue; + String date = html.substring(i + UPDATED.length(), j).trim(); + + // parse id, title, and artist + Matcher m = BEATMAP_PATTERN.matcher(beatmap); + if (!m.matches()) + continue; + + nodeList.add(new DownloadNode(Integer.parseInt(m.group(1)), date, m.group(3), null, m.group(2), null, "")); + } + + nodes = nodeList.toArray(new DownloadNode[nodeList.size()]); + + // store total result count + this.totalResults = nodes.length; + } catch (MalformedURLException | UnsupportedEncodingException e) { + ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true); + } + return nodes; + } + + @Override + public int minQueryLength() { return 0; } + + @Override + public int totalResults() { return totalResults; } +} diff --git a/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java b/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java index d7179664..834f2b9f 100644 --- a/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java +++ b/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java @@ -39,6 +39,8 @@ /** * Download server: http://loli.al/ + *

+ * This server went offline in August 2015. */ public class OsuMirrorServer extends DownloadServer { /** Server name. */ diff --git a/src/itdelatrisu/opsu/downloads/servers/YaSOnlineServer.java b/src/itdelatrisu/opsu/downloads/servers/YaSOnlineServer.java new file mode 100644 index 00000000..95846bfd --- /dev/null +++ b/src/itdelatrisu/opsu/downloads/servers/YaSOnlineServer.java @@ -0,0 +1,204 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015 Jeffrey Han + * + * opsu! is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * opsu! is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with opsu!. If not, see . + */ +package itdelatrisu.opsu.downloads.servers; + +import itdelatrisu.opsu.ErrorHandler; +import itdelatrisu.opsu.Utils; +import itdelatrisu.opsu.downloads.DownloadNode; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; + +import org.json.JSONObject; + +/** + * Download server: http://osu.yas-online.net/ + */ +public class YaSOnlineServer extends DownloadServer { + /** Server name. */ + private static final String SERVER_NAME = "YaS Online"; + + /** Formatted download URL (returns JSON): {@code beatmapSetID} */ + private static final String DOWNLOAD_URL = "https://osu.yas-online.net/json.mapdata.php?mapId=%d"; + + /** + * Formatted download fetch URL: {@code downloadLink} + * (e.g. {@code /fetch/49125122158ef360a66a07bce2d0483596913843-m-10418.osz}) + */ + private static final String DOWNLOAD_FETCH_URL = "https://osu.yas-online.net%s"; + + /** Maximum beatmaps displayed per page. */ + private static final int PAGE_LIMIT = 25; + + /** Formatted home URL: {@code page} */ + private static final String HOME_URL = "https://osu.yas-online.net/json.maplist.php?o=%d"; + + /** Formatted search URL: {@code query} */ + private static final String SEARCH_URL = "https://osu.yas-online.net/json.search.php?searchQuery=%s"; + + /** Total result count from the last query. */ + private int totalResults = -1; + + /** Max server download ID seen (for approximating total pages). */ + private int maxServerID = 0; + + /** Constructor. */ + public YaSOnlineServer() {} + + @Override + public String getName() { return SERVER_NAME; } + + @Override + public String getDownloadURL(int beatmapSetID) { + try { + // TODO: do this asynchronously (will require lots of changes...) + return getDownloadURLFromMapData(beatmapSetID); + } catch (IOException e) { + return null; + } + } + + /** + * Returns the beatmap download URL by downloading its map data. + *

+ * This is needed because there is no other way to find a beatmap's direct + * download URL. + * @param beatmapSetID the beatmap set ID + * @return the URL string, or null if the address could not be determined + * @throws IOException if any connection error occurred + */ + private String getDownloadURLFromMapData(int beatmapSetID) throws IOException { + try { + // read JSON + String search = String.format(DOWNLOAD_URL, beatmapSetID); + JSONObject json = Utils.readJsonObjectFromUrl(new URL(search)); + JSONObject results; + if (json == null || + !json.getString("result").equals("success") || + (results = json.getJSONObject("success")).length() == 0) { + return null; + } + + // parse result + Iterator keys = results.keys(); + if (!keys.hasNext()) + return null; + String key = (String) keys.next(); + JSONObject item = results.getJSONObject(key); + String downloadLink = item.getString("downloadLink"); + return String.format(DOWNLOAD_FETCH_URL, downloadLink); + } catch (MalformedURLException | UnsupportedEncodingException e) { + ErrorHandler.error(String.format("Problem retrieving download URL for beatmap '%d'.", beatmapSetID), e, true); + return null; + } + } + + @Override + public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException { + DownloadNode[] nodes = null; + try { + // read JSON + String search; + boolean isSearch; + if (query.isEmpty()) { + isSearch = false; + search = String.format(HOME_URL, (page - 1) * PAGE_LIMIT); + } else { + isSearch = true; + search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8")); + } + JSONObject json = Utils.readJsonObjectFromUrl(new URL(search)); + if (json == null) { + this.totalResults = -1; + return null; + } + JSONObject results; + if (!json.getString("result").equals("success") || + (results = json.getJSONObject("success")).length() == 0) { + this.totalResults = 0; + return new DownloadNode[0]; + } + + // parse result list + List nodeList = new ArrayList(); + for (Object obj : results.keySet()) { + String key = (String) obj; + JSONObject item = results.getJSONObject(key); + + // parse title and artist + String title, artist; + String str = item.getString("map"); + int index = str.indexOf(" - "); + if (index > -1) { + title = str.substring(0, index); + artist = str.substring(index + 3); + } else { // should never happen... + title = str; + artist = "?"; + } + + // only contains date added if part of a beatmap pack + int added = item.getInt("added"); + String date = (added == 0) ? "?" : formatDate(added); + + // approximate page count + int serverID = item.getInt("id"); + if (serverID > maxServerID) + maxServerID = serverID; + + nodeList.add(new DownloadNode(item.getInt("mapid"), date, title, null, artist, null, "")); + } + nodes = nodeList.toArray(new DownloadNode[nodeList.size()]); + + // store total result count + if (isSearch) + this.totalResults = nodes.length; + else + this.totalResults = maxServerID; + } catch (MalformedURLException | UnsupportedEncodingException e) { + ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true); + } + return nodes; + } + + @Override + public int minQueryLength() { return 3; } + + @Override + public int totalResults() { return totalResults; } + + /** + * Returns a formatted date string from a raw date. + * @param timestamp the UTC timestamp, in seconds + * @return the formatted date + */ + private String formatDate(int timestamp) { + Date d = new Date(timestamp * 1000L); + DateFormat fmt = new SimpleDateFormat("d MMM yyyy HH:mm:ss"); + return fmt.format(d); + } +} diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java index f2b56cbb..b996d343 100644 --- a/src/itdelatrisu/opsu/objects/Circle.java +++ b/src/itdelatrisu/opsu/objects/Circle.java @@ -101,10 +101,16 @@ public void draw(Graphics g, int trackPosition) { float approachScale = 1 + scale * 3; float alpha = Utils.clamp(1 - fadeinScale, 0, 1); + if (GameMod.HIDDEN.isActive()) { + float fadeOutScale = -(float)(timeDiff-game.getApproachTime())/game.getDecayTime(); + float fadeOutAlpha = Utils.clamp(1-fadeOutScale, 0, 1); + alpha = Math.min(alpha, fadeOutAlpha); + } + float oldAlpha = Utils.COLOR_WHITE_FADE.a; Utils.COLOR_WHITE_FADE.a = color.a = alpha; - if (timeDiff >= 0) + if (timeDiff >= 0 && !GameMod.HIDDEN.isActive()) GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color); GameImage.HITCIRCLE.getImage().drawCentered(x, y, color); boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber(); diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 3ca27899..25490ccd 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -214,6 +214,11 @@ public void draw(Graphics g, int trackPosition) { tick.drawCentered(c[0], c[1], Utils.COLOR_WHITE_FADE); } } + if (GameMod.HIDDEN.isActive()) { + float fadeOutScale = -(float) (timeDiff - game.getApproachTime()) / game.getDecayTime(); + float fadeOutAlpha = Utils.clamp(1 - fadeOutScale, 0, 1); + alpha = Math.min(alpha, fadeOutAlpha); + } if (sliderClickedInitial) ; // don't draw current combo number if already clicked else @@ -247,7 +252,8 @@ public void draw(Graphics g, int trackPosition) { if (timeDiff >= 0) { // approach circle - GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color); + if (!GameMod.HIDDEN.isActive()) + GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color); } else { // Since update() might not have run before drawing during a replay, the // slider time may not have been calculated, which causes NAN numbers and flicker. diff --git a/src/itdelatrisu/opsu/objects/Spinner.java b/src/itdelatrisu/opsu/objects/Spinner.java index 1a64856b..91dc7233 100644 --- a/src/itdelatrisu/opsu/objects/Spinner.java +++ b/src/itdelatrisu/opsu/objects/Spinner.java @@ -166,7 +166,7 @@ public Spinner(HitObject hitObject, Game game, GameData data) { final int maxVel = 48; final int minTime = 2000; final int maxTime = 5000; - maxStoredDeltaAngles = (int) Utils.clamp((hitObject.getEndTime() - hitObject.getTime() - minTime) + maxStoredDeltaAngles = Utils.clamp((hitObject.getEndTime() - hitObject.getTime() - minTime) * (maxVel - minVel) / (maxTime - minTime) + minVel, minVel, maxVel); storedDeltaAngle = new float[maxStoredDeltaAngles]; @@ -214,13 +214,15 @@ public void draw(Graphics g, int trackPosition) { spinnerMetreSub.draw(0, height - spinnerMetreSub.getHeight()); // main spinner elements - float approachScale = 1 - Utils.clamp(((float) timeDiff / (hitObject.getTime() - hitObject.getEndTime())), 0f, 1f); GameImage.SPINNER_CIRCLE.getImage().setAlpha(alpha); GameImage.SPINNER_CIRCLE.getImage().setRotation(drawRotation * 360f); GameImage.SPINNER_CIRCLE.getImage().drawCentered(width / 2, height / 2); - Image approachCircleScaled = GameImage.SPINNER_APPROACHCIRCLE.getImage().getScaledCopy(approachScale); - approachCircleScaled.setAlpha(alpha); - approachCircleScaled.drawCentered(width / 2, height / 2); + if (!GameMod.HIDDEN.isActive()) { + float approachScale = 1 - Utils.clamp(((float) timeDiff / (hitObject.getTime() - hitObject.getEndTime())), 0f, 1f); + Image approachCircleScaled = GameImage.SPINNER_APPROACHCIRCLE.getImage().getScaledCopy(approachScale); + approachCircleScaled.setAlpha(alpha); + approachCircleScaled.drawCentered(width / 2, height / 2); + } GameImage.SPINNER_SPIN.getImage().setAlpha(alpha); GameImage.SPINNER_SPIN.getImage().drawCentered(width / 2, height * 3 / 4); diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java index 8db2a0a0..aa0183d6 100644 --- a/src/itdelatrisu/opsu/objects/curves/Curve.java +++ b/src/itdelatrisu/opsu/objects/curves/Curve.java @@ -92,12 +92,13 @@ public static void init(int width, int height, float circleSize, Color borderCol //ContextCapabilities capabilities = GLContext.getCapabilities(); - mmsliderSupported = true;//capabilities.GL_EXT_framebuffer_object && capabilities.OpenGL32; + //mmsliderSupported = capabilities.GL_EXT_framebuffer_object && capabilities.OpenGL30; + mmsliderSupported = true; if (mmsliderSupported) CurveRenderState.init(width, height, circleSize); else { if (Options.getSkin().getSliderStyle() != Skin.STYLE_PEPPYSLIDER) - Log.warn("New slider style requires FBO support and OpenGL 3.2."); + Log.warn("New slider style requires FBO support and OpenGL 3.0."); } } diff --git a/src/itdelatrisu/opsu/render/CurveRenderState.java b/src/itdelatrisu/opsu/render/CurveRenderState.java index 7440c0cb..9397236f 100644 --- a/src/itdelatrisu/opsu/render/CurveRenderState.java +++ b/src/itdelatrisu/opsu/render/CurveRenderState.java @@ -125,10 +125,19 @@ public void draw(Color color, Color borderColor, Vec2f[] curve) { fbo = mapping; int old_fb = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING); - int old_tex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + /* + int oldFb = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT); + */ + int oldTex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); - //GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fbo.getID()); - //GL11.glViewport(0, 0, fbo.width, fbo.height); + //glGetInteger requires a buffer of size 16, even though just 4 + //values are returned in this specific case + /* + IntBuffer oldViewport = BufferUtils.createIntBuffer(16); + GL11.glGetInteger(GL11.GL_VIEWPORT, oldViewport); + EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fbo.getID()); + GL11.glViewport(0, 0, fbo.width, fbo.height); + */ fbo.bind(); GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); @@ -138,9 +147,13 @@ public void draw(Color color, Color borderColor, Vec2f[] curve) { fbo.unbind(); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_tex); + + GL11.glBindTexture(GL11.GL_TEXTURE_2D, oldTex); + /* + EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, oldFb); + GL11.glViewport(oldViewport.get(0), oldViewport.get(1), oldViewport.get(2), oldViewport.get(3)); + */ GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_fb); - //GL11.glViewport(0, 0, containerWidth, containerHeight); Utils.COLOR_WHITE_FADE.a = alpha; } @@ -164,41 +177,6 @@ public void draw(Color color, Color borderColor, Vec2f[] curve) { Graphics.getGraphics().setColor(new Color(1f, 1f, 1f, alpha)); fbo.draw(0, 0, containerWidth, containerHeight); - - - //fbo.bindTexture(); - //fbo.fbo.getColorBufferTexture(). - //GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); - //GL11.glBindTexture(GL11.GL_TEXTURE_2D, fbo.getTextureID()); - //GL11.glBindTexture(GL11.GL_TEXTURE_2D, staticState.gradientTexture); - - /*GL11.glBegin(); - //setup buffers - int vtx_buf = GL15.glGenBuffers(); - FloatBuffer buff = BufferUtils.createByteBuffer(9999).asFloatBuffer(); - - float tas = 1;//0.7f; - buff.put(new float[]{ - 0,0,-tas,-tas, - 0,1,-tas,tas, - 1,1,tas,tas, - 1,0,tas,-tas - }); - buff.flip(); - GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vtx_buf); - GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buff, GL15.GL_STATIC_DRAW); - - //use shader - //draw buffers - GL20.glVertexAttribPointer(GL11.vertLoc, 2, GL11.GL_FLOAT, false, 4*4, 4*2); - GL20.glVertexAttribPointer(GL11.texCoordLoc, 2, GL11.GL_FLOAT, false, 4*4, 0); - GL20.glUniform4f(GL11.colBorderLoc, 1, 1, 1,alpha); - - GL11.glDrawArrays(GL11.GL_TRIANGLE_FAN, 0, 4); - GL15.glDeleteBuffers(vtx_buf); - - GL11.glEnd(); - //*/ } /** @@ -447,7 +425,7 @@ public void initGradient() { Image slider = GameImage.SLIDER_GRADIENT.getImage().getScaledCopy(1.0f / GameImage.getUIscale()); /* - //staticState.gradientTexture = GL11.glGenTextures(); + staticState.gradientTexture = GL11.glGenTextures(); ByteBuffer buff = BufferUtils.createByteBuffer(slider.getWidth() * 4); for (int i = 0; i < slider.getWidth(); ++i) { Color col = slider.getColor(i, 0); @@ -579,6 +557,7 @@ public void initShaderProgram() { } GL20.glAttachShader(program, vtxShdr); GL20.glAttachShader(program, frgShdr); + GL30.glBindFragDataLocation(program, 0, "out_colour"); GL20.glLinkProgram(program); res = GL20.glGetProgrami(program, GL20.GL_LINK_STATUS); if (res != GL11.GL_TRUE) { diff --git a/src/itdelatrisu/opsu/render/Rendertarget.java b/src/itdelatrisu/opsu/render/Rendertarget.java index 6dacaff8..dc18804d 100644 --- a/src/itdelatrisu/opsu/render/Rendertarget.java +++ b/src/itdelatrisu/opsu/render/Rendertarget.java @@ -24,9 +24,6 @@ /* import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL20; -import org.lwjgl.opengl.GL30; -import org.lwjgl.opengl.GL32; */ /** @@ -59,9 +56,9 @@ private Rendertarget(int width, int height) { this.width = width; this.height = height; /* - fboID = GL30.glGenFramebuffers(); + fboID = EXTFramebufferObject.glGenFramebuffersEXT(); textureID = GL11.glGenTextures(); - depthBufferID = GL30.glGenRenderbuffers(); + depthBufferID = EXTFramebufferObject.glGenRenderbuffersEXT(); */ fbo = new FrameBuffer(Format.RGBA8888, width, height, true); } @@ -71,7 +68,7 @@ private Rendertarget(int width, int height) { */ public void bind() { /* - GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fboID); + EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fboID); Gdx.gl.glFramebufferRenderbuffer(Gdx.gl.GL_FRAMEBUFFER, Gdx.gl.GL_COLOR_ATTACHMENT0, Gdx.gl.GL_RENDERBUFFER, textureID); Gdx.gl.glFramebufferTexture2D(Gdx.gl.GL_FRAMEBUFFER, Gdx.gl.GL_COLOR_ATTACHMENT0, Gdx.gl.GL_TEXTURE_2D, textureID, 0); Gdx.gl.glFramebufferRenderbuffer(Gdx.gl.GL_FRAMEBUFFER, Gdx.gl.GL_DEPTH_ATTACHMENT, Gdx.gl.GL_RENDERBUFFER, depthBufferID); @@ -111,7 +108,9 @@ public int getTextureID() { * Bind the default framebuffer. */ public static void unbind() { - //GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, 0); + /* + EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 0); + */ if(currentFBO != null) currentFBO.end(); } @@ -123,78 +122,28 @@ public static void unbind() { * @param height the height */ public static Rendertarget createRTTFramebuffer(int width, int height) { - //int old_framebuffer = GL11.glGetInteger(GL30.GL_READ_FRAMEBUFFER_BINDING); - //int old_texture = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + /* + int old_framebuffer = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT); + int old_texture = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + */ Rendertarget buffer = new Rendertarget(width,height); - //buffer.bind(); - /* + buffer.bind(); + int fboTexture = buffer.textureID; GL11.glBindTexture(GL11.GL_TEXTURE_2D, fboTexture); - GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, width, height, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, null); + GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, 4, width, height, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_INT, (ByteBuffer) null); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE); - - GL30.glBindRenderbuffer(GL30.GL_RENDERBUFFER, buffer.depthBufferID); - GL30.glRenderbufferStorage(GL30.GL_RENDERBUFFER, GL11.GL_DEPTH_COMPONENT16, width, height); - GL30.glFramebufferRenderbuffer(GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_RENDERBUFFER, buffer.depthBufferID); - - GL32.glFramebufferTexture(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, fboTexture, 0); - //GL20.glDrawBuffers(GL30.GL_COLOR_ATTACHMENT0); - GL30.glFramebufferRenderbuffer(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL30.GL_RENDERBUFFER, fboTexture); - - */ - /* - int texH = height; - int texW = width; - int tb = buffer.textureID; - int rb = buffer.depthBufferID; - int fb = buffer.fboID; - Gdx.gl.glBindTexture(Gdx.gl.GL_TEXTURE_2D, tb); - Gdx.gl.glTexParameteri(Gdx.gl.GL_TEXTURE_2D, Gdx.gl.GL_TEXTURE_MIN_FILTER, Gdx.gl.GL_NEAREST); - Gdx.gl.glTexParameteri(Gdx.gl.GL_TEXTURE_2D, Gdx.gl.GL_TEXTURE_MAG_FILTER, Gdx.gl.GL_NEAREST); - Gdx.gl.glTexParameteri(Gdx.gl.GL_TEXTURE_2D, Gdx.gl.GL_TEXTURE_WRAP_S, Gdx.gl.GL_CLAMP_TO_EDGE); - Gdx.gl.glTexParameteri(Gdx.gl.GL_TEXTURE_2D, Gdx.gl.GL_TEXTURE_WRAP_T, Gdx.gl.GL_CLAMP_TO_EDGE); - Gdx.gl.glTexImage2D(Gdx.gl.GL_TEXTURE_2D, 0, Gdx.gl.GL_RGBA, texH, texH, 0, Gdx.gl.GL_RGBA, Gdx.gl.GL_UNSIGNED_BYTE, - //bbuf - null - ); - - Gdx.gl.glBindRenderbuffer(Gdx.gl.GL_RENDERBUFFER, rb); - Gdx.gl.glRenderbufferStorage(Gdx.gl.GL_RENDERBUFFER, Gdx.gl.GL_DEPTH_COMPONENT16, texW, texH); - //Gdx.gl.glBindRenderbuffer(Gdx.gl.GL_RENDERBUFFER, rb2); - //Gdx.gl.glRenderbufferStorage(Gdx.gl.GL_RENDERBUFFER, Gdx.gl.GL_DEPTH_COMPONENT16, texW, texH); - - // Bind the framebuffer - Gdx.gl.glBindFramebuffer(Gdx.gl.GL_FRAMEBUFFER, fb); - - // specify texture as color attachment - Gdx.gl.glFramebufferRenderbuffer(Gdx.gl.GL_FRAMEBUFFER, Gdx.gl.GL_COLOR_ATTACHMENT0, Gdx.gl.GL_RENDERBUFFER, tb); - Gdx.gl.glFramebufferTexture2D(Gdx.gl.GL_FRAMEBUFFER, Gdx.gl.GL_COLOR_ATTACHMENT0, Gdx.gl.GL_TEXTURE_2D, tb, 0); - - // attach render buffer as depth buffer - Gdx.gl.glFramebufferRenderbuffer(Gdx.gl.GL_FRAMEBUFFER, Gdx.gl.GL_DEPTH_ATTACHMENT, Gdx.gl.GL_RENDERBUFFER, rb); - //Gdx.gl.glFramebufferRenderbuffer(Gdx.gl.GL_FRAMEBUFFER, Gdx.gl.GL_STENCIL_ATTACHMENT, Gdx.gl.GL_RENDERBUFFER, rb2); - - switch( GL20.glCheckFramebufferStatus(GL20.GL_FRAMEBUFFER)){ - case GL20.GL_FRAMEBUFFER_COMPLETE: - System.out.println("Frame Buffer Complete"); - break; - - case GL20.GL_FRAMEBUFFER_UNSUPPORTED: - System.out.println("Frame Buffer unsupported"); - break; - - default: - System.out.println("Frame Buffer unknown" + GL20.glCheckFramebufferStatus(GL20.GL_FRAMEBUFFER) ); - } - - + EXTFramebufferObject.glBindRenderbufferEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, buffer.depthBufferID); + EXTFramebufferObject.glRenderbufferStorageEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, GL11.GL_DEPTH_COMPONENT, width, height); + EXTFramebufferObject.glFramebufferRenderbufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_DEPTH_ATTACHMENT_EXT, EXTFramebufferObject.GL_RENDERBUFFER_EXT, buffer.depthBufferID); + + EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, GL11.GL_TEXTURE_2D, fboTexture, 0); + GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_texture); - GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_framebuffer); + EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, old_framebuffer); */ return buffer; @@ -205,9 +154,11 @@ public static Rendertarget createRTTFramebuffer(int width, int height) { * to use this rendertarget with OpenGL after calling this method. */ public void destroyRTT() { - //GL30.glDeleteFramebuffers(fboID); - //GL30.glDeleteRenderbuffers(depthBufferID); - //GL11.glDeleteTextures(textureID); + /* + EXTFramebufferObject.glDeleteFramebuffersEXT(fboID); + EXTFramebufferObject.glDeleteRenderbuffersEXT(depthBufferID); + GL11.glDeleteTextures(textureID); + */ fbo.dispose(); } } diff --git a/src/itdelatrisu/opsu/states/ButtonMenu.java b/src/itdelatrisu/opsu/states/ButtonMenu.java index c626fa99..02cdbe55 100644 --- a/src/itdelatrisu/opsu/states/ButtonMenu.java +++ b/src/itdelatrisu/opsu/states/ButtonMenu.java @@ -33,6 +33,8 @@ import itdelatrisu.opsu.beatmap.BeatmapSetNode; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimatedValue; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.util.ArrayList; import java.util.List; @@ -263,8 +265,11 @@ public void scroll(GameContainer container, StateBasedGame game, int newValue) { /** The actual title string list, generated upon entering the state. */ private List actualTitle; + /** The horizontal center offset, used for the initial button animation. */ + private AnimatedValue centerOffset; + /** Initial x coordinate offsets left/right of center (for shifting animation), times width. (TODO) */ - private static final float OFFSET_WIDTH_RATIO = 1 / 18f; + private static final float OFFSET_WIDTH_RATIO = 1 / 25f; /** * Constructor. @@ -338,18 +343,14 @@ public void draw(GameContainer container, StateBasedGame game, Graphics g) { */ public void update(GameContainer container, int delta, int mouseX, int mouseY) { float center = container.getWidth() / 2f; + boolean centerOffsetUpdated = centerOffset.update(delta); + float centerOffsetX = centerOffset.getValue(); for (int i = 0; i < buttons.length; i++) { menuButtons[i].hoverUpdate(delta, mouseX, mouseY); // move button to center - float x = menuButtons[i].getX(); - if (i % 2 == 0) { - if (x < center) - menuButtons[i].setX(Math.min(x + (delta / 5f), center)); - } else { - if (x > center) - menuButtons[i].setX(Math.max(x - (delta / 5f), center)); - } + if (centerOffsetUpdated) + menuButtons[i].setX((i % 2 == 0) ? center + centerOffsetX : center - centerOffsetX); } } @@ -406,9 +407,10 @@ public void scroll(GameContainer container, StateBasedGame game, int newValue) { */ public void enter(GameContainer container, StateBasedGame game) { float center = container.getWidth() / 2f; - float centerOffset = container.getWidth() * OFFSET_WIDTH_RATIO; + float centerOffsetX = container.getWidth() * OFFSET_WIDTH_RATIO; + centerOffset = new AnimatedValue(700, centerOffsetX, 0, AnimationEquation.OUT_BOUNCE); for (int i = 0; i < buttons.length; i++) { - menuButtons[i].setX(center + ((i % 2 == 0) ? centerOffset * -1 : centerOffset)); + menuButtons[i].setX(center + ((i % 2 == 0) ? centerOffsetX : centerOffsetX * -1)); menuButtons[i].resetHover(); } diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java index d40fe6a4..ec229e15 100644 --- a/src/itdelatrisu/opsu/states/DownloadsMenu.java +++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java @@ -36,7 +36,8 @@ import itdelatrisu.opsu.downloads.servers.BloodcatServer; import itdelatrisu.opsu.downloads.servers.DownloadServer; import itdelatrisu.opsu.downloads.servers.HexideServer; -import itdelatrisu.opsu.downloads.servers.OsuMirrorServer; +import itdelatrisu.opsu.downloads.servers.MnetworkServer; +import itdelatrisu.opsu.downloads.servers.YaSOnlineServer; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; @@ -76,7 +77,9 @@ public class DownloadsMenu extends BasicGameState { private static final int MIN_REQUEST_INTERVAL = 300; /** Available beatmap download servers. */ - private static final DownloadServer[] SERVERS = { new BloodcatServer(), new OsuMirrorServer(), new HexideServer() }; + private static final DownloadServer[] SERVERS = { + new BloodcatServer(), new HexideServer(), new YaSOnlineServer(), new MnetworkServer() + }; /** The beatmap download server index. */ private int serverIndex = 0; @@ -138,9 +141,6 @@ private enum Page { RESET, CURRENT, PREVIOUS, NEXT }; /** Page direction for last query. */ private Page lastQueryDir = Page.RESET; - /** Number of active requests. */ - private int activeRequests = 0; - /** Previous and next page buttons. */ private MenuButton prevPage, nextPage; @@ -156,6 +156,92 @@ private enum Page { RESET, CURRENT, PREVIOUS, NEXT }; /** The bar notification to send upon entering the state. */ private String barNotificationOnLoad; + /** Search query, executed in {@code queryThread}. */ + private SearchQuery searchQuery; + + /** Search query helper class. */ + private class SearchQuery implements Runnable { + /** The search query. */ + private String query; + + /** The download server. */ + private DownloadServer server; + + /** Whether the query was interrupted. */ + private boolean interrupted = false; + + /** Whether the query has completed execution. */ + private boolean complete = false; + + /** + * Constructor. + * @param query the search query + * @param server the download server + */ + public SearchQuery(String query, DownloadServer server) { + this.query = query; + this.server = server; + } + + /** Interrupt the query and prevent the results from being processed, if not already complete. */ + public void interrupt() { interrupted = true; } + + /** Returns whether the query has completed execution. */ + public boolean isComplete() { return complete; } + + @Override + public void run() { + // check page direction + Page lastPageDir = pageDir; + pageDir = Page.RESET; + int lastPageSize = (resultList != null) ? resultList.length : 0; + int newPage = page; + if (lastPageDir == Page.RESET) + newPage = 1; + else if (lastPageDir == Page.NEXT) + newPage++; + else if (lastPageDir == Page.PREVIOUS) + newPage--; + try { + DownloadNode[] nodes = server.resultList(query, newPage, rankedOnly); + if (!interrupted) { + // update page total + page = newPage; + if (nodes != null) { + if (lastPageDir == Page.NEXT) + pageResultTotal += nodes.length; + else if (lastPageDir == Page.PREVIOUS) + pageResultTotal -= lastPageSize; + else if (lastPageDir == Page.RESET) + pageResultTotal = nodes.length; + } else + pageResultTotal = 0; + + resultList = nodes; + totalResults = server.totalResults(); + focusResult = -1; + startResult = 0; + if (nodes == null) + searchResultString = "An error has occurred."; + else { + if (query.isEmpty()) + searchResultString = "Type to search!"; + else if (totalResults == 0 || resultList.length == 0) + searchResultString = "No results found."; + else + searchResultString = String.format("%d result%s found!", + totalResults, (totalResults == 1) ? "" : "s"); + } + } + } catch (IOException e) { + if (!interrupted) + searchResultString = "Could not establish connection to server."; + } finally { + complete = true; + } + } + } + // game-related variables private GameContainer container; private StateBasedGame game; @@ -396,72 +482,22 @@ public void update(GameContainer container, StateBasedGame game, int delta) searchTimer = 0; searchTimerReset = false; - final String query = search.getText().trim().toLowerCase(); - final DownloadServer server = SERVERS[serverIndex]; + String query = search.getText().trim().toLowerCase(); + DownloadServer server = SERVERS[serverIndex]; if ((lastQuery == null || !query.equals(lastQuery)) && (query.length() == 0 || query.length() >= server.minQueryLength())) { lastQuery = query; lastQueryDir = pageDir; - if (queryThread != null && queryThread.isAlive()) + if (queryThread != null && queryThread.isAlive()) { queryThread.interrupt(); + if (searchQuery != null) + searchQuery.interrupt(); + } // execute query - queryThread = new Thread() { - @Override - public void run() { - activeRequests++; - - // check page direction - Page lastPageDir = pageDir; - pageDir = Page.RESET; - int lastPageSize = (resultList != null) ? resultList.length : 0; - int newPage = page; - if (lastPageDir == Page.RESET) - newPage = 1; - else if (lastPageDir == Page.NEXT) - newPage++; - else if (lastPageDir == Page.PREVIOUS) - newPage--; - try { - DownloadNode[] nodes = server.resultList(query, newPage, rankedOnly); - if (activeRequests - 1 == 0) { - // update page total - page = newPage; - if (nodes != null) { - if (lastPageDir == Page.NEXT) - pageResultTotal += nodes.length; - else if (lastPageDir == Page.PREVIOUS) - pageResultTotal -= lastPageSize; - else if (lastPageDir == Page.RESET) - pageResultTotal = nodes.length; - } else - pageResultTotal = 0; - - resultList = nodes; - totalResults = server.totalResults(); - focusResult = -1; - startResult = 0; - if (nodes == null) - searchResultString = "An error has occurred."; - else { - if (query.isEmpty()) - searchResultString = "Type to search!"; - else if (totalResults == 0 || resultList.length == 0) - searchResultString = "No results found."; - else - searchResultString = String.format("%d result%s found!", - totalResults, (totalResults == 1) ? "" : "s"); - } - } - } catch (IOException e) { - searchResultString = "Could not establish connection to server."; - } finally { - activeRequests--; - queryThread = null; - } - } - }; + searchQuery = new SearchQuery(query, server); + queryThread = new Thread(searchQuery); queryThread.start(); } } @@ -594,23 +630,27 @@ public void update(LineEvent event) { // pages if (nodes.length > 0) { if (page > 1 && prevPage.contains(x, y)) { - if (lastQueryDir == Page.PREVIOUS && queryThread != null && queryThread.isAlive()) + if (lastQueryDir == Page.PREVIOUS && searchQuery != null && !searchQuery.isComplete()) ; // don't send consecutive requests else { SoundController.playSound(SoundEffect.MENUCLICK); pageDir = Page.PREVIOUS; lastQuery = null; + if (searchQuery != null) + searchQuery.interrupt(); resetSearchTimer(); } return; } if (pageResultTotal < totalResults && nextPage.contains(x, y)) { - if (lastQueryDir == Page.NEXT && queryThread != null && queryThread.isAlive()) + if (lastQueryDir == Page.NEXT && searchQuery != null && !searchQuery.isComplete()) ; // don't send consecutive requests else { SoundController.playSound(SoundEffect.MENUCLICK); pageDir = Page.NEXT; lastQuery = null; + if (searchQuery != null) + searchQuery.interrupt(); resetSearchTimer(); return; } @@ -663,6 +703,8 @@ public void run() { search.setText(""); lastQuery = null; pageDir = Page.RESET; + if (searchQuery != null) + searchQuery.interrupt(); resetSearchTimer(); return; } @@ -671,6 +713,8 @@ public void run() { rankedOnly = !rankedOnly; lastQuery = null; pageDir = Page.RESET; + if (searchQuery != null) + searchQuery.interrupt(); resetSearchTimer(); return; } @@ -687,6 +731,8 @@ public void run() { serverIndex = (serverIndex + 1) % SERVERS.length; lastQuery = null; pageDir = Page.RESET; + if (searchQuery != null) + searchQuery.interrupt(); resetSearchTimer(); return; } @@ -790,6 +836,8 @@ public void keyPressed(int key, char c) { SoundController.playSound(SoundEffect.MENUCLICK); lastQuery = null; pageDir = Page.CURRENT; + if (searchQuery != null) + searchQuery.interrupt(); resetSearchTimer(); break; case Input.KEY_F7: diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 58bb298c..dc445087 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -50,6 +50,7 @@ import itdelatrisu.opsu.replay.ReplayFrame; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimationEquation; //import java.io.File; @@ -119,6 +120,10 @@ public enum Restart { /** Hit object approach time, in milliseconds. */ private int approachTime; + /** Decay time for elements in "Hidden" mod, in milliseconds. */ + //TODO: figure out actual formula for decay time + private int decayTime = 800; + /** Time offsets for obtaining each hit result (indexed by HIT_* constants). */ private int[] hitResultOffset; @@ -624,6 +629,8 @@ else if(Options.isInGamePauseEnabled() && cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY); } + Utils.FONT_DEFAULT.drawString(10,10, MusicController.getPosition()+""); + if (isReplay) UI.draw(g, replayX, replayY, replayKeyPressed); else if (GameMod.AUTO.isActive()) @@ -1180,6 +1187,13 @@ public void enter(GameContainer container, StateBasedGame game) // restart the game if (restart != Restart.FALSE) { + // load mods + if (isReplay) { + previousMods = GameMod.getModState(); + GameMod.loadModState(replay.mods); + } + + // check restart state if (restart == Restart.NEW) { // new game loadImages(); @@ -1264,10 +1278,6 @@ else if (hitObject.isSpinner()) // load replay frames if (isReplay) { - // load mods - previousMods = GameMod.getModState(); - GameMod.loadModState(replay.mods); - // load initial data replayX = container.getWidth() / 2; replayY = container.getHeight() / 2; @@ -1509,6 +1519,8 @@ private void loadImages() { Image skip = GameImage.SKIP.getImage(); skipButton = new MenuButton(skip, width - skip.getWidth() / 2f, height - (skip.getHeight() / 2f)); } + skipButton.setHoverAnimationDuration(350); + skipButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK); skipButton.setHoverExpand(1.1f, MenuButton.Expand.UP_LEFT); Image pause = GameImage.MUSIC_PAUSE.getImage().getScaledCopy( 2.5f*Options.getMobileUIScale(0.2f) / Options.getMobileUIScale()); @@ -1599,6 +1611,11 @@ private void setMapModifiers() { */ public int getApproachTime() { return approachTime; } + /** + * Returns the object decay time in the "Hidden" mod, in milliseconds. + */ + public int getDecayTime() { return decayTime; } + /** * Returns an array of hit result offset times, in milliseconds (indexed by GameData.HIT_* constants). */ diff --git a/src/itdelatrisu/opsu/states/GamePauseMenu.java b/src/itdelatrisu/opsu/states/GamePauseMenu.java index f7451f3f..423eebec 100644 --- a/src/itdelatrisu/opsu/states/GamePauseMenu.java +++ b/src/itdelatrisu/opsu/states/GamePauseMenu.java @@ -29,6 +29,7 @@ import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimationEquation; /* import org.lwjgl.input.Keyboard; import org.newdawn.slick.Color; @@ -230,6 +231,14 @@ public void loadImages() { continueButton = new MenuButton(GameImage.PAUSE_CONTINUE.getImage(), width / 2f, height * 0.25f); retryButton = new MenuButton(GameImage.PAUSE_RETRY.getImage(), width / 2f, height * 0.5f); backButton = new MenuButton(GameImage.PAUSE_BACK.getImage(), width / 2f, height * 0.75f); + final int buttonAnimationDuration = 300; + continueButton.setHoverAnimationDuration(buttonAnimationDuration); + retryButton.setHoverAnimationDuration(buttonAnimationDuration); + backButton.setHoverAnimationDuration(buttonAnimationDuration); + final AnimationEquation buttonAnimationEquation = AnimationEquation.IN_OUT_BACK; + continueButton.setHoverAnimationEquation(buttonAnimationEquation); + retryButton.setHoverAnimationEquation(buttonAnimationEquation); + backButton.setHoverAnimationEquation(buttonAnimationEquation); continueButton.setHoverExpand(); retryButton.setHoverExpand(); backButton.setHoverExpand(); diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index a37d1879..38dde49e 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -35,6 +35,8 @@ import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.MenuButton.Expand; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimatedValue; +import itdelatrisu.opsu.ui.animations.AnimationEquation; /* @@ -65,7 +67,7 @@ */ public class MainMenu extends BasicGameState { /** Idle time, in milliseconds, before returning the logo to its original position. */ - private static final short MOVE_DELAY = 5000; + private static final short LOGO_IDLE_DELAY = 10000; /** Max alpha level of the menu background. */ private static final float BG_MAX_ALPHA = 0.9f; @@ -73,12 +75,21 @@ public class MainMenu extends BasicGameState { /** Logo button that reveals other buttons on click. */ private MenuButton logo; - /** Whether or not the logo has been clicked. */ - private boolean logoClicked = false; + /** Logo states. */ + private enum LogoState { DEFAULT, OPENING, OPEN, CLOSING } + + /** Current logo state. */ + private LogoState logoState = LogoState.DEFAULT; /** Delay timer, in milliseconds, before starting to move the logo back to the center. */ private int logoTimer = 0; + /** Logo horizontal offset for opening and closing actions. */ + private AnimatedValue logoOpen, logoClose; + + /** Logo button alpha levels. */ + private AnimatedValue logoButtonAlpha; + /** Main "Play" and "Exit" buttons. */ private MenuButton playButton, exitButton; @@ -91,8 +102,8 @@ public class MainMenu extends BasicGameState { /** Button linking to repository. */ private MenuButton repoButton; - /** Button for installing updates. */ - private MenuButton updateButton; + /** Buttons for installing updates. */ + private MenuButton updateButton, restartButton; /** Application start time, for drawing the total running time. */ private long programStartTime; @@ -101,7 +112,7 @@ public class MainMenu extends BasicGameState { private Stack previous; /** Background alpha level (for fade-in effect). */ - private float bgAlpha = 0f; + private AnimatedValue bgAlpha = new AnimatedValue(1100, 0f, BG_MAX_ALPHA, AnimationEquation.LINEAR); /** Whether or not a notification was already sent upon entering. */ private boolean enterNotification = false; @@ -149,9 +160,18 @@ public void init(GameContainer container, StateBasedGame game) exitButton = new MenuButton(exitImg, width * 0.75f - exitOffset, (height / 2) + (exitImg.getHeight() / 2f) ); - logo.setHoverExpand(1.05f); - playButton.setHoverExpand(1.05f); - exitButton.setHoverExpand(1.05f); + final int logoAnimationDuration = 350; + logo.setHoverAnimationDuration(logoAnimationDuration); + playButton.setHoverAnimationDuration(logoAnimationDuration); + exitButton.setHoverAnimationDuration(logoAnimationDuration); + final AnimationEquation logoAnimationEquation = AnimationEquation.IN_OUT_BACK; + logo.setHoverAnimationEquation(logoAnimationEquation); + playButton.setHoverAnimationEquation(logoAnimationEquation); + exitButton.setHoverAnimationEquation(logoAnimationEquation); + final float logoHoverScale = 1.08f; + logo.setHoverExpand(logoHoverScale); + playButton.setHoverExpand(logoHoverScale); + exitButton.setHoverExpand(logoHoverScale); // initialize music buttons int musicWidth = GameImage.MUSIC_PLAY.getImage().getWidth(); @@ -174,24 +194,40 @@ public void init(GameContainer container, StateBasedGame game) // initialize downloads button Image dlImg = GameImage.DOWNLOADS.getImage(); downloadsButton = new MenuButton(dlImg, width - dlImg.getWidth() / 2f, height / 2f); + downloadsButton.setHoverAnimationDuration(350); + downloadsButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK); downloadsButton.setHoverExpand(1.03f, Expand.LEFT); // initialize repository button float startX = width * 0.997f, startY = height * 0.997f; - if (Desktop.isDesktopSupported()) { // only if a webpage can be opened + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { // only if a webpage can be opened Image repoImg = GameImage.REPOSITORY.getImage(); repoButton = new MenuButton(repoImg, startX - repoImg.getWidth(), startY - repoImg.getHeight() ); + repoButton.setHoverAnimationDuration(350); + repoButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK); repoButton.setHoverExpand(); - startX -= repoImg.getWidth() * 1.75f; - } else - startX -= width * 0.005f; + } - // initialize update button - Image bangImg = GameImage.BANG.getImage(); - updateButton = new MenuButton(bangImg, startX - bangImg.getWidth(), startY - bangImg.getHeight()); - updateButton.setHoverExpand(1.15f); + // initialize update buttons + float updateX = width / 2f, updateY = height * 17 / 18f; + Image downloadImg = GameImage.DOWNLOAD.getImage(); + updateButton = new MenuButton(downloadImg, updateX, updateY); + updateButton.setHoverAnimationDuration(400); + updateButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_QUAD); + updateButton.setHoverExpand(1.1f); + Image updateImg = GameImage.UPDATE.getImage(); + restartButton = new MenuButton(updateImg, updateX, updateY); + restartButton.setHoverAnimationDuration(2000); + restartButton.setHoverAnimationEquation(AnimationEquation.LINEAR); + restartButton.setHoverRotate(360); + + // logo animations + float centerOffsetX = width / 5f; + logoOpen = new AnimatedValue(400, 0, centerOffsetX, AnimationEquation.OUT_QUAD); + logoClose = new AnimatedValue(2200, centerOffsetX, 0, AnimationEquation.OUT_QUAD); + logoButtonAlpha = new AnimatedValue(300, 0f, 1f, AnimationEquation.LINEAR); reset(); } @@ -205,11 +241,11 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) // draw background Beatmap beatmap = MusicController.getBeatmap(); if (Options.isDynamicBackgroundEnabled() && - beatmap != null && beatmap.drawBG(width, height, bgAlpha, true)) + beatmap != null && beatmap.drawBG(width, height, bgAlpha.getValue(), true)) ; else { Image bg = GameImage.MENU_BG.getImage(); - bg.setAlpha(bgAlpha); + bg.setAlpha(bgAlpha.getValue()); bg.draw(); } @@ -225,7 +261,7 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) downloadsButton.draw(); // draw buttons - if (logoTimer > 0) { + if (logoState == LogoState.OPEN || logoState == LogoState.CLOSING) { playButton.draw(); exitButton.draw(); } @@ -255,22 +291,11 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) // draw update button if (Updater.get().showButton()) { - Color updateColor = null; - switch (Updater.get().getStatus()) { - case UPDATE_AVAILABLE: - updateColor = Color.red; - break; - case UPDATE_DOWNLOADED: - updateColor = Color.green; - break; - case UPDATE_DOWNLOADING: - updateColor = Color.yellow; - break; - default: - updateColor = Color.white; - break; - } - updateButton.draw(updateColor); + Updater.Status status = Updater.get().getStatus(); + if (status == Updater.Status.UPDATE_AVAILABLE || status == Updater.Status.UPDATE_DOWNLOADING) + updateButton.draw(); + else if (status == Updater.Status.UPDATE_DOWNLOADED) + restartButton.draw(); } // draw text @@ -309,7 +334,10 @@ public void update(GameContainer container, StateBasedGame game, int delta) exitButton.hoverUpdate(delta, mouseX, mouseY, 0.25f); if (repoButton != null) repoButton.hoverUpdate(delta, mouseX, mouseY); - updateButton.hoverUpdate(delta, mouseX, mouseY); + if (Updater.get().showButton()) { + updateButton.autoHoverUpdate(delta, true); + restartButton.autoHoverUpdate(delta, false); + } downloadsButton.hoverUpdate(delta, mouseX, mouseY); // ensure only one button is in hover state at once boolean noHoverUpdate = musicPositionBarContains(mouseX, mouseY); @@ -326,46 +354,44 @@ public void update(GameContainer container, StateBasedGame game, int delta) MusicController.toggleTrackDimmed(0.33f); // fade in background - if (bgAlpha < BG_MAX_ALPHA) { - bgAlpha += delta / 1000f; - if (bgAlpha > BG_MAX_ALPHA) - bgAlpha = BG_MAX_ALPHA; - } + bgAlpha.update(delta); // buttons - if (logoClicked) { - if (logoTimer == 0) { // shifting to left - if (logo.getX() > container.getWidth() / 3.3f) - logo.setX(logo.getX() - delta); - else - logoTimer = 1; - } else if (logoTimer >= MOVE_DELAY) // timer over: shift back to center - logoClicked = false; - else { // increment timer - logoTimer += delta; - if (logoTimer <= 500) { - // fade in buttons - playButton.getImage().setAlpha(logoTimer / 400f); - exitButton.getImage().setAlpha(logoTimer / 400f); - } - } - } else { - // fade out buttons - if (logoTimer > 0) { - float alpha = playButton.getImage().getAlpha(); - if (alpha > 0f) { - playButton.getImage().setAlpha(alpha - (delta / 200f)); - exitButton.getImage().setAlpha(alpha - (delta / 200f)); - } else - logoTimer = 0; + int centerX = container.getWidth() / 2; + float currentLogoButtonAlpha; + switch (logoState) { + case DEFAULT: + break; + case OPENING: + if (logoOpen.update(delta)) // shifting to left + logo.setX(centerX - logoOpen.getValue()); + else { + logoState = LogoState.OPEN; + logoTimer = 0; + logoButtonAlpha.setTime(0); } - - // move back to original location - if (logo.getX() < container.getWidth() / 2) { - logo.setX(logo.getX() + (delta / 3f)); - if (logo.getX() > container.getWidth() / 2) - logo.setX(container.getWidth() / 2); + break; + case OPEN: + if (logoButtonAlpha.update(delta)) { // fade in buttons + currentLogoButtonAlpha = logoButtonAlpha.getValue(); + playButton.getImage().setAlpha(currentLogoButtonAlpha); + exitButton.getImage().setAlpha(currentLogoButtonAlpha); + } else if (logoTimer >= LOGO_IDLE_DELAY) { // timer over: shift back to center + logoState = LogoState.CLOSING; + logoClose.setTime(0); + logoTimer = 0; + } else // increment timer + logoTimer += delta; + break; + case CLOSING: + if (logoButtonAlpha.update(-delta)) { // fade out buttons + currentLogoButtonAlpha = logoButtonAlpha.getValue(); + playButton.getImage().setAlpha(currentLogoButtonAlpha); + exitButton.getImage().setAlpha(currentLogoButtonAlpha); } + if (logoClose.update(delta)) // shifting to right + logo.setX(centerX - logoClose.getValue()); + break; } // tooltips @@ -377,8 +403,12 @@ else if (musicNext.contains(mouseX, mouseY)) UI.updateTooltip(delta, "Next track", false); else if (musicPrevious.contains(mouseX, mouseY)) UI.updateTooltip(delta, "Previous track", false); - else if (Updater.get().showButton() && updateButton.contains(mouseX, mouseY)) - UI.updateTooltip(delta, Updater.get().getStatus().getDescription(), true); + else if (Updater.get().showButton()) { + Updater.Status status = Updater.get().getStatus(); + if (((status == Updater.Status.UPDATE_AVAILABLE || status == Updater.Status.UPDATE_DOWNLOADING) && updateButton.contains(mouseX, mouseY)) || + (status == Updater.Status.UPDATE_DOWNLOADED && restartButton.contains(mouseX, mouseY))) + UI.updateTooltip(delta, status.getDescription(), true); + } } @Override @@ -418,8 +448,8 @@ public void enter(GameContainer container, StateBasedGame game) musicPrevious.resetHover(); if (repoButton != null && !repoButton.contains(mouseX, mouseY)) repoButton.resetHover(); - if (!updateButton.contains(mouseX, mouseY)) - updateButton.resetHover(); + updateButton.resetHover(); + restartButton.resetHover(); if (!downloadsButton.contains(mouseX, mouseY)) downloadsButton.resetHover(); } @@ -455,71 +485,85 @@ public void mousePressed(int button, int x, int y) { MusicController.resume(); UI.sendBarNotification("Play"); } + return; } else if (musicNext.contains(x, y)) { nextTrack(); UI.sendBarNotification(">> Next"); + return; } else if (musicPrevious.contains(x, y)) { if (!previous.isEmpty()) { SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU); menu.setFocus(BeatmapSetList.get().getBaseNode(previous.pop()), -1, true, false); if (Options.isDynamicBackgroundEnabled()) - bgAlpha = 0f; + bgAlpha.setTime(0); } else MusicController.setPosition(0); UI.sendBarNotification("<< Previous"); + return; } // downloads button actions - else if (downloadsButton.contains(x, y)) { + if (downloadsButton.contains(x, y)) { SoundController.playSound(SoundEffect.MENUHIT); game.enterState(Opsu.STATE_DOWNLOADSMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); + return; } // repository button actions - else if (repoButton != null && repoButton.contains(x, y)) { + if (repoButton != null && repoButton.contains(x, y)) { try { Desktop.getDesktop().browse(Options.REPOSITORY_URI); + } catch (UnsupportedOperationException e) { + UI.sendBarNotification("The repository web page could not be opened."); } catch (IOException e) { ErrorHandler.error("Could not browse to repository URI.", e, false); } + return; } // update button actions - else if (Updater.get().showButton() && updateButton.contains(x, y)) { - switch (Updater.get().getStatus()) { - case UPDATE_AVAILABLE: + if (Updater.get().showButton()) { + Updater.Status status = Updater.get().getStatus(); + if (updateButton.contains(x, y) && status == Updater.Status.UPDATE_AVAILABLE) { SoundController.playSound(SoundEffect.MENUHIT); Updater.get().startDownload(); - break; - case UPDATE_DOWNLOADED: + updateButton.removeHoverEffects(); + updateButton.setHoverAnimationDuration(800); + updateButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_QUAD); + updateButton.setHoverFade(0.6f); + return; + } else if (restartButton.contains(x, y) && status == Updater.Status.UPDATE_DOWNLOADED) { SoundController.playSound(SoundEffect.MENUHIT); Updater.get().prepareUpdate(); container.setForceExit(false); container.exit(); - break; - default: - break; + return; } } // start moving logo (if clicked) - else if (!logoClicked) { + if (logoState == LogoState.DEFAULT || logoState == LogoState.CLOSING) { if (logo.contains(x, y, 0.25f)) { - logoClicked = true; + logoState = LogoState.OPENING; + logoOpen.setTime(0); logoTimer = 0; playButton.getImage().setAlpha(0f); exitButton.getImage().setAlpha(0f); SoundController.playSound(SoundEffect.MENUHIT); + return; } } // other button actions (if visible) - else if (logoClicked) { + else if (logoState == LogoState.OPEN || logoState == LogoState.OPENING) { if (logo.contains(x, y, 0.25f) || playButton.contains(x, y, 0.25f)) { SoundController.playSound(SoundEffect.MENUHIT); enterSongMenu(); - } else if (exitButton.contains(x, y, 0.25f)) + return; + } else if (exitButton.contains(x, y, 0.25f)) { container.exit(); + return; + } } } @@ -539,8 +583,9 @@ public void keyPressed(int key, char c) { break; case Input.KEY_P: SoundController.playSound(SoundEffect.MENUHIT); - if (!logoClicked) { - logoClicked = true; + if (logoState == LogoState.DEFAULT || logoState == LogoState.CLOSING) { + logoState = LogoState.OPENING; + logoOpen.setTime(0); logoTimer = 0; playButton.getImage().setAlpha(0f); exitButton.getImage().setAlpha(0f); @@ -588,8 +633,11 @@ private boolean musicPositionBarContains(float cx, float cy) { public void reset() { // reset logo logo.setX(container.getWidth() / 2); - logoClicked = false; + logoOpen.setTime(0); + logoClose.setTime(0); + logoButtonAlpha.setTime(0); logoTimer = 0; + logoState = LogoState.DEFAULT; logo.resetHover(); playButton.resetHover(); @@ -601,6 +649,7 @@ public void reset() { if (repoButton != null) repoButton.resetHover(); updateButton.resetHover(); + restartButton.resetHover(); downloadsButton.resetHover(); } @@ -618,7 +667,7 @@ private void nextTrack() { previous.add(node.index); } if (Options.isDynamicBackgroundEnabled() && !sameAudio && !MusicController.isThemePlaying()) - bgAlpha = 0f; + bgAlpha.setTime(0); } /** diff --git a/src/itdelatrisu/opsu/states/OptionsMenu.java b/src/itdelatrisu/opsu/states/OptionsMenu.java index 19de8fdb..4a83db8a 100644 --- a/src/itdelatrisu/opsu/states/OptionsMenu.java +++ b/src/itdelatrisu/opsu/states/OptionsMenu.java @@ -101,7 +101,9 @@ private enum OptionTab { GameOption.FIXED_OD, GameOption.CHECKPOINT, GameOption.REPLAY_SEEKING, - GameOption.LOAD_HD_IMAGES, + /* + GameOption.DISABLE_UPDATER + */ }), EXTRA ("Extras", new GameOption[] { GameOption.MOBILE_UI_SCALING, @@ -213,12 +215,12 @@ public void init(GameContainer container, StateBasedGame game) @Override public void render(GameContainer container, StateBasedGame game, Graphics g) throws SlickException { - g.setBackground(Utils.COLOR_BLACK_ALPHA); - int width = container.getWidth(); int height = container.getHeight(); int mouseX = input.getMouseX(), mouseY = input.getMouseY(); - float lineY = OptionTab.DISPLAY.button.getY() + (GameImage.MENU_TAB.getImage().getHeight() / 2f); + + // background + GameImage.OPTIONS_BG.getImage().draw(); // title float marginX = width * 0.015f, marginY = height * 0.01f; @@ -226,9 +228,6 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) Utils.FONT_DEFAULT.drawString(marginX, marginY + Utils.FONT_XLARGE.getLineHeight() * 0.92f, "Change the way opsu! behaves", Color.white); - // background - GameImage.OPTIONS_BG.getImage().draw(0, lineY); - // game options g.setLineWidth(1f); GameOption hoverOption = (keyEntryLeft) ? GameOption.KEY_LEFT : @@ -256,6 +255,7 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) currentTab.getName(), true, false); g.setColor(Color.white); g.setLineWidth(2f); + float lineY = OptionTab.DISPLAY.button.getY() + (GameImage.MENU_TAB.getImage().getHeight() / 2f); g.drawLine(0, lineY, width, lineY); g.resetLineWidth(); diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index dbec838e..da984092 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -43,6 +43,8 @@ import itdelatrisu.opsu.states.ButtonMenu.MenuState; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimatedValue; +import itdelatrisu.opsu.ui.animations.AnimationEquation; //import java.io.File; import java.util.Map; @@ -141,8 +143,8 @@ public SongNode(BeatmapSetNode node, int index) { /** Button coordinate values. */ private float buttonX, buttonY, buttonOffset, buttonWidth, buttonHeight; - /** Current x offset of song buttons for mouse hover, in pixels. */ - private float hoverOffset = 0f; + /** Horizontal offset of song buttons for mouse hover, in pixels. */ + private AnimatedValue hoverOffset = new AnimatedValue(250, 0, MAX_HOVER_OFFSET, AnimationEquation.OUT_QUART); /** Current index of hovered song button. */ private int hoverIndex = -1; @@ -313,7 +315,7 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) g.setClip(0, (int) (headerY + DIVIDER_LINE_WIDTH / 2), width, (int) (footerY - headerY)); for (int i = songButtonIndex; i <= MAX_SONG_BUTTONS && node != null; i++, node = node.next) { // draw the node - float offset = (i == hoverIndex) ? hoverOffset : 0f; + float offset = (i == hoverIndex) ? hoverOffset.getValue() : 0f; ScoreData[] scores = getScoreDataForNode(node, false); node.draw(buttonX - offset, buttonY + (i*buttonOffset) + DIVIDER_LINE_WIDTH / 2, (scores == null) ? Grade.NULL : scores[0].getGrade(), (node == focusNode)); @@ -566,15 +568,11 @@ public void update(GameContainer container, StateBasedGame game, int delta) float cx = (node.index == BeatmapSetList.get().getExpandedIndex()) ? buttonX * 0.9f : buttonX; if ((mouseX > cx && mouseX < cx + buttonWidth) && (mouseY > buttonY + (i * buttonOffset) && mouseY < buttonY + (i * buttonOffset) + buttonHeight)) { - if (i == hoverIndex) { - if (hoverOffset < MAX_HOVER_OFFSET) { - hoverOffset += delta / 3f; - if (hoverOffset > MAX_HOVER_OFFSET) - hoverOffset = MAX_HOVER_OFFSET; - } - } else { + if (i == hoverIndex) + hoverOffset.update(delta); + else { hoverIndex = i; - hoverOffset = 0f; + hoverOffset.setTime(0); } isHover = true; break; @@ -582,7 +580,7 @@ public void update(GameContainer container, StateBasedGame game, int delta) } } if (!isHover) { - hoverOffset = 0f; + hoverOffset.setTime(0); hoverIndex = -1; } else return; @@ -666,7 +664,7 @@ public void mouseClicked(int button, int x, int y, int clickCount) { float cx = (node.index == expandedIndex) ? buttonX * 0.9f : buttonX; if ((x > cx && x < cx + buttonWidth) && (y > buttonY + (i * buttonOffset) && y < buttonY + (i * buttonOffset) + buttonHeight)) { - float oldHoverOffset = hoverOffset; + int oldHoverOffsetTime = hoverOffset.getTime(); int oldHoverIndex = hoverIndex; // clicked node is already expanded @@ -691,7 +689,7 @@ public void mouseClicked(int button, int x, int y, int clickCount) { } // restore hover data - hoverOffset = oldHoverOffset; + hoverOffset.setTime(oldHoverOffsetTime); hoverIndex = oldHoverIndex; // open beatmap menu @@ -827,11 +825,11 @@ public void keyPressed(int key, char c) { if (next != null) { SoundController.playSound(SoundEffect.MENUCLICK); BeatmapSetNode oldStartNode = startNode; - float oldHoverOffset = hoverOffset; + int oldHoverOffsetTime = hoverOffset.getTime(); int oldHoverIndex = hoverIndex; setFocus(next, 0, false, true); if (startNode == oldStartNode) { - hoverOffset = oldHoverOffset; + hoverOffset.setTime(oldHoverOffsetTime); hoverIndex = oldHoverIndex; } } @@ -843,11 +841,11 @@ public void keyPressed(int key, char c) { if (prev != null) { SoundController.playSound(SoundEffect.MENUCLICK); BeatmapSetNode oldStartNode = startNode; - float oldHoverOffset = hoverOffset; + int oldHoverOffsetTime = hoverOffset.getTime(); int oldHoverIndex = hoverIndex; setFocus(prev, (prev.index == focusNode.index) ? 0 : prev.getBeatmapSet().size() - 1, false, true); if (startNode == oldStartNode) { - hoverOffset = oldHoverOffset; + hoverOffset.setTime(oldHoverOffsetTime); hoverIndex = oldHoverIndex; } } @@ -945,7 +943,7 @@ public void enter(GameContainer container, StateBasedGame game) selectRandomButton.resetHover(); selectMapOptionsButton.resetHover(); selectOptionsButton.resetHover(); - hoverOffset = 0f; + hoverOffset.setTime(0); hoverIndex = -1; startScore = 0; beatmapMenuTimer = -1; @@ -1083,7 +1081,7 @@ else if (startNode.next != null) oldFocusNode = null; randomStack = new Stack(); songInfo = null; - hoverOffset = 0f; + hoverOffset.setTime(0); hoverIndex = -1; search.setText(""); searchTimer = SEARCH_DELAY; @@ -1164,7 +1162,7 @@ private void changeIndex(int shift) { break; } if (shifted) { - hoverOffset = 0f; + hoverOffset.setTime(0); hoverIndex = -1; } return; @@ -1182,7 +1180,7 @@ public BeatmapSetNode setFocus(BeatmapSetNode node, int beatmapIndex, boolean ch if (node == null) return null; - hoverOffset = 0f; + hoverOffset.setTime(0); hoverIndex = -1; songInfo = null; BeatmapSetNode oldFocus = focusNode; diff --git a/src/itdelatrisu/opsu/states/Splash.java b/src/itdelatrisu/opsu/states/Splash.java index 2f424984..b547df17 100644 --- a/src/itdelatrisu/opsu/states/Splash.java +++ b/src/itdelatrisu/opsu/states/Splash.java @@ -31,6 +31,8 @@ import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.replay.ReplayImporter; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimatedValue; +import itdelatrisu.opsu.ui.animations.AnimationEquation; /* import java.io.File; @@ -38,7 +40,6 @@ 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.state.BasicGameState; @@ -50,6 +51,9 @@ * Loads game resources and enters "Main Menu" state. */ public class Splash extends BasicGameState { + /** Minimum time, in milliseconds, to display the splash screen (and fade in the logo). */ + private static final int MIN_SPLASH_TIME = 400; + /** Whether or not loading has completed. */ private boolean finished = false; @@ -62,6 +66,9 @@ public class Splash extends BasicGameState { /** Whether the skin being loaded is a new skin (for program restarts). */ private boolean newSkin = false; + /** Logo alpha level. */ + private AnimatedValue logoAlpha; + // game-related variables private int state; private GameContainer container; @@ -83,6 +90,8 @@ public void init(GameContainer container, StateBasedGame game) // load Utils class first (needed in other 'init' methods) Utils.init(container, game); + // fade in logo + this.logoAlpha = new AnimatedValue(MIN_SPLASH_TIME, 0f, 1f, AnimationEquation.LINEAR); GameImage.MENU_LOGO.getImage().setAlpha(0f); } @@ -147,13 +156,11 @@ public void run() { } // fade in logo - Image logo = GameImage.MENU_LOGO.getImage(); - float alpha = logo.getAlpha(); - if (alpha < 1f) - logo.setAlpha(alpha + (delta / 500f)); + if (logoAlpha.update(delta)) + GameImage.MENU_LOGO.getImage().setAlpha(logoAlpha.getValue()); // change states when loading complete - if (finished && alpha >= 1f) { + if (finished && logoAlpha.getValue() >= 1f) { // initialize song list if (BeatmapSetList.get().size() > 0) { BeatmapSetList.get().init(); diff --git a/src/itdelatrisu/opsu/ui/Cursor.java b/src/itdelatrisu/opsu/ui/Cursor.java index e2d50a69..ee1d50ad 100644 --- a/src/itdelatrisu/opsu/ui/Cursor.java +++ b/src/itdelatrisu/opsu/ui/Cursor.java @@ -133,7 +133,7 @@ public void draw(int mouseX, int mouseY, boolean mousePressed) { cursorMiddle = GameImage.CURSOR_MIDDLE.getImage(); int removeCount = 0; - int FPSmod = (Options.getTargetFPS() / 60); + float FPSmod = Math.max(container.getFPS(), 1) / 60f; Skin skin = Options.getSkin(); // scale cursor @@ -143,8 +143,6 @@ public void draw(int mouseX, int mouseY, boolean mousePressed) { if (cursorScale != 1f) { cursor = cursor.getScaledCopy(cursorScale); cursorTrail = cursorTrail.getScaledCopy(cursorScale); - if (hasMiddle) - cursorMiddle = cursorMiddle.getScaledCopy(cursorScale); } // TODO: use an image buffer @@ -159,13 +157,13 @@ public void draw(int mouseX, int mouseY, boolean mousePressed) { lastX = mouseX; lastY = mouseY; - removeCount = (cursorX.size() / (6 * FPSmod)) + 1; + removeCount = (int) (cursorX.size() / (6 * FPSmod)) + 1; } else { // old style: sample one point at a time cursorX.add(mouseX); cursorY.add(mouseY); - int max = 10 * FPSmod; + int max = (int) (10 * FPSmod); if (cursorX.size() > max) removeCount = cursorX.size() - max; } diff --git a/src/itdelatrisu/opsu/ui/MenuButton.java b/src/itdelatrisu/opsu/ui/MenuButton.java index e7f964e0..1ac2fd9b 100644 --- a/src/itdelatrisu/opsu/ui/MenuButton.java +++ b/src/itdelatrisu/opsu/ui/MenuButton.java @@ -19,6 +19,8 @@ package itdelatrisu.opsu.ui; import itdelatrisu.opsu.Utils; +import itdelatrisu.opsu.ui.animations.AnimatedValue; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import fluddokt.opsu.fake.*; /* @@ -66,11 +68,26 @@ public class MenuButton { /** The hover actions for this button. */ private int hoverEffect = 0; - /** The current and max scale of the button. */ - private float scale = 1f, hoverScale = 1.25f; + /** The hover animation duration, in milliseconds. */ + private int animationDuration = 100; - /** The current and base alpha level of the button. */ - private float alpha = 1f, baseAlpha = 0.75f; + /** The hover animation equation. */ + private AnimationEquation animationEqn = AnimationEquation.LINEAR; + + /** Whether the animation is advancing forwards (if advancing automatically). */ + private boolean autoAnimationForward = true; + + /** The scale of the button. */ + private AnimatedValue scale; + + /** The default max scale of the button. */ + private static final float DEFAULT_SCALE_MAX = 1.25f; + + /** The alpha level of the button. */ + private AnimatedValue alpha; + + /** The default base alpha level of the button. */ + private static final float DEFAULT_ALPHA_BASE = 0.75f; /** The scaled expansion direction for the button. */ private Expand dir = Expand.CENTER; @@ -78,8 +95,11 @@ public class MenuButton { /** Scaled expansion directions. */ public enum Expand { CENTER, UP, RIGHT, LEFT, DOWN, UP_RIGHT, UP_LEFT, DOWN_RIGHT, DOWN_LEFT; } - /** The current and max rotation angles of the button. */ - private float angle = 0f, maxAngle = 30f; + /** The rotation angle of the button. */ + private AnimatedValue angle; + + /** The default max rotation angle of the button. */ + private static final float DEFAULT_ANGLE_MAX = 30f; /** * Creates a new button from an Image. @@ -195,15 +215,15 @@ public void draw(Color filter) { float oldAlpha = image.getAlpha(); float oldAngle = image.getRotation(); if ((hoverEffect & EFFECT_EXPAND) > 0) { - if (scale != 1f) { - image = image.getScaledCopy(scale); + if (scale.getValue() != 1f) { + image = image.getScaledCopy(scale.getValue()); image.setAlpha(oldAlpha); } } if ((hoverEffect & EFFECT_FADE) > 0) - image.setAlpha(alpha); + image.setAlpha(alpha.getValue()); if ((hoverEffect & EFFECT_ROTATE) > 0) - image.setRotation(angle); + image.setRotation(angle.getValue()); image.draw(x - xRadius, y - yRadius, filter); if (image == this.img) { image.setAlpha(oldAlpha); @@ -220,9 +240,10 @@ public void draw(Color filter) { imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter); } else if ((hoverEffect & EFFECT_FADE) > 0) { float a = image.getAlpha(), aL = imgL.getAlpha(), aR = imgR.getAlpha(); - image.setAlpha(alpha); - imgL.setAlpha(alpha); - imgR.setAlpha(alpha); + float currentAlpha = alpha.getValue(); + image.setAlpha(currentAlpha); + imgL.setAlpha(currentAlpha); + imgR.setAlpha(currentAlpha); image.draw(x - xRadius + imgL.getWidth(), y - yRadius, filter); imgL.draw(x - xRadius, y - yRadius, filter); imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter); @@ -270,28 +291,63 @@ public boolean contains(float cx, float cy, float alpha) { */ public void resetHover() { if ((hoverEffect & EFFECT_EXPAND) > 0) { - this.scale = 1f; + scale.setTime(0); setHoverRadius(); } if ((hoverEffect & EFFECT_FADE) > 0) - this.alpha = baseAlpha; + alpha.setTime(0); if ((hoverEffect & EFFECT_ROTATE) > 0) - this.angle = 0f; + angle.setTime(0); + autoAnimationForward = true; } /** * Removes all hover effects that have been set for the button. */ - public void removeHoverEffects() { hoverEffect = 0; } + public void removeHoverEffects() { + this.hoverEffect = 0; + this.scale = null; + this.alpha = null; + this.angle = null; + autoAnimationForward = true; + } + + /** + * Sets the hover animation duration. + * @param duration the duration, in milliseconds + */ + public void setHoverAnimationDuration(int duration) { + this.animationDuration = duration; + if (scale != null) + scale.setDuration(duration); + if (alpha != null) + alpha.setDuration(duration); + if (angle != null) + angle.setDuration(duration); + } + + /** + * Sets the hover animation equation. + * @param eqn the equation to use + */ + public void setHoverAnimationEquation(AnimationEquation eqn) { + this.animationEqn = eqn; + if (scale != null) + scale.setEquation(eqn); + if (alpha != null) + alpha.setEquation(eqn); + if (angle != null) + angle.setEquation(eqn); + } /** * Sets the "expand" hover effect. */ - public void setHoverExpand() { hoverEffect |= EFFECT_EXPAND; } + public void setHoverExpand() { setHoverExpand(DEFAULT_SCALE_MAX, this.dir); } /** * Sets the "expand" hover effect. - * @param scale the maximum scale factor (default 1.25f) + * @param scale the maximum scale factor */ public void setHoverExpand(float scale) { setHoverExpand(scale, this.dir); } @@ -299,45 +355,45 @@ public void resetHover() { * Sets the "expand" hover effect. * @param dir the expansion direction */ - public void setHoverExpand(Expand dir) { setHoverExpand(this.hoverScale, dir); } + public void setHoverExpand(Expand dir) { setHoverExpand(DEFAULT_SCALE_MAX, dir); } /** * Sets the "expand" hover effect. - * @param scale the maximum scale factor (default 1.25f) + * @param scale the maximum scale factor * @param dir the expansion direction */ public void setHoverExpand(float scale, Expand dir) { hoverEffect |= EFFECT_EXPAND; - this.hoverScale = scale; + this.scale = new AnimatedValue(animationDuration, 1f, scale, animationEqn); this.dir = dir; } /** * Sets the "fade" hover effect. */ - public void setHoverFade() { hoverEffect |= EFFECT_FADE; } + public void setHoverFade() { setHoverFade(DEFAULT_ALPHA_BASE); } /** * Sets the "fade" hover effect. - * @param baseAlpha the base alpha level to fade in from (default 0.7f) + * @param baseAlpha the base alpha level to fade in from */ public void setHoverFade(float baseAlpha) { hoverEffect |= EFFECT_FADE; - this.baseAlpha = baseAlpha; + this.alpha = new AnimatedValue(animationDuration, baseAlpha, 1f, animationEqn); } /** * Sets the "rotate" hover effect. */ - public void setHoverRotate() { hoverEffect |= EFFECT_ROTATE; } + public void setHoverRotate() { setHoverRotate(DEFAULT_ANGLE_MAX); } /** * Sets the "rotate" hover effect. - * @param maxAngle the maximum rotation angle, in degrees (default 30f) + * @param maxAngle the maximum rotation angle, in degrees */ public void setHoverRotate(float maxAngle) { hoverEffect |= EFFECT_ROTATE; - this.maxAngle = maxAngle; + this.angle = new AnimatedValue(animationDuration, 0f, maxAngle, animationEqn); } /** @@ -374,45 +430,53 @@ public void hoverUpdate(int delta, boolean isHover) { if (hoverEffect == 0) return; + int d = delta * (isHover ? 1 : -1); + // scale the button if ((hoverEffect & EFFECT_EXPAND) > 0) { - int sign = 0; - if (isHover && scale < hoverScale) - sign = 1; - else if (!isHover && scale > 1f) - sign = -1; - if (sign != 0) { - scale = Utils.getBoundedValue(scale, sign * (hoverScale - 1f) * delta / 100f, 1, hoverScale); + if (scale.update(d)) setHoverRadius(); - } } // fade the button - if ((hoverEffect & EFFECT_FADE) > 0) { - int sign = 0; - if (isHover && alpha < 1f) - sign = 1; - else if (!isHover && alpha > baseAlpha) - sign = -1; - if (sign != 0) - alpha = Utils.getBoundedValue(alpha, sign * (1f - baseAlpha) * delta / 200f, baseAlpha, 1f); - } + if ((hoverEffect & EFFECT_FADE) > 0) + alpha.update(d); // rotate the button - if ((hoverEffect & EFFECT_ROTATE) > 0) { - int sign = 0; - boolean right = (maxAngle > 0); - if (isHover && angle != maxAngle) - sign = (right) ? 1 : -1; - else if (!isHover && angle != 0) - sign = (right) ? -1 : 1; - if (sign != 0) { - float diff = sign * Math.abs(maxAngle) * delta / 125f; - angle = (right) ? - Utils.getBoundedValue(angle, diff, 0, maxAngle) : - Utils.getBoundedValue(angle, diff, maxAngle, 0); + if ((hoverEffect & EFFECT_ROTATE) > 0) + angle.update(d); + } + + /** + * Automatically advances the hover animation in a loop. + * @param delta the delta interval + * @param reverseAtEnd whether to reverse or restart the animation upon reaching the end + */ + public void autoHoverUpdate(int delta, boolean reverseAtEnd) { + if (hoverEffect == 0) + return; + + int time = ((hoverEffect & EFFECT_EXPAND) > 0) ? scale.getTime() : + ((hoverEffect & EFFECT_FADE) > 0) ? alpha.getTime() : + ((hoverEffect & EFFECT_ROTATE) > 0) ? angle.getTime() : -1; + if (time == -1) + return; + + int d = delta * (autoAnimationForward ? 1 : -1); + if (Utils.getBoundedValue(time, d, 0, animationDuration) == time) { + if (reverseAtEnd) + autoAnimationForward = !autoAnimationForward; + else { + if ((hoverEffect & EFFECT_EXPAND) > 0) + scale.setTime(0); + if ((hoverEffect & EFFECT_FADE) > 0) + alpha.setTime(0); + if ((hoverEffect & EFFECT_ROTATE) > 0) + angle.setTime(0); } } + + hoverUpdate(delta, autoAnimationForward); } /** @@ -425,10 +489,11 @@ private void setHoverRadius() { image = anim.getCurrentFrame(); int xOffset = 0, yOffset = 0; + float currentScale = scale.getValue(); if (dir != Expand.CENTER) { // offset by difference between normal/scaled image dimensions - xOffset = (int) ((scale - 1f) * image.getWidth()); - yOffset = (int) ((scale - 1f) * image.getHeight()); + xOffset = (int) ((currentScale - 1f) * image.getWidth()); + yOffset = (int) ((currentScale - 1f) * image.getHeight()); if (dir == Expand.UP || dir == Expand.DOWN) xOffset = 0; // no horizontal offset if (dir == Expand.RIGHT || dir == Expand.LEFT) @@ -438,7 +503,7 @@ private void setHoverRadius() { if (dir == Expand.DOWN || dir == Expand.DOWN_LEFT || dir == Expand.DOWN_RIGHT) yOffset *= -1; // flip y for down } - this.xRadius = ((image.getWidth() * scale) + xOffset) / 2f; - this.yRadius = ((image.getHeight() * scale) + yOffset) / 2f; + this.xRadius = ((image.getWidth() * currentScale) + xOffset) / 2f; + this.yRadius = ((image.getHeight() * currentScale) + yOffset) / 2f; } } diff --git a/src/itdelatrisu/opsu/ui/UI.java b/src/itdelatrisu/opsu/ui/UI.java index 2a513e2c..ec119ec2 100644 --- a/src/itdelatrisu/opsu/ui/UI.java +++ b/src/itdelatrisu/opsu/ui/UI.java @@ -28,6 +28,8 @@ import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.beatmap.BeatmapParser; import itdelatrisu.opsu.replay.ReplayImporter; +import itdelatrisu.opsu.ui.animations.AnimatedValue; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import javax.swing.JOptionPane; import javax.swing.UIManager; @@ -74,11 +76,8 @@ public class UI { /** Whether or not to check the current tooltip for line breaks. */ private static boolean tooltipNewlines; - /** The current tooltip timer. */ - private static int tooltipTimer = -1; - - /** Duration, in milliseconds, to fade tooltips. */ - private static final int TOOLTIP_FADE_TIME = 200; + /** The alpha level of the current tooltip (if any). */ + private static AnimatedValue tooltipAlpha = new AnimatedValue(200, 0f, 1f, AnimationEquation.LINEAR); // game-related variables private static GameContainer container; @@ -110,6 +109,8 @@ public static void init(GameContainer container, StateBasedGame game) Image back = GameImage.MENU_BACK.getImage(); backButton = new MenuButton(back, back.getWidth() / 2f, container.getHeight() - (back.getHeight() / 2f)); } + backButton.setHoverAnimationDuration(350); + backButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK); backButton.setHoverExpand(MenuButton.Expand.UP_RIGHT); } @@ -121,8 +122,7 @@ public static void update(int delta) { cursor.update(delta); updateVolumeDisplay(delta); updateBarNotification(delta); - if (tooltipTimer > 0) - tooltipTimer -= delta; + tooltipAlpha.update(-delta); } /** @@ -364,12 +364,7 @@ public static void updateTooltip(int delta, String s, boolean newlines) { if (s != null) { tooltip = s; tooltipNewlines = newlines; - if (tooltipTimer <= 0) - tooltipTimer = delta; - else - tooltipTimer += delta * 2; - if (tooltipTimer > TOOLTIP_FADE_TIME) - tooltipTimer = TOOLTIP_FADE_TIME; + tooltipAlpha.update(delta * 2); } } @@ -379,7 +374,7 @@ public static void updateTooltip(int delta, String s, boolean newlines) { * @param g the graphics context */ public static void drawTooltip(Graphics g) { - if (tooltipTimer <= 0 || tooltip == null) + if (tooltipAlpha.getTime() == 0 || tooltip == null) return; int containerWidth = container.getWidth(), containerHeight = container.getHeight(); @@ -412,7 +407,7 @@ else if (y < margin) y = margin; // draw tooltip text inside a filled rectangle - float alpha = (float) tooltipTimer / TOOLTIP_FADE_TIME; + float alpha = tooltipAlpha.getValue(); float oldAlpha = Utils.COLOR_BLACK_ALPHA.a; Utils.COLOR_BLACK_ALPHA.a = alpha; g.setColor(Utils.COLOR_BLACK_ALPHA); @@ -434,7 +429,7 @@ else if (y < margin) * Resets the tooltip. */ public static void resetTooltip() { - tooltipTimer = -1; + tooltipAlpha.setTime(0); tooltip = null; } diff --git a/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java b/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java new file mode 100644 index 00000000..5e1636f9 --- /dev/null +++ b/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java @@ -0,0 +1,124 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015 Jeffrey Han + * + * opsu! is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * opsu! is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with opsu!. If not, see . + */ + +package itdelatrisu.opsu.ui.animations; + +import itdelatrisu.opsu.Utils; + +/** + * Utility class for updating a value using an animation equation. + */ +public class AnimatedValue { + /** The animation duration, in milliseconds. */ + private int duration; + + /** The current time, in milliseconds. */ + private int time; + + /** The base value. */ + private float base; + + /** The maximum difference from the base value. */ + private float diff; + + /** The current value. */ + private float value; + + /** The animation equation to use. */ + private AnimationEquation eqn; + + /** + * Constructor. + * @param duration the total animation duration, in milliseconds + * @param min the minimum value + * @param max the maximum value + * @param eqn the animation equation to use + */ + public AnimatedValue(int duration, float min, float max, AnimationEquation eqn) { + this.time = 0; + this.duration = duration; + this.value = min; + this.base = min; + this.diff = max - min; + this.eqn = eqn; + } + + /** + * Returns the current value. + */ + public float getValue() { return value; } + + /** + * Returns the current animation time, in milliseconds. + */ + public int getTime() { return time; } + + /** + * Sets the animation time manually. + * @param time the new time, in milliseconds + */ + public void setTime(int time) { + this.time = Utils.clamp(time, 0, duration); + updateValue(); + } + + /** + * Sets the animation duration. + * @param duration the new duration, in milliseconds + */ + public void setDuration(int duration) { + this.duration = duration; + int newTime = Utils.clamp(time, 0, duration); + if (time != newTime) { + this.time = newTime; + updateValue(); + } + } + + /** + * Sets the animation equation to use. + * @param eqn the new equation + */ + public void setEquation(AnimationEquation eqn) { + this.eqn = eqn; + updateValue(); + } + + /** + * Updates the animation by a delta interval. + * @param delta the delta interval since the last call. + * @return true if an update was applied, false if the animation was not updated + */ + public boolean update(int delta) { + int newTime = Utils.getBoundedValue(time, delta, 0, duration); + if (time != newTime) { + this.time = newTime; + updateValue(); + return true; + } + return false; + } + + /** + * Recalculates the value by applying the animation equation with the current time. + */ + private void updateValue() { + float t = eqn.calc((float) time / duration); + this.value = base + (t * diff); + } +} diff --git a/src/itdelatrisu/opsu/ui/animations/AnimationEquation.java b/src/itdelatrisu/opsu/ui/animations/AnimationEquation.java new file mode 100644 index 00000000..a30c5b30 --- /dev/null +++ b/src/itdelatrisu/opsu/ui/animations/AnimationEquation.java @@ -0,0 +1,309 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015 Jeffrey Han + * + * opsu! is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * opsu! is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with opsu!. If not, see . + */ + +package itdelatrisu.opsu.ui.animations; + + +/* + * These equations are copyright (c) 2001 Robert Penner, all rights reserved, + * and are open source under the BSD License. + * http://www.opensource.org/licenses/bsd-license.php + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the author nor the names of contributors may be used + * to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Easing functions for animations. + * + * @author Robert Penner (http://robertpenner.com/easing/) + * @author CharlotteGore (https://github.com/CharlotteGore/functional-easing) + */ +public enum AnimationEquation { + /* Linear */ + LINEAR { + @Override + public float calc(float t) { return t; } + }, + + /* Quadratic */ + IN_QUAD { + @Override + public float calc(float t) { return t * t; } + }, + OUT_QUAD { + @Override + public float calc(float t) { return -1 * t * (t - 2); } + }, + IN_OUT_QUAD { + @Override + public float calc(float t) { + t = t * 2; + if (t < 1) + return 0.5f * t * t; + t = t - 1; + return -0.5f * (t * (t - 2) - 1); + } + }, + + /* Cubic */ + IN_CUBIC { + @Override + public float calc(float t) { return t * t * t; } + }, + OUT_CUBIC { + @Override + public float calc(float t) { + t = t - 1; + return t * t * t + 1; + } + }, + IN_OUT_CUBIC { + @Override + public float calc(float t) { + t = t * 2; + if (t < 1) + return 0.5f * t * t * t; + t = t - 2; + return 0.5f * (t * t * t + 2); + } + }, + + /* Quartic */ + IN_QUART { + @Override + public float calc(float t) { return t * t * t * t; } + }, + OUT_QUART { + @Override + public float calc(float t) { + t = t - 1; + return -1 * (t * t * t * t - 1); + } + }, + IN_OUT_QUART { + @Override + public float calc(float t) { + t = t * 2; + if (t < 1) + return 0.5f * t * t * t * t; + t = t - 2; + return -0.5f * (t * t * t * t - 2); + } + }, + + /* Quintic */ + IN_QUINT { + @Override + public float calc(float t) { return t * t * t * t * t; } + }, + OUT_QUINT { + @Override + public float calc(float t) { + t = t - 1; + return (t * t * t * t * t + 1); + } + }, + IN_OUT_QUINT { + @Override + public float calc(float t) { + t = t * 2; + if (t < 1) + return 0.5f * t * t * t * t * t; + t = t - 2; + return 0.5f * (t * t * t * t * t + 2); + } + }, + + /* Sine */ + IN_SINE { + @Override + public float calc(float t) { return -1 * (float) Math.cos(t * (Math.PI / 2)) + 1; } + }, + OUT_SINE { + @Override + public float calc(float t) { return (float) Math.sin(t * (Math.PI / 2)); } + }, + IN_OUT_SINE { + @Override + public float calc(float t) { return (float) (Math.cos(Math.PI * t) - 1) / -2; } + }, + + /* Exponential */ + IN_EXPO { + @Override + public float calc(float t) { return (t == 0) ? 0 : (float) Math.pow(2, 10 * (t - 1)); } + }, + OUT_EXPO { + @Override + public float calc(float t) { return (t == 1) ? 1 : (float) -Math.pow(2, -10 * t) + 1; } + }, + IN_OUT_EXPO { + @Override + public float calc(float t) { + if (t == 0 || t == 1) + return t; + t = t * 2; + if (t < 1) + return 0.5f * (float) Math.pow(2, 10 * (t - 1)); + t = t - 1; + return 0.5f * ((float) -Math.pow(2, -10 * t) + 2); + } + }, + + /* Circular */ + IN_CIRC { + @Override + public float calc(float t) { return -1 * ((float) Math.sqrt(1 - t * t) - 1); } + }, + OUT_CIRC { + @Override + public float calc(float t) { + t = t - 1; + return (float) Math.sqrt(1 - t * t); + } + }, + IN_OUT_CIRC { + @Override + public float calc(float t) { + t = t * 2; + if (t < 1) + return -0.5f * ((float) Math.sqrt(1 - t * t) - 1); + t = t - 2; + return 0.5f * ((float) Math.sqrt(1 - t * t) + 1); + } + }, + + /* Back */ + IN_BACK { + @Override + public float calc(float t) { return t * t * ((OVERSHOOT + 1) * t - OVERSHOOT); } + }, + OUT_BACK { + @Override + public float calc(float t) { + t = t - 1; + return t * t * ((OVERSHOOT + 1) * t + OVERSHOOT) + 1; + } + }, + IN_OUT_BACK { + @Override + public float calc(float t) { + float overshoot = OVERSHOOT * 1.525f; + t = t * 2; + if (t < 1) + return 0.5f * (t * t * ((overshoot + 1) * t - overshoot)); + t = t - 2; + return 0.5f * (t * t * ((overshoot + 1) * t + overshoot) + 2); + } + }, + + /* Bounce */ + IN_BOUNCE { + @Override + public float calc(float t) { return 1 - OUT_BOUNCE.calc(1 - t); } + }, + OUT_BOUNCE { + @Override + public float calc(float t) { + if (t < 0.36363636f) + return 7.5625f * t * t; + else if (t < 0.72727273f) { + t = t - 0.54545454f; + return 7.5625f * t * t + 0.75f; + } else if (t < 0.90909091f) { + t = t - 0.81818182f; + return 7.5625f * t * t + 0.9375f; + } else { + t = t - 0.95454546f; + return 7.5625f * t * t + 0.984375f; + } + } + }, + IN_OUT_BOUNCE { + @Override + public float calc(float t) { + if (t < 0.5f) + return IN_BOUNCE.calc(t * 2) * 0.5f; + return OUT_BOUNCE.calc(t * 2 - 1) * 0.5f + 0.5f; + } + }, + + /* Elastic */ + IN_ELASTIC { + @Override + public float calc(float t) { + if (t == 0 || t == 1) + return t; + float period = 0.3f; + t = t - 1; + return -((float) Math.pow(2, 10 * t) * (float) Math.sin(((t - period / 4) * (Math.PI * 2)) / period)); + } + }, + OUT_ELASTIC { + @Override + public float calc(float t) { + if (t == 0 || t == 1) + return t; + float period = 0.3f; + return (float) Math.pow(2, -10 * t) * (float) Math.sin((t - period / 4) * (Math.PI * 2) / period) + 1; + } + }, + IN_OUT_ELASTIC { + @Override + public float calc(float t) { + if (t == 0 || t == 1) + return t; + float period = 0.44999996f; + t = t * 2 - 1; + if (t < 0) + return -0.5f * ((float) Math.pow(2, 10 * t) * (float) Math.sin((t - period / 4) * (Math.PI * 2) / period)); + return (float) Math.pow(2, -10 * t) * (float) Math.sin((t - period / 4) * (Math.PI * 2) / period) * 0.5f + 1; + } + }; + + /** Overshoot constant for "back" easings. */ + private static final float OVERSHOOT = 1.70158f; + + /** + * Calculates a new {@code t} value using the animation equation. + * @param t the raw {@code t} value [0,1] + * @return the new {@code t} value [0,1] + */ + public abstract float calc(float t); +} diff --git a/src/org/newdawn/slick/Image.java b/src/org/newdawn/slick/Image.java index 1737a808..38d9e8be 100644 --- a/src/org/newdawn/slick/Image.java +++ b/src/org/newdawn/slick/Image.java @@ -595,6 +595,7 @@ public void draw(float x, float y) { * @param y The y location to draw the image at * @param filter The color to filter with when drawing */ + @Override public void draw(float x, float y, Color filter) { init(); draw(x,y,width,height, filter); @@ -719,6 +720,7 @@ public void draw(float x,float y,float scale,Color filter) { * @param height * The height to render the image at */ + @Override public void draw(float x,float y,float width,float height) { init(); draw(x,y,width,height,Color.white); @@ -797,6 +799,7 @@ public void drawSheared(float x,float y, float hshear, float vshear, Color filte * @param height The height to render the image at * @param filter The color to filter with while drawing */ + @Override public void draw(float x,float y,float width,float height,Color filter) { if (alpha != 1) { if (filter == null) { diff --git a/src/org/newdawn/slick/Input.java b/src/org/newdawn/slick/Input.java index 1066ca34..c74eff95 100644 --- a/src/org/newdawn/slick/Input.java +++ b/src/org/newdawn/slick/Input.java @@ -1233,7 +1233,7 @@ public void poll(int width, int height) { } while (Mouse.next()) { - if (Mouse.getEventButton() >= 0) { + if (Mouse.getEventButton() >= 0 && Mouse.getEventButton() < mousePressed.length) { if (Mouse.getEventButtonState()) { consumed = false; mousePressed[Mouse.getEventButton()] = true;