From 66016160a4278a4695af56fb920b02734bcd8a55 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sun, 15 Feb 2015 21:38:54 -0500 Subject: [PATCH 1/3] Song menu graphical updates. - Added "selection-*" images from Xiaounlimited's "Nexus Ivory" skin. These do the same thing as the F1-F3 keyboard buttons in the song menu. - Removed the old wrench icon and replaced it with an "Other Options" button. F1 no longer opens the options menu. - Moved the search bar to under the tabs and better simulate osu! behavior. Removed the old search icon. - Added solid black bars at the top and bottom of the song menu. Moved the top divider closer to the end of the information text. Cropped song button images to fit between the bars. Signed-off-by: Jeffrey Han --- README.md | 3 +- res/options.png | Bin 2095 -> 0 bytes res/search.png | Bin 2523 -> 0 bytes res/selection-mods-over.png | Bin 0 -> 5092 bytes res/selection-mods.png | Bin 0 -> 3650 bytes res/selection-options-over.png | Bin 0 -> 5116 bytes res/selection-options.png | Bin 0 -> 3669 bytes res/selection-random-over.png | Bin 0 -> 4690 bytes res/selection-random.png | Bin 0 -> 3522 bytes res/selection-selectoptions-over.png | Bin 0 -> 5248 bytes res/selection-selectoptions.png | Bin 0 -> 3665 bytes src/itdelatrisu/opsu/GameImage.java | 63 +++- src/itdelatrisu/opsu/OsuGroupNode.java | 32 ++- src/itdelatrisu/opsu/ScoreData.java | 12 +- src/itdelatrisu/opsu/SongSort.java | 14 +- src/itdelatrisu/opsu/Utils.java | 10 +- src/itdelatrisu/opsu/states/SongMenu.java | 334 ++++++++++++++-------- 17 files changed, 308 insertions(+), 160 deletions(-) delete mode 100644 res/options.png delete mode 100644 res/search.png create mode 100644 res/selection-mods-over.png create mode 100644 res/selection-mods.png create mode 100644 res/selection-options-over.png create mode 100644 res/selection-options.png create mode 100644 res/selection-random-over.png create mode 100644 res/selection-random.png create mode 100644 res/selection-selectoptions-over.png create mode 100644 res/selection-selectoptions.png diff --git a/README.md b/README.md index 2827413d..a6960d73 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,8 @@ folder if placed in the `SongPacks` directory. ### First Run The `Music Offset` value will likely need to be adjusted when playing for the first time, or whenever hit objects are out of sync with the music. This and -other game options can be accessed by clicking the wrench icon in the song menu. +other game options can be accessed by clicking the "Other Options" button in +the song menu. ## Building opsu! is distributed as a Maven project. diff --git a/res/options.png b/res/options.png deleted file mode 100644 index 74c38e455feb7782731c91eac0ab049af80ac10c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2095 zcma)-`!^GeAIE3I+%^=smO4shN)+ZcYKvGi%%wOkUC;>o=6>5u%3YzBE_h1V8qE{B zlJ%sgq)2TpF`LWsw6eLax$N@Q^A|kl^Lc;X=e*AQoX?N%B3~a*h=#ES004k^9YgtV zc~m$B=bV8>G_s(D-X;`5CTT+Zuj?EirLwxH#|ImidL1Xs=VQY8&j+gqQ z_gvM%1Zb(M_-P%%>0Q?s%z<4gQV6;0krfSS?;f`Fa5mqyP1E#FzeL z;ed7w*49YmB6O83PaJF`_O5>ZR+|+g8y?MKbPR`s^R}z~x20}6Ssj_p+8=3gz&xxi z*U=rjCi(83_GoqoZ_uhQHgIL^IdbB89v0LO><5QyTY>ijD1egOiBo^2HNeorP+kx( z67DX}uY)!N?Hl9t3E`Pv-^$?g@K*D_M;A|hj8*5hgx@2E$(k}lT1E}C*f4f=5X?eu zD~5`1?#w>uaxVcI7lf7C3A9;=+q0)qa3|U_rx<~x@g83{6ecGihx9M-Ck60m3>wMr zNxmI@w>(=MDsWckM7b3f-Ab+f8l) zZ(b_TGE#oi{zCu_Tpj*t^x)#@t9a)3EydlcPaz|fL~dj$fd7dLoZgQqXL&Cqnpq&2 z?r?WUsR2B2ep_D%AAld}hK#rzxhN;(UVy7fU+`Qh?WfCqV6qk_AMb7C6S0vt-OU(= z{RLbE4&I~7oO88Vx@Np{8+=*mg=YVsOSzEPw5lnFs!$mLFXn*p$VYf@xH}~Z=uGa` zZ)aQZ{8PwI^-2rDN_C)zhWuVIT-~(%K&wlw(qj-zL(2J)n?=rHz7}j|CNWF!HG|r$ zcj%tBoxd~|lYpUruTD(wJJEDVpX+19N?}UKnxV^2Bvp0FPFhlM`VPk)zr{A?KC*+7 z0Vc9N2Rd>mY7~+8*>pS3t$tgFn8kV(5Q0dC*tg0$(P8P0@t^Ikn{IDWfi|o@E*)%# zLDrGM!65UGIJ|M8ox2xSQL0De95J~doR)RAKZF}Wealy3Dn#uZ6!B@>7r53>yyq(~ ziJlG#I*;c?dyjwK_YM)6==}0&R%J=I&dN@L`D(3P2YEMSH)}-<((W(^AE0gB&L2+% zBDC(fPIcF*5FwpqQnhsF*ON_T@EF_oUR-4{Nta+i2-~`_BT+c7LjcXY7i*n=jh#bh z6cx%s_sE~C*RHP@Jx95Qc6rEfpSnea&x6o{_nj=ZV3>6= z=ABY1O$23!m}fU81f@0o97ByV!X1AiTm|}qR57XuD^Ezl#Uk|eF!R*lrh}voi-JkM zx$s$l?7Wl8_>q5HJq3Uk1Go?wv+!sJe+f;$-e5Hwxb1T1)&Tq5_5P@}IE@oeLbmw8VUC z7^K;1b_z9$x>ZL!!h?f7Rt^DNZbyyg$1dUG51YmB;nx;p8_sJu(zHfN{kaUw`R^3y1|MT zlY4L5Fz#zi9_vlLy7I+8Rm87+<3^ik=+~mBm9H$}#(tUaDj_-XB?kFyXHRLAfU_&% z6;xZY&DUD(cLK&1QK~s6@)^YeOM(+x%Ep%@x1L=9vm>ZKUJGi><&J_Up1O+U;sLW@ z#Y7(MRMT4jzrT^3pqLS}OwG6U;*Ye9DHDI6$gHZe+BCXIot{PkHDeOTsbxAO#@a;Y z$!Dyet67kpmt*%xe$mk6lkbY94{d7mYr zlrN2r3UW3Ra&NGHnD;p#=BQwJL1$(3&UGV23SCypot@Rm;BpcJ__D6K`&*OFSZ9bh z6(6c|k76(_1O<$Y_KVHX_S188)3Rdm<_JJ?f*y&uSos=jjc;UTedyC!GD1b(;JGv(+q)L zRT>7SEh#?Uz-V^Wfx34k)(mMj?@ny)JlB&*>6CDXtomxT9O(x&tbA^EeKq`_w20L| zH)1%uL~KYo=+Re^&HUNl63ru zghjafjkRCNRzEKJJubL%e+MZvKU4n-@NW%mvK>=(ak%XRrnknxi}Q?3dULq%zZRle cqpbp@+mi=t(wh(8sCIzYQ6JR9BawOk2l~wKI{*Lx diff --git a/res/search.png b/res/search.png deleted file mode 100644 index 2382d41919c742093f1403384237bbf82bb66a12..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2523 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 literal 0 HcmV?d00001 diff --git a/res/selection-mods.png b/res/selection-mods.png new file mode 100644 index 0000000000000000000000000000000000000000..53078a2e7af8f3193b4b4030ace0b0d106384402 GIT binary patch literal 3650 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 literal 0 HcmV?d00001 diff --git a/res/selection-random.png b/res/selection-random.png new file mode 100644 index 0000000000000000000000000000000000000000..8afd55d3ef4bed13c224fabeb4b8e458cd4a83cd GIT binary patch literal 3522 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 literal 0 HcmV?d00001 diff --git a/res/selection-selectoptions.png b/res/selection-selectoptions.png new file mode 100644 index 0000000000000000000000000000000000000000..ab60811ff57c36faced34a3fd62343c651e3ba2f GIT binary patch literal 3665 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 CH`eU{ literal 0 HcmV?d00001 diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index ec6a3ff3..cdb4e999 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -368,6 +368,54 @@ public enum GameImage { }, // Non-Game Components + SELECTION_MODS ("selection-mods", "png", false, false) { + @Override + protected Image process_sub(Image img, int w, int h) { + return img.getScaledCopy((h * 0.115f) / img.getHeight()); + } + }, + SELECTION_MODS_OVERLAY ("selection-mods-over", "png", false, false) { + @Override + protected Image process_sub(Image img, int w, int h) { + return img.getScaledCopy((h * 0.115f) / img.getHeight()); + } + }, + SELECTION_RANDOM ("selection-random", "png", false, false) { + @Override + protected Image process_sub(Image img, int w, int h) { + return img.getScaledCopy((h * 0.115f) / img.getHeight()); + } + }, + SELECTION_RANDOM_OVERLAY ("selection-random-over", "png", false, false) { + @Override + protected Image process_sub(Image img, int w, int h) { + return img.getScaledCopy((h * 0.115f) / img.getHeight()); + } + }, + SELECTION_OPTIONS ("selection-options", "png", false, false) { + @Override + protected Image process_sub(Image img, int w, int h) { + return img.getScaledCopy((h * 0.115f) / img.getHeight()); + } + }, + SELECTION_OPTIONS_OVERLAY ("selection-options-over", "png", false, false) { + @Override + protected Image process_sub(Image img, int w, int h) { + return img.getScaledCopy((h * 0.115f) / img.getHeight()); + } + }, + SELECTION_OTHER_OPTIONS ("selection-selectoptions", "png", false, false) { + @Override + protected Image process_sub(Image img, int w, int h) { + return img.getScaledCopy((h * 0.115f) / img.getHeight()); + } + }, + SELECTION_OTHER_OPTIONS_OVERLAY ("selection-selectoptions-over", "png", false, false) { + @Override + protected Image process_sub(Image img, int w, int h) { + return img.getScaledCopy((h * 0.115f) / img.getHeight()); + } + }, VOLUME ("volume-bg", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { @@ -383,7 +431,8 @@ public enum GameImage { MENU_BUTTON_BG ("menu-button-background", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy(w / 2, h / SongMenu.MAX_SONG_BUTTONS); + // TODO: scale these properly (messy due to non-cropped images) + return img.getScaledCopy(w / 2, (int) (h * 0.95f) / SongMenu.MAX_SONG_BUTTONS); } }, MENU_TAB ("selection-tab", "png", false, false) { @@ -392,18 +441,6 @@ public enum GameImage { return img.getScaledCopy((h * 0.033f) / img.getHeight()); } }, - MENU_SEARCH ("search", "png", false, false) { - @Override - protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy(Utils.FONT_BOLD.getLineHeight() * 2f / img.getHeight()); - } - }, - MENU_OPTIONS ("options", "png", false, false) { - @Override - protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy(Utils.FONT_BOLD.getLineHeight() * 2f / img.getHeight()); - } - }, MENU_MUSICNOTE ("music-note", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { diff --git a/src/itdelatrisu/opsu/OsuGroupNode.java b/src/itdelatrisu/opsu/OsuGroupNode.java index 69e8eb39..3cefe22e 100644 --- a/src/itdelatrisu/opsu/OsuGroupNode.java +++ b/src/itdelatrisu/opsu/OsuGroupNode.java @@ -54,38 +54,54 @@ public class OsuGroupNode { * Draws the button. * @param x the x coordinate * @param y the y coordinate + * @param headerY the header end y coordinate (for cropping) + * @param footerY the footer start y coordinate (for cropping) * @param grade the highest grade, if any * @param focus true if this is the focused node */ - public void draw(float x, float y, Grade grade, boolean focus) { + public void draw(float x, float y, float headerY, float footerY, Grade grade, boolean focus) { boolean expanded = (osuFileIndex > -1); - float xOffset = 0f; OsuFile osu; - Color textColor = Color.lightGray; Image bg = GameImage.MENU_BUTTON_BG.getImage(); + Color bgColor; + Color textColor = Color.lightGray; + // draw song button background if (expanded) { // expanded - xOffset = bg.getWidth() / 10f; + x -= bg.getWidth() / 10f; if (focus) { - bg.draw(x - xOffset, y, Color.white); + bgColor = Color.white; textColor = Color.white; } else - bg.draw(x - xOffset, y, Utils.COLOR_BLUE_BUTTON); + bgColor = Utils.COLOR_BLUE_BUTTON; osu = osuFiles.get(osuFileIndex); } else { - bg.draw(x, y, Utils.COLOR_ORANGE_BUTTON); + bgColor = Utils.COLOR_ORANGE_BUTTON; osu = osuFiles.get(0); } + // crop image if necessary + if (y < headerY) { + int cropHeight = (int) (headerY - y); + Image bgCropped = bg.getSubImage(0, cropHeight, bg.getWidth(), bg.getHeight() - cropHeight); + bgCropped.draw(x, headerY, bgColor); + } else if (y + bg.getHeight() > footerY) { + int cropHeight = (int) (footerY - y); + Image bgCropped = bg.getSubImage(0, 0, bg.getWidth(), cropHeight); + bgCropped.draw(x, y, bgColor); + } else + bg.draw(x, y, bgColor); - float cx = x + (bg.getWidth() * 0.05f) - xOffset; + float cx = x + (bg.getWidth() * 0.05f); float cy = y + (bg.getHeight() * 0.2f) - 3; + // draw grade if (grade != Grade.NULL) { Image gradeImg = grade.getMenuImage(); gradeImg.drawCentered(cx - bg.getWidth() * 0.01f + gradeImg.getWidth() / 2f, y + bg.getHeight() / 2.2f); cx += gradeImg.getWidth(); } + // draw text Utils.FONT_MEDIUM.drawString(cx, cy, osu.getTitle(), textColor); Utils.FONT_DEFAULT.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() - 4, String.format("%s // %s", osu.getArtist(), osu.creator), textColor); diff --git a/src/itdelatrisu/opsu/ScoreData.java b/src/itdelatrisu/opsu/ScoreData.java index 5ad238a1..a08ea718 100644 --- a/src/itdelatrisu/opsu/ScoreData.java +++ b/src/itdelatrisu/opsu/ScoreData.java @@ -78,13 +78,13 @@ public class ScoreData implements Comparable { /** * Initializes the base coordinates for drawing. - * @param width the container width - * @param height the container height + * @param containerWidth the container width + * @param topY the top y coordinate */ - public static void init(int width, int height) { - baseX = width * 0.01f; - baseY = height * 0.16f; - buttonWidth = width * 0.4f; + public static void init(int containerWidth, float topY) { + baseX = containerWidth * 0.01f; + baseY = topY; + buttonWidth = containerWidth * 0.4f; float gradeHeight = GameImage.MENU_BUTTON_BG.getImage().getHeight() * 0.45f; buttonHeight = Math.max(gradeHeight, Utils.FONT_DEFAULT.getLineHeight() * 3.03f); buttonOffset = buttonHeight + gradeHeight / 10f; diff --git a/src/itdelatrisu/opsu/SongSort.java b/src/itdelatrisu/opsu/SongSort.java index f2a8016f..5b28ea90 100644 --- a/src/itdelatrisu/opsu/SongSort.java +++ b/src/itdelatrisu/opsu/SongSort.java @@ -145,21 +145,21 @@ public enum SongSort { /** * Initializes the sort tab. - * @param width the container width - * @param height the container height + * @param containerWidth the container width + * @param bottomY the bottom y coordinate */ - public void init(int width, int height) { + public void init(int containerWidth, float bottomY) { Image tab = GameImage.MENU_TAB.getImage(); int tabWidth = tab.getWidth(); - float buttonX = width / 2f; - float tabOffset = (width - buttonX - tabWidth) / (SIZE - 1); + float buttonX = containerWidth / 2f; + float tabOffset = (containerWidth - buttonX - tabWidth) / (SIZE - 1); if (tabOffset > tabWidth) { // prevent tabs from being spaced out tabOffset = tabWidth; - buttonX = (width * 0.99f) - (tabWidth * SIZE); + buttonX = (containerWidth * 0.99f) - (tabWidth * SIZE); } this.tab = new MenuButton(tab, (buttonX + (tabWidth / 2f)) + (id * tabOffset), - (height * 0.15f) - (tab.getHeight() / 2f) - 2f + bottomY - (tab.getHeight() / 2f) ); } diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 39270d73..2d126c37 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -82,7 +82,8 @@ public class Utils { COLOR_GREEN = new Color(137, 201, 79), COLOR_LIGHT_ORANGE = new Color(255,192,128), COLOR_LIGHT_GREEN = new Color(128,255,128), - COLOR_LIGHT_BLUE = new Color(128,128,255); + COLOR_LIGHT_BLUE = new Color(128,128,255), + COLOR_GREEN_SEARCH = new Color(173, 255, 47); /** The default map colors, used when a map does not provide custom colors. */ public static final Color[] DEFAULT_COMBO = { @@ -215,16 +216,9 @@ public class Utils { for (GameMod mod : GameMod.values()) mod.init(width, height); - // initialize sorts - for (SongSort sort : SongSort.values()) - sort.init(width, height); - // initialize hit objects OsuHitObject.init(width, height); - // initialize score data buttons - ScoreData.init(width, height); - // initialize download nodes DownloadNode.init(width, height); diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index be4d93be..d616125c 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -83,6 +83,12 @@ public class SongMenu extends BasicGameState { /** Maximum x offset of song buttons for mouse hover, in pixels. */ private static final float MAX_HOVER_OFFSET = 30f; + /** Time, in milliseconds, for the search bar to fade in or out. */ + private static final int SEARCH_TRANSITION_TIME = 250; + + /** Line width of the header/footer divider. */ + private static final int DIVIDER_LINE_WIDTH = 4; + /** Song node class representing an OsuGroupNode and file index. */ private static class SongNode { /** Song node. */ @@ -136,8 +142,8 @@ public class SongMenu extends BasicGameState { /** Current index of hovered song button. */ private int hoverIndex = -1; - /** The options button (to enter the "Game Options" menu). */ - private MenuButton optionsButton; + /** The selection buttons. */ + private MenuButton selectModsButton, selectRandomButton, selectMapOptionsButton, selectOptionsButton; /** The search textfield. */ private TextField search; @@ -146,10 +152,10 @@ public class SongMenu extends BasicGameState { * Delay timer, in milliseconds, before running another search. * This is overridden by character entry (reset) and 'esc' (immediate search). */ - private int searchTimer; + private int searchTimer = 0; /** Information text to display based on the search query. */ - private String searchResultString; + private String searchResultString = null; /** Loader animation. */ private Animation loader; @@ -184,6 +190,12 @@ public class SongMenu extends BasicGameState { /** Current start score (topmost score entry). */ private int startScore = 0; + /** Header and footer end and start y coordinates, respectively. */ + private float headerY, footerY; + + /** Time, in milliseconds, for fading the search bar. */ + private int searchTransitionTimer = SEARCH_TRANSITION_TIME; + // game-related variables private GameContainer container; private StateBasedGame game; @@ -204,26 +216,35 @@ public class SongMenu extends BasicGameState { int width = container.getWidth(); int height = container.getHeight(); + // header/footer coordinates + headerY = height * 0.0075f + GameImage.MENU_MUSICNOTE.getImage().getHeight() + + Utils.FONT_BOLD.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() + + Utils.FONT_SMALL.getLineHeight(); + footerY = height - GameImage.SELECTION_MODS.getImage().getHeight(); + + // initialize sorts + for (SongSort sort : SongSort.values()) + sort.init(width, headerY - SongMenu.DIVIDER_LINE_WIDTH / 2); + + // initialize score data buttons + ScoreData.init(width, headerY + height * 0.01f); + // song button background & graphics context Image menuBackground = GameImage.MENU_BUTTON_BG.getImage(); // song button coordinates buttonX = width * 0.6f; - buttonY = height * 0.16f; + buttonY = headerY; buttonWidth = menuBackground.getWidth(); buttonHeight = menuBackground.getHeight(); - buttonOffset = (height * 0.8f) / MAX_SONG_BUTTONS; + buttonOffset = (footerY - headerY - DIVIDER_LINE_WIDTH) / MAX_SONG_BUTTONS; // search - searchTimer = 0; - searchResultString = "Type to search!"; - Image searchIcon = GameImage.MENU_SEARCH.getImage(); - Image tab = GameImage.MENU_TAB.getImage(); + int textFieldX = (int) (width * 0.7125f + Utils.FONT_BOLD.getWidth("Search: ")); + int textFieldY = (int) (headerY + Utils.FONT_BOLD.getLineHeight() / 2); search = new TextField( - container, Utils.FONT_DEFAULT, - (int) buttonX + (tab.getWidth() / 2) + searchIcon.getWidth(), - (int) ((height * 0.15f) - (tab.getHeight() * 2.5f)), - (int) (buttonWidth / 2), Utils.FONT_DEFAULT.getLineHeight() + container, Utils.FONT_BOLD, textFieldX, textFieldY, + (int) (width * 0.99f) - textFieldX, Utils.FONT_BOLD.getLineHeight() ); search.setBackgroundColor(Color.transparent); search.setBorderColor(Color.transparent); @@ -231,10 +252,22 @@ public class SongMenu extends BasicGameState { search.setConsumeEvents(false); search.setMaxLength(60); - // options button - Image optionsIcon = GameImage.MENU_OPTIONS.getImage(); - optionsButton = new MenuButton(optionsIcon, search.getX() - (optionsIcon.getWidth() * 1.5f), search.getY()); - optionsButton.setHoverExpand(1.75f); + // selection buttons + float selectX = GameImage.MENU_BACK.getImage().getWidth() * 1.75f; + float selectY = height - GameImage.SELECTION_MODS.getImage().getHeight() / 2f; + float selectOffset = GameImage.SELECTION_MODS.getImage().getWidth() * 1.05f; + selectModsButton = new MenuButton(GameImage.SELECTION_MODS_OVERLAY.getImage(), + selectX, selectY); + selectRandomButton = new MenuButton(GameImage.SELECTION_RANDOM_OVERLAY.getImage(), + selectX + selectOffset, selectY); + selectMapOptionsButton = new MenuButton(GameImage.SELECTION_OPTIONS_OVERLAY.getImage(), + selectX + selectOffset * 2f, selectY); + selectOptionsButton = new MenuButton(GameImage.SELECTION_OTHER_OPTIONS_OVERLAY.getImage(), + selectX + selectOffset * 3f, selectY); + selectModsButton.setHoverFade(0f); + selectRandomButton.setHoverFade(0f); + selectMapOptionsButton.setHoverFade(0f); + selectOptionsButton.setHoverFade(0f); // loader int loaderDim = GameImage.MENU_MUSICNOTE.getImage().getWidth(); @@ -258,19 +291,19 @@ public class SongMenu extends BasicGameState { GameImage.PLAYFIELD.getImage().draw(); } - // header setup - float lowerBound = height * 0.15f; - g.setColor(Utils.COLOR_BLACK_ALPHA); - g.fillRect(0, 0, width, lowerBound); + // top/bottom bars + g.setColor(Color.black); + g.fillRect(0, 0, width, headerY); + g.fillRect(0, footerY, width, height - footerY); g.setColor(Utils.COLOR_BLUE_DIVIDER); - g.setLineWidth(2f); - g.drawLine(0, lowerBound, width, lowerBound); + g.setLineWidth(DIVIDER_LINE_WIDTH); + g.drawLine(0, headerY, width, headerY); + g.drawLine(0, footerY - DIVIDER_LINE_WIDTH / 2, width, footerY - DIVIDER_LINE_WIDTH / 2); g.resetLineWidth(); // header if (focusNode != null) { float marginX = width * 0.005f, marginY = height * 0.005f; - Image musicNote = GameImage.MENU_MUSICNOTE.getImage(); if (MusicController.isTrackLoading()) loader.draw(marginX, marginY); @@ -282,14 +315,14 @@ public class SongMenu extends BasicGameState { if (songInfo == null) songInfo = focusNode.getInfo(); marginX += 5; - Utils.FONT_LARGE.drawString(marginX + iconWidth, marginY, songInfo[0], Color.white); - Utils.FONT_DEFAULT.drawString(marginX + iconWidth, marginY + Utils.FONT_LARGE.getLineHeight() * 0.75f, songInfo[1], Color.white); - float headerY = marginY + iconHeight; - Utils.FONT_BOLD.drawString(marginX, headerY, songInfo[2], Color.white); - headerY += Utils.FONT_BOLD.getLineHeight() - 6; - Utils.FONT_DEFAULT.drawString(marginX, headerY, songInfo[3], Color.white); - headerY += Utils.FONT_DEFAULT.getLineHeight() - 4; - Utils.FONT_SMALL.drawString(marginX, headerY, songInfo[4], Color.white); + Utils.FONT_LARGE.drawString(marginX + iconWidth * 1.05f, marginY, songInfo[0], Color.white); + Utils.FONT_DEFAULT.drawString(marginX + iconWidth * 1.05f, marginY + Utils.FONT_LARGE.getLineHeight() * 0.75f, songInfo[1], Color.white); + float headerTextY = marginY + iconHeight; + Utils.FONT_BOLD.drawString(marginX, headerTextY, songInfo[2], Color.white); + headerTextY += Utils.FONT_BOLD.getLineHeight() - 6; + Utils.FONT_DEFAULT.drawString(marginX, headerTextY, songInfo[3], Color.white); + headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 4; + Utils.FONT_SMALL.drawString(marginX, headerTextY, songInfo[4], Color.white); } // song buttons @@ -300,6 +333,7 @@ public class SongMenu extends BasicGameState { ScoreData[] scores = getScoreDataForNode(node, false); node.draw( buttonX - offset, buttonY + (i*buttonOffset), + headerY + DIVIDER_LINE_WIDTH / 2, footerY - DIVIDER_LINE_WIDTH, (scores == null) ? Grade.NULL : scores[0].getGrade(), (node == focusNode) ); @@ -324,8 +358,16 @@ public class SongMenu extends BasicGameState { ScoreData.drawScrollbar(g, startScore, focusScores.length); } - // options button - optionsButton.draw(); + // selection buttons + // TODO +// GameImage.SELECTION_MODS.getImage().drawCentered(selectModsButton.getX(), selectModsButton.getY()); +// selectModsButton.draw(); + GameImage.SELECTION_RANDOM.getImage().drawCentered(selectRandomButton.getX(), selectRandomButton.getY()); + selectRandomButton.draw(); + GameImage.SELECTION_OPTIONS.getImage().drawCentered(selectMapOptionsButton.getX(), selectMapOptionsButton.getY()); + selectMapOptionsButton.draw(); + GameImage.SELECTION_OTHER_OPTIONS.getImage().drawCentered(selectOptionsButton.getX(), selectOptionsButton.getY()); + selectOptionsButton.draw(); // sorting tabs SongSort currentSort = SongSort.getSort(); @@ -343,15 +385,37 @@ public class SongMenu extends BasicGameState { currentSort.draw(true, false); // search - Image searchIcon = GameImage.MENU_SEARCH.getImage(); - Utils.FONT_BOLD.drawString( - search.getX(), search.getY() - Utils.FONT_BOLD.getLineHeight(), - searchResultString, Color.white - ); - searchIcon.draw(search.getX() - searchIcon.getWidth(), - search.getY() - Utils.FONT_DEFAULT.getLineHeight()); - g.setColor(Color.white); - search.render(container, g); + boolean searchEmpty = search.getText().isEmpty(); + int searchX = search.getX(), searchY = search.getY(); + float searchBaseX = width * 0.7f; + float searchTextX = width * 0.7125f; + float searchRectHeight = Utils.FONT_BOLD.getLineHeight() * 2; + float searchExtraHeight = Utils.FONT_DEFAULT.getLineHeight() * 0.7f; + float searchProgress = (searchTransitionTimer < SEARCH_TRANSITION_TIME) ? + ((float) searchTransitionTimer / SEARCH_TRANSITION_TIME) : 1f; + float oldAlpha = Utils.COLOR_BLACK_ALPHA.a; + if (searchEmpty) { + searchRectHeight += (1f - searchProgress) * searchExtraHeight; + Utils.COLOR_BLACK_ALPHA.a = 0.5f - searchProgress * 0.3f; + } else { + searchRectHeight += searchProgress * searchExtraHeight; + Utils.COLOR_BLACK_ALPHA.a = 0.2f + searchProgress * 0.3f; + } + g.setColor(Utils.COLOR_BLACK_ALPHA); + g.fillRect(searchBaseX, headerY + DIVIDER_LINE_WIDTH / 2, width - searchBaseX, searchRectHeight); + Utils.COLOR_BLACK_ALPHA.a = oldAlpha; + Utils.FONT_BOLD.drawString(searchTextX, searchY, "Search:", Utils.COLOR_GREEN_SEARCH); + if (searchEmpty) + Utils.FONT_BOLD.drawString(searchX, searchY, "Type to search!", Color.white); + else { + g.setColor(Color.white); + // TODO: why is this needed to correctly position the TextField? + search.setLocation(searchX - 3, searchY - 1); + search.render(container, g); + search.setLocation(searchX, searchY); + Utils.FONT_DEFAULT.drawString(searchTextX, searchY + Utils.FONT_BOLD.getLineHeight(), + (searchResultString == null) ? "Searching..." : searchResultString, Color.white); + } // scroll bar if (focusNode != null) { @@ -364,7 +428,7 @@ public class SongMenu extends BasicGameState { else if (startNode.index == focusNode.index) startIndex += startNode.osuFileIndex; Utils.drawScrollbar(g, startIndex, totalNodes, MAX_SONG_BUTTONS, - width, height * 0.16f, 0, buttonHeight, buttonOffset, + width, headerY + DIVIDER_LINE_WIDTH / 2, 0, buttonOffset - DIVIDER_LINE_WIDTH * 1.5f, buttonOffset, Utils.COLOR_BLACK_ALPHA, Color.white, true); } } @@ -394,7 +458,10 @@ public class SongMenu extends BasicGameState { Utils.updateVolumeDisplay(delta); int mouseX = input.getMouseX(), mouseY = input.getMouseY(); Utils.getBackButton().hoverUpdate(delta, mouseX, mouseY); - optionsButton.hoverUpdate(delta, mouseX, mouseY); + selectModsButton.hoverUpdate(delta, mouseX, mouseY); + selectRandomButton.hoverUpdate(delta, mouseX, mouseY); + selectMapOptionsButton.hoverUpdate(delta, mouseX, mouseY); + selectOptionsButton.hoverUpdate(delta, mouseX, mouseY); // beatmap menu timer if (beatmapMenuTimer > -1) { @@ -425,7 +492,7 @@ public class SongMenu extends BasicGameState { // empty search if (search.getText().isEmpty()) - searchResultString = "Type to search!"; + searchResultString = null; // search produced new list: re-initialize it startNode = focusNode = null; @@ -450,39 +517,45 @@ public class SongMenu extends BasicGameState { searchResultString = "No matches found. Hit 'esc' to reset."; } } + if (searchTransitionTimer < SEARCH_TRANSITION_TIME) { + searchTransitionTimer += delta; + if (searchTransitionTimer > SEARCH_TRANSITION_TIME) + searchTransitionTimer = SEARCH_TRANSITION_TIME; + } // slide buttons int height = container.getHeight(); - float targetY = height * 0.16f; - if (buttonY > targetY) { + if (buttonY > headerY) { buttonY -= height * delta / 20000f; - if (buttonY < targetY) - buttonY = targetY; - } else if (buttonY < targetY) { + if (buttonY < headerY) + buttonY = headerY; + } else if (buttonY < headerY) { buttonY += height * delta / 20000f; - if (buttonY > targetY) - buttonY = targetY; + if (buttonY > headerY) + buttonY = headerY; } // mouse hover - OsuGroupNode node = startNode; boolean isHover = false; - for (int i = 0; i < MAX_SONG_BUTTONS && node != null; i++, node = node.next) { - float cx = (node.index == OsuGroupList.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; + if (mouseY > headerY && mouseY < footerY) { + OsuGroupNode node = startNode; + for (int i = 0; i < MAX_SONG_BUTTONS && node != null; i++, node = node.next) { + float cx = (node.index == OsuGroupList.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 { + hoverIndex = i; + hoverOffset = 0f; } - } else { - hoverIndex = i; - hoverOffset = 0f; + isHover = true; + break; } - isHover = true; - break; } } if (!isHover) { @@ -512,8 +585,17 @@ public class SongMenu extends BasicGameState { return; } - // options - if (optionsButton.contains(x, y)) { + // selection buttons + if (selectModsButton.contains(x, y)) { + this.keyPressed(Input.KEY_F1, '\0'); + return; + } else if (selectRandomButton.contains(x, y)) { + this.keyPressed(Input.KEY_F2, '\0'); + return; + } else if (selectMapOptionsButton.contains(x, y)) { + this.keyPressed(Input.KEY_F3, '\0'); + return; + } else if (selectOptionsButton.contains(x, y)) { SoundController.playSound(SoundEffect.MENUHIT); game.enterState(Opsu.STATE_OPTIONSMENU, new EmptyTransition(), new FadeInTransition(Color.black)); return; @@ -539,46 +621,48 @@ public class SongMenu extends BasicGameState { } // song buttons - int expandedIndex = OsuGroupList.get().getExpandedIndex(); - OsuGroupNode node = startNode; - for (int i = 0; i < MAX_SONG_BUTTONS && node != null; i++, node = node.next) { - // is button at this index clicked? - 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 oldHoverIndex = hoverIndex; - - // clicked node is already expanded - if (node.index == expandedIndex) { - if (node.osuFileIndex == focusNode.osuFileIndex) { - // if already focused, load the beatmap - if (button != Input.MOUSE_RIGHT_BUTTON) - startGame(); - else + if (y > headerY && y < footerY) { + int expandedIndex = OsuGroupList.get().getExpandedIndex(); + OsuGroupNode node = startNode; + for (int i = 0; i < MAX_SONG_BUTTONS && node != null; i++, node = node.next) { + // is button at this index clicked? + 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 oldHoverIndex = hoverIndex; + + // clicked node is already expanded + if (node.index == expandedIndex) { + if (node.osuFileIndex == focusNode.osuFileIndex) { + // if already focused, load the beatmap + if (button != Input.MOUSE_RIGHT_BUTTON) + startGame(); + else + SoundController.playSound(SoundEffect.MENUCLICK); + } else { + // focus the node SoundController.playSound(SoundEffect.MENUCLICK); - } else { - // focus the node - SoundController.playSound(SoundEffect.MENUCLICK); - setFocus(node, 0, false); + setFocus(node, 0, false); + } } + + // clicked node is a new group + else { + SoundController.playSound(SoundEffect.MENUCLICK); + setFocus(node, -1, false); + } + + // restore hover data + hoverOffset = oldHoverOffset; + hoverIndex = oldHoverIndex; + + // open beatmap menu + if (button == Input.MOUSE_RIGHT_BUTTON) + beatmapMenuTimer = (node.index == expandedIndex) ? BEATMAP_MENU_DELAY * 4 / 5 : 0; + + return; } - - // clicked node is a new group - else { - SoundController.playSound(SoundEffect.MENUCLICK); - setFocus(node, -1, false); - } - - // restore hover data - hoverOffset = oldHoverOffset; - hoverIndex = oldHoverIndex; - - // open beatmap menu - if (button == Input.MOUSE_RIGHT_BUTTON) - beatmapMenuTimer = (node.index == expandedIndex) ? BEATMAP_MENU_DELAY * 4 / 5 : 0; - - return; } } @@ -621,6 +705,7 @@ public class SongMenu extends BasicGameState { // clear search text search.setText(""); searchTimer = SEARCH_DELAY; + searchTransitionTimer = 0; } else { // return to main menu SoundController.playSound(SoundEffect.MENUBACK); @@ -629,12 +714,14 @@ public class SongMenu extends BasicGameState { } break; case Input.KEY_F1: - SoundController.playSound(SoundEffect.MENUHIT); - game.enterState(Opsu.STATE_OPTIONSMENU, new EmptyTransition(), new FadeInTransition(Color.black)); + // TODO: mods menu +// SoundController.playSound(SoundEffect.MENUHIT); +// game.enterState(); break; case Input.KEY_F2: if (focusNode == null) break; + SoundController.playSound(SoundEffect.MENUHIT); if (input.isKeyDown(Input.KEY_RSHIFT) || input.isKeyDown(Input.KEY_LSHIFT)) { // shift key: previous random track SongNode prev; @@ -718,8 +805,16 @@ public class SongMenu extends BasicGameState { break; default: // wait for user to finish typing - if (Character.isLetterOrDigit(c) || key == Input.KEY_BACK) + // TODO: accept all characters (current conditions are from TextField class) + if ((c > 31 && c < 127) || key == Input.KEY_BACK) { searchTimer = 0; + int textLength = search.getText().length(); + if (key == Input.KEY_BACK) { + if (textLength == 0) + searchTransitionTimer = 0; + } else if (textLength == 1) + searchTransitionTimer = 0; + } break; } } @@ -782,11 +877,15 @@ public class SongMenu extends BasicGameState { throws SlickException { Display.setTitle(game.getTitle()); Utils.getBackButton().resetHover(); - optionsButton.resetHover(); + selectModsButton.resetHover(); + selectRandomButton.resetHover(); + selectMapOptionsButton.resetHover(); + selectOptionsButton.resetHover(); hoverOffset = 0f; hoverIndex = -1; startScore = 0; beatmapMenuTimer = -1; + searchTransitionTimer = SEARCH_TRANSITION_TIME; // reset song stack randomStack = new Stack(); @@ -920,7 +1019,8 @@ public class SongMenu extends BasicGameState { hoverIndex = -1; search.setText(""); searchTimer = SEARCH_DELAY; - searchResultString = "Type to search!"; + searchTransitionTimer = SEARCH_TRANSITION_TIME; + searchResultString = null; // reload songs in new thread reloadThread = new Thread() { @@ -977,16 +1077,16 @@ public class SongMenu extends BasicGameState { if (n < 0 && startNode.prev != null) { startNode = startNode.prev; buttonY += buttonOffset / 4; - if (buttonY > height * 0.18f) - buttonY = height * 0.18f; + if (buttonY > headerY + height * 0.02f) + buttonY = headerY + height * 0.02f; n++; shifted = true; } else if (n > 0 && startNode.next != null && OsuGroupList.get().getNode(startNode, MAX_SONG_BUTTONS) != null) { startNode = startNode.next; buttonY -= buttonOffset / 4; - if (buttonY < height * 0.14f) - buttonY = height * 0.14f; + if (buttonY < headerY - height * 0.02f) + buttonY = headerY - height * 0.02f; n--; shifted = true; } else From 329d54c2308c4ed591454259d118e30e4bb9aadf Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Mon, 16 Feb 2015 01:24:22 -0500 Subject: [PATCH 2/3] Minor changes. - Replaced duplicate GameImage.process_sub() calls from related images with calling a single method, for easier editing. - Suppress warnings from overwritten Slick2D classes. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/GameImage.java | 75 ++++++++++--------- .../opsu/audio/MusicController.java | 11 ++- src/itdelatrisu/opsu/states/Game.java | 2 +- src/org/newdawn/slick/Music.java | 1 + src/org/newdawn/slick/openal/SoundStore.java | 4 +- 5 files changed, 53 insertions(+), 40 deletions(-) diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index cdb4e999..2dfe20da 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -41,25 +41,25 @@ public enum GameImage { CURSOR_MIDDLE ("cursormiddle", "png") { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy(1 + ((h - 600) / 1000f)); + return CURSOR.process_sub(img, w, h); } }, CURSOR_TRAIL ("cursortrail", "png") { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy(1 + ((h - 600) / 1000f)); + return CURSOR.process_sub(img, w, h); } }, CURSOR_OLD ("cursor2", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy(1 + ((h - 600) / 1000f)); + return CURSOR.process_sub(img, w, h); } }, CURSOR_TRAIL_OLD ("cursortrail2", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy(1 + ((h - 600) / 1000f)); + return CURSOR.process_sub(img, w, h); } }, @@ -82,25 +82,25 @@ public enum GameImage { COUNTDOWN_3 ("count3", "png") { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h / 3f) / img.getHeight()); + return COUNTDOWN_READY.process_sub(img, w, h); } }, COUNTDOWN_2 ("count2", "png") { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h / 3f) / img.getHeight()); + return COUNTDOWN_READY.process_sub(img, w, h); } }, COUNTDOWN_1 ("count1", "png") { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h / 3f) / img.getHeight()); + return COUNTDOWN_READY.process_sub(img, w, h); } }, COUNTDOWN_GO ("go", "png") { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h / 3f) / img.getHeight()); + return COUNTDOWN_READY.process_sub(img, w, h); } }, HITCIRCLE_SELECT ("hitcircleselect", "png"), @@ -202,49 +202,49 @@ public enum GameImage { RANKING_SSH ("ranking-XH", "png") { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h / 2f) / img.getHeight()); + return RANKING_SS.process_sub(img, w, h); } }, RANKING_SSH_SMALL ("ranking-XH-small", "png"), RANKING_S ("ranking-S", "png") { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h / 2f) / img.getHeight()); + return RANKING_SS.process_sub(img, w, h); } }, RANKING_S_SMALL ("ranking-S-small", "png"), RANKING_SH ("ranking-SH", "png") { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h / 2f) / img.getHeight()); + return RANKING_SS.process_sub(img, w, h); } }, RANKING_SH_SMALL ("ranking-SH-small", "png"), RANKING_A ("ranking-A", "png") { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h / 2f) / img.getHeight()); + return RANKING_SS.process_sub(img, w, h); } }, RANKING_A_SMALL ("ranking-A-small", "png"), RANKING_B ("ranking-B", "png") { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h / 2f) / img.getHeight()); + return RANKING_SS.process_sub(img, w, h); } }, RANKING_B_SMALL ("ranking-B-small", "png"), RANKING_C ("ranking-C", "png") { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h / 2f) / img.getHeight()); + return RANKING_SS.process_sub(img, w, h); } }, RANKING_C_SMALL ("ranking-C-small", "png"), RANKING_D ("ranking-D", "png") { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h / 2f) / img.getHeight()); + return RANKING_SS.process_sub(img, w, h); } }, RANKING_D_SMALL ("ranking-D-small", "png"), @@ -315,59 +315,59 @@ public enum GameImage { MOD_NO_FAIL ("selection-mod-nofail", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.12f) / img.getHeight()); + return MOD_EASY.process_sub(img, w, h); } }, MOD_HARD_ROCK ("selection-mod-hardrock", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.12f) / img.getHeight()); + return MOD_EASY.process_sub(img, w, h); } }, MOD_SUDDEN_DEATH ("selection-mod-suddendeath", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.12f) / img.getHeight()); + return MOD_EASY.process_sub(img, w, h); } }, MOD_SPUN_OUT ("selection-mod-spunout", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.12f) / img.getHeight()); + return MOD_EASY.process_sub(img, w, h); } }, MOD_AUTO ("selection-mod-autoplay", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.12f) / img.getHeight()); + return MOD_EASY.process_sub(img, w, h); } }, MOD_HALF_TIME ("selection-mod-halftime", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.12f) / img.getHeight()); + return MOD_EASY.process_sub(img, w, h); } }, MOD_DOUBLE_TIME ("selection-mod-doubletime", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.12f) / img.getHeight()); + return MOD_EASY.process_sub(img, w, h); } }, MOD_HIDDEN ("selection-mod-hidden", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.12f) / img.getHeight()); + return MOD_EASY.process_sub(img, w, h); } }, MOD_FLASHLIGHT ("selection-mod-flashlight", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.12f) / img.getHeight()); + return MOD_EASY.process_sub(img, w, h); } }, - // Non-Game Components + // Selection Buttons SELECTION_MODS ("selection-mods", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { @@ -377,45 +377,47 @@ public enum GameImage { SELECTION_MODS_OVERLAY ("selection-mods-over", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.115f) / img.getHeight()); + return SELECTION_MODS.process_sub(img, w, h); } }, SELECTION_RANDOM ("selection-random", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.115f) / img.getHeight()); + return SELECTION_MODS.process_sub(img, w, h); } }, SELECTION_RANDOM_OVERLAY ("selection-random-over", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.115f) / img.getHeight()); + return SELECTION_MODS.process_sub(img, w, h); } }, SELECTION_OPTIONS ("selection-options", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.115f) / img.getHeight()); + return SELECTION_MODS.process_sub(img, w, h); } }, SELECTION_OPTIONS_OVERLAY ("selection-options-over", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.115f) / img.getHeight()); + return SELECTION_MODS.process_sub(img, w, h); } }, SELECTION_OTHER_OPTIONS ("selection-selectoptions", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.115f) / img.getHeight()); + return SELECTION_MODS.process_sub(img, w, h); } }, SELECTION_OTHER_OPTIONS_OVERLAY ("selection-selectoptions-over", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.115f) / img.getHeight()); + return SELECTION_MODS.process_sub(img, w, h); } }, + + // Non-Game Components VOLUME ("volume-bg", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { @@ -483,6 +485,8 @@ public enum GameImage { MENU_BUTTON_MID ("button-middle", "png", false, false), MENU_BUTTON_LEFT ("button-left", "png", false, false), MENU_BUTTON_RIGHT ("button-right", "png", false, false), + + // Music Player Buttons MUSIC_PLAY ("music-play", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { @@ -492,21 +496,22 @@ public enum GameImage { MUSIC_PAUSE ("music-pause", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h / 18f) / img.getHeight()); + return MUSIC_PLAY.process_sub(img, w, h); } }, MUSIC_NEXT ("music-next", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h / 18f) / img.getHeight()); + return MUSIC_PLAY.process_sub(img, w, h); } }, MUSIC_PREVIOUS ("music-previous", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h / 18f) / img.getHeight()); + return MUSIC_PLAY.process_sub(img, w, h); } }, + RANKING_RETRY ("ranking-retry", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { diff --git a/src/itdelatrisu/opsu/audio/MusicController.java b/src/itdelatrisu/opsu/audio/MusicController.java index 0cfe8bb8..2a540017 100644 --- a/src/itdelatrisu/opsu/audio/MusicController.java +++ b/src/itdelatrisu/opsu/audio/MusicController.java @@ -248,10 +248,15 @@ public class MusicController { /** * Plays the current track. + * @param loop whether or not to loop the track */ - public static void play() { - if (trackExists()) - player.play(); + public static void play(boolean loop) { + if (trackExists()) { + if (loop) + player.loop(); + else + player.play(); + } } /** diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 1d5163b6..df4bbdd1 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -701,7 +701,7 @@ public class Game extends BasicGameState { resetGameData(); // needs to play before setting position to resume without lag later - MusicController.play(); + MusicController.play(false); MusicController.setPosition(0); MusicController.pause(); diff --git a/src/org/newdawn/slick/Music.java b/src/org/newdawn/slick/Music.java index 7bf70396..c3da9aeb 100644 --- a/src/org/newdawn/slick/Music.java +++ b/src/org/newdawn/slick/Music.java @@ -44,6 +44,7 @@ import org.newdawn.slick.util.Log; * @author kevin * @author Nathan Sweet */ +@SuppressWarnings({"rawtypes", "unchecked"}) public class Music { /** The music currently being played or null if none */ private static Music currentMusic; diff --git a/src/org/newdawn/slick/openal/SoundStore.java b/src/org/newdawn/slick/openal/SoundStore.java index 64fc2382..71c5319a 100644 --- a/src/org/newdawn/slick/openal/SoundStore.java +++ b/src/org/newdawn/slick/openal/SoundStore.java @@ -52,6 +52,7 @@ import org.newdawn.slick.util.ResourceLoader; * @author Kevin Glass * @author Rockstar setVolume cleanup */ +@SuppressWarnings({"rawtypes", "unchecked", "unused"}) public class SoundStore { /** The single instance of this class */ @@ -318,7 +319,8 @@ public class SoundStore { inited = true; AccessController.doPrivileged(new PrivilegedAction() { - public Object run() { + @Override + public Object run() { try { AL.create(); soundWorks = true; From 69f5aa5748ccb8ed021a3b372e97b360edeec3c9 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Mon, 16 Feb 2015 17:53:24 -0500 Subject: [PATCH 3/3] Added a separate Game Mods menu. - Replaces the mods being shown in the options menu. - Added all the (base) mod icons to the menu, with the unimplemented ones grayed out. - Added a rotation effect to MenuButton, and now multiple effects can be set at once. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/GameData.java | 9 +- src/itdelatrisu/opsu/GameImage.java | 14 +- src/itdelatrisu/opsu/GameMod.java | 247 +++++++++++++----- src/itdelatrisu/opsu/MenuButton.java | 209 ++++++++------- src/itdelatrisu/opsu/Utils.java | 31 ++- src/itdelatrisu/opsu/states/ButtonMenu.java | 133 +++++++++- .../opsu/states/DownloadsMenu.java | 1 + src/itdelatrisu/opsu/states/OptionsMenu.java | 48 +--- src/itdelatrisu/opsu/states/SongMenu.java | 11 +- 9 files changed, 471 insertions(+), 232 deletions(-) diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index 80617239..6c80ead5 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -1007,13 +1007,6 @@ public class GameData { if (hitValue > 0) { SoundController.playHitSound(hitSound); - // game mod score multipliers - float modMultiplier = 1f; - for (GameMod mod : GameMod.values()) { - if (mod.isActive()) - modMultiplier *= mod.getMultiplier(); - } - /** * [SCORE FORMULA] * Score = Hit Value + Hit Value * (Combo * Difficulty * Mod) / 25 @@ -1022,7 +1015,7 @@ public class GameData { * - Difficulty: the beatmap difficulty * - Mod: mod multipliers */ - score += (hitValue + (hitValue * (Math.max(combo - 1, 0) * difficulty * modMultiplier) / 25)); + score += (hitValue + (hitValue * (Math.max(combo - 1, 0) * difficulty * GameMod.getScoreMultiplier()) / 25)); incrementComboStreak(); } hitResultCount[result]++; diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index 2dfe20da..d3cf4d2b 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -309,7 +309,7 @@ public enum GameImage { MOD_EASY ("selection-mod-easy", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy((h * 0.12f) / img.getHeight()); + return img.getScaledCopy((h / 12f) / img.getHeight()); } }, MOD_NO_FAIL ("selection-mod-nofail", "png", false, false) { @@ -366,6 +366,18 @@ public enum GameImage { return MOD_EASY.process_sub(img, w, h); } }, + MOD_RELAX ("selection-mod-relax", "png", false, false) { + @Override + protected Image process_sub(Image img, int w, int h) { + return MOD_EASY.process_sub(img, w, h); + } + }, + MOD_AUTOPILOT ("selection-mod-relax2", "png", false, false) { + @Override + protected Image process_sub(Image img, int w, int h) { + return MOD_EASY.process_sub(img, w, h); + } + }, // Selection Buttons SELECTION_MODS ("selection-mods", "png", false, false) { diff --git a/src/itdelatrisu/opsu/GameMod.java b/src/itdelatrisu/opsu/GameMod.java index e9b34f9b..037ed70b 100644 --- a/src/itdelatrisu/opsu/GameMod.java +++ b/src/itdelatrisu/opsu/GameMod.java @@ -21,6 +21,7 @@ package itdelatrisu.opsu; import java.util.Arrays; import java.util.Collections; +import org.newdawn.slick.Color; import org.newdawn.slick.Image; import org.newdawn.slick.Input; @@ -28,29 +29,103 @@ import org.newdawn.slick.Input; * Game mods. */ public enum GameMod { - EASY (0, GameImage.MOD_EASY, "EZ", 2, Input.KEY_Q, 0.5f, + EASY (Category.EASY, 0, GameImage.MOD_EASY, "EZ", 2, Input.KEY_Q, 0.5f, "Reduces overall difficulty - larger circles, more forgiving HP drain, less accuracy required."), - NO_FAIL (1, GameImage.MOD_NO_FAIL, "NF", 1, Input.KEY_W, 0.5f, + NO_FAIL (Category.EASY, 1, GameImage.MOD_NO_FAIL, "NF", 1, Input.KEY_W, 0.5f, "You can't fail. No matter what."), - HARD_ROCK (2, GameImage.MOD_HARD_ROCK, "HR", 16, Input.KEY_A, 1.06f, + HALF_TIME (Category.EASY, 2, GameImage.MOD_HALF_TIME, "HT", 256, Input.KEY_E, 0.3f, false, + "Less zoom."), + HARD_ROCK (Category.HARD, 0, GameImage.MOD_HARD_ROCK, "HR", 16, Input.KEY_A, 1.06f, "Everything just got a bit harder..."), - SUDDEN_DEATH (3, GameImage.MOD_SUDDEN_DEATH, "SD", 32, Input.KEY_S, + SUDDEN_DEATH (Category.HARD, 1, GameImage.MOD_SUDDEN_DEATH, "SD", 32, Input.KEY_S, 1f, "Miss a note and fail."), - SPUN_OUT (4, GameImage.MOD_SPUN_OUT, "SO", 4096, Input.KEY_V, 0.9f, +// PERFECT (Category.HARD, 1, GameImage.MOD_PERFECT, "PF", 64, Input.KEY_S, 1f, +// "SS or quit."), + DOUBLE_TIME (Category.HARD, 2, GameImage.MOD_DOUBLE_TIME, "DT", 64, Input.KEY_D, 1.12f, false, + "Zoooooooooom."), +// NIGHTCORE (Category.HARD, 2, GameImage.MOD_NIGHTCORE, "NT", 64, Input.KEY_D, 1.12f, +// "uguuuuuuuu"), + HIDDEN (Category.HARD, 3, GameImage.MOD_HIDDEN, "HD", 8, Input.KEY_F, 1.06f, false, + "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, false, + "Restricted view area."), + RELAX (Category.SPECIAL, 0, GameImage.MOD_RELAX, "RL", 128, Input.KEY_Z, 0f, false, + "You don't need to click. Give your clicking/tapping finger a break from the heat of things. **UNRANKED**"), + AUTOPILOT (Category.SPECIAL, 1, GameImage.MOD_AUTOPILOT, "AP", 8192, Input.KEY_X, 0f, false, + "Automatic cursor movement - just follow the rhythm. **UNRANKED**"), + SPUN_OUT (Category.SPECIAL, 2, GameImage.MOD_SPUN_OUT, "SO", 4096, Input.KEY_V, 0.9f, "Spinners will be automatically completed."), - AUTO (5, GameImage.MOD_AUTO, "", 2048, Input.KEY_B, + AUTO (Category.SPECIAL, 3, GameImage.MOD_AUTO, "", 2048, Input.KEY_B, 1f, "Watch a perfect automated play through the song."); -// HALF_TIME (6, GameImage.MOD_HALF_TIME, "HT", 256, Input.KEY_E, 0.3f, -// "Less zoom."), -// DOUBLE_TIME (7, GameImage.MOD_DOUBLE_TIME, "DT", 64, Input.KEY_D, 1.12f, -// "Zoooooooooom."), -// HIDDEN (8, GameImage.MOD_HIDDEN, "HD", 8, Input.KEY_F, 1.06f, -// "Play with no approach circles and fading notes for a slight score advantage."), -// FLASHLIGHT (9, GameImage.MOD_FLASHLIGHT, "FL", 1024, Input.KEY_G, 1.12f, -// "Restricted view area."); - /** The ID of the mod (used for positioning). */ - private int id; + /** Mod categories. */ + public enum Category { + EASY (0, "Difficulty Reduction", Color.green), + HARD (1, "Difficulty Increase", Color.red), + SPECIAL (2, "Special", Color.white); + + /** Drawing index. */ + private int index; + + /** Category name. */ + private String name; + + /** Text color. */ + private Color color; + + /** The coordinates of the category. */ + private float x, y; + + /** + * Constructor. + * @param index the drawing index + * @param name the category name + * @param color the text color + */ + Category(int index, String name, Color color) { + this.index = index; + this.name = name; + this.color = color; + } + + /** + * Initializes the category. + * @param width the container width + * @param height the container height + */ + public void init(int width, int height) { + float multY = Utils.FONT_LARGE.getLineHeight() * 2 + height * 0.06f; + float offsetY = GameImage.MOD_EASY.getImage().getHeight() * 1.5f; + this.x = width / 30f; + this.y = multY + Utils.FONT_LARGE.getLineHeight() * 3f + offsetY * index; + } + + /** + * Returns the category name. + */ + public String getName() { return name; } + + /** + * Returns the text color. + */ + public Color getColor() { return color; } + + /** + * Returns the x coordinate of the category. + */ + public float getX() { return x; } + + /** + * Returns the y coordinate of the category. + */ + public float getY() { return y; } + } + + /** The category for the mod. */ + private Category category; + + /** The index in the category (for positioning). */ + private int categoryIndex; /** The file name of the mod image. */ private GameImage image; @@ -58,18 +133,21 @@ public enum GameMod { /** The abbreviation for the mod. */ private String abbrev; - /** Bit value associated with the mod. */ - private int bit; - /** - * The shortcut key associated with the mod. + * Bit value associated with the mod. * See the osu! API: https://github.com/peppy/osu-api/wiki#mods */ + private int bit; + + /** The shortcut key associated with the mod. */ private int key; /** The score multiplier. */ private float multiplier; + /** Whether or not the mod is implemented. */ + private boolean implemented; + /** The description of the mod. */ private String description; @@ -89,28 +167,56 @@ public enum GameMod { Collections.reverse(Arrays.asList(VALUES_REVERSED)); } + /** The last calculated score multiplier, or -1f if it must be recalculated. */ + private static float scoreMultiplier = -1f; + /** - * Constructor. - * @param id the ID of the mod (for positioning). - * @param image the GameImage - * @param abbrev the two-letter abbreviation - * @param bit the bit - * @param key the shortcut key - * @param description the description + * Initializes the game mods. + * @param width the container width + * @param height the container height */ - GameMod(int id, GameImage image, String abbrev, int bit, int key, String description) { - this.id = id; - this.image = image; - this.abbrev = abbrev; - this.bit = bit; - this.key = key; - this.multiplier = 1f; - this.description = description; + public static void init(int width, int height) { + // initialize categories + for (Category c : Category.values()) + c.init(width, height); + + // create buttons + float baseX = Category.EASY.getX() + Utils.FONT_LARGE.getWidth(Category.EASY.getName()) * 1.25f; + float offsetX = GameImage.MOD_EASY.getImage().getWidth() * 2.1f; + for (GameMod mod : GameMod.values()) { + Image img = mod.image.getImage(); + mod.button = new MenuButton(img, + baseX + (offsetX * mod.categoryIndex) + img.getWidth() / 2f, + mod.category.getY()); + mod.button.setHoverExpand(1.2f); + mod.button.setHoverRotate(10f); + + // reset state + mod.active = false; + } + + scoreMultiplier = -1f; + } + + /** + * Returns the current score multiplier from all active mods. + */ + public static float getScoreMultiplier() { + if (scoreMultiplier < 0f) { + float multiplier = 1f; + for (GameMod mod : GameMod.values()) { + if (mod.isActive()) + multiplier *= mod.getMultiplier(); + } + scoreMultiplier = multiplier; + } + return scoreMultiplier; } /** * Constructor. - * @param id the ID of the mod (for positioning). + * @param category the category for the mod + * @param categoryIndex the index in the category * @param image the GameImage * @param abbrev the two-letter abbreviation * @param bit the bit @@ -118,37 +224,36 @@ public enum GameMod { * @param multiplier the score multiplier * @param description the description */ - GameMod(int id, GameImage image, String abbrev, int bit, int key, float multiplier, String description) { - this.id = id; + GameMod(Category category, int categoryIndex, GameImage image, String abbrev, + int bit, int key, float multiplier, String description) { + this(category, categoryIndex, image, abbrev, bit, key, 1f, true, description); + } + + /** + * Constructor. + * @param category the category for the mod + * @param categoryIndex the index in the category + * @param image the GameImage + * @param abbrev the two-letter abbreviation + * @param bit the bit + * @param key the shortcut key + * @param multiplier the score multiplier + * @param implemented whether the mod is implemented + * @param description the description + */ + GameMod(Category category, int categoryIndex, GameImage image, String abbrev, + int bit, int key, float multiplier, boolean implemented, String description) { + this.category = category; + this.categoryIndex = categoryIndex; this.image = image; this.abbrev = abbrev; this.bit = bit; this.key = key; this.multiplier = multiplier; + this.implemented = implemented; this.description = description; } - /** - * Initializes the game mod. - * @param width the container width - * @param height the container height - */ - public void init(int width, int height) { - Image img = image.getImage(); - - // find coordinates - float offsetX = img.getWidth() * 1.5f; - float x = (width / 2f) - (offsetX * SIZE / 2.75f); - float y = (height * 0.8f) + (img.getHeight() / 2); - - // create button - this.button = new MenuButton(img, x + (offsetX * id), y); - this.button.setHoverExpand(1.15f); - - // reset state - this.active = false; - } - /** * Returns the abbreviated name of the mod. * @return the two-letter abbreviation @@ -185,7 +290,11 @@ public enum GameMod { * @param checkInverse if true, perform checks for mutual exclusivity */ public void toggle(boolean checkInverse) { + if (!implemented) + return; + active = !active; + scoreMultiplier = -1f; if (checkInverse) { if (AUTO.isActive()) { @@ -230,10 +339,12 @@ public enum GameMod { * Draws the game mod. */ public void draw() { - if (!active) - button.getImage().setAlpha(0.5f); - button.draw(); - button.getImage().setAlpha(1.0f); + if (!implemented) { + button.getImage().setAlpha(0.2f); + button.draw(); + button.getImage().setAlpha(1f); + } else + button.draw(); } /** @@ -244,6 +355,16 @@ public enum GameMod { */ public boolean contains(float x, float y) { return button.contains(x, y); } + /** + * Returns the center x coordinate of the button. + */ + public float getButtonX() { return button.getX(); } + + /** + * Returns the center y coordinate of the button. + */ + public float getButtonY() { return button.getY(); } + /** * Resets the hover fields for the button. */ diff --git a/src/itdelatrisu/opsu/MenuButton.java b/src/itdelatrisu/opsu/MenuButton.java index e7308601..cf4d35d9 100644 --- a/src/itdelatrisu/opsu/MenuButton.java +++ b/src/itdelatrisu/opsu/MenuButton.java @@ -24,9 +24,8 @@ import org.newdawn.slick.Font; import org.newdawn.slick.Image; /** - * A convenience class for menu buttons. - * Consists of an image or animation and coordinates. - * Multi-part images and animations currently do not support hover updates. + * A convenience class for menu buttons, consisting of an image or animation + * and coordinates. Multi-part images currently do not support effects. */ public class MenuButton { /** The image associated with the button. */ @@ -53,24 +52,30 @@ public class MenuButton { /** The color to draw the text with. */ private Color color; - /** Hover action types. */ - private enum HoverAction { NONE, EXPAND, FADE }; + /** Effect types. */ + private static final int + EFFECT_EXPAND = 1, + EFFECT_FADE = 2, + EFFECT_ROTATE = 4; - /** The hover action for this button. */ - private HoverAction hoverAction = HoverAction.NONE; + /** The hover actions for this button. */ + private int hoverEffect = 0; - /** The current and max scale of the button (for hovering). */ + /** The current and max scale of the button. */ private float scale = 1f, hoverScale = 1.25f; - /** The current and base alpha level of the button (for hovering). */ + /** The current and base alpha level of the button. */ private float alpha = 1f, baseAlpha = 0.75f; - /** The scaled expansion direction for the button (for hovering). */ + /** The scaled expansion direction for the button. */ private Expand dir = Expand.CENTER; - /** Scaled expansion directions (for hovering). */ + /** 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; + /** * Creates a new button from an Image. * @param img the image @@ -162,43 +167,7 @@ public class MenuButton { /** * Draws the button. */ - public void draw() { - if (img != null) { - if (imgL == null) { - if (hoverAction == HoverAction.EXPAND) { - Image imgScaled = (scale == 1f) ? img : img.getScaledCopy(scale); - imgScaled.setAlpha(img.getAlpha()); - imgScaled.draw(x - xRadius, y - yRadius); - } else if (hoverAction == HoverAction.FADE) { - float a = img.getAlpha(); - img.setAlpha(alpha); - img.draw(x - xRadius, y - yRadius); - img.setAlpha(a); - } else - img.draw(x - xRadius, y - yRadius); - } else { - if (hoverAction == HoverAction.FADE) { - float a = img.getAlpha(), aL = imgL.getAlpha(), aR = imgR.getAlpha(); - img.setAlpha(alpha); - imgL.setAlpha(alpha); - imgR.setAlpha(alpha); - img.draw(x - xRadius + imgL.getWidth(), y - yRadius); - imgL.draw(x - xRadius, y - yRadius); - imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius); - img.setAlpha(a); - imgL.setAlpha(aL); - imgR.setAlpha(aR); - } else { - img.draw(x - xRadius + imgL.getWidth(), y - yRadius); - imgL.draw(x - xRadius, y - yRadius); - imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius); - } - } - } else - anim.draw(x - xRadius, y - yRadius); - if (text != null) - font.drawString(x - font.getWidth(text) / 2f, y - font.getLineHeight() / 2f, text, color); - } + public void draw() { draw(null); } /** * Draw the button with a color filter. @@ -207,37 +176,48 @@ public class MenuButton { public void draw(Color filter) { if (img != null) { if (imgL == null) { - if (hoverAction == HoverAction.EXPAND) { - Image imgScaled = (scale == 1f) ? img : img.getScaledCopy(scale); - imgScaled.setAlpha(img.getAlpha()); - imgScaled.draw(x - xRadius, y - yRadius, filter); - } else if (hoverAction == HoverAction.FADE) { - float a = img.getAlpha(); - img.setAlpha(alpha); - img.draw(x - xRadius, y - yRadius, filter); - img.setAlpha(a); - } else - img.draw(x - xRadius, y - yRadius, filter); + if (hoverEffect == 0) + Utils.draw(img, x - xRadius, y - yRadius, filter); + else { + Image hoverImg = img; + float oldAlpha = img.getAlpha(); + float oldAngle = img.getRotation(); + if ((hoverEffect & EFFECT_EXPAND) > 0) { + if (scale != 1f) { + hoverImg = hoverImg.getScaledCopy(scale); + hoverImg.setAlpha(oldAlpha); + } + } + if ((hoverEffect & EFFECT_FADE) > 0) + hoverImg.setAlpha(alpha); + if ((hoverEffect & EFFECT_ROTATE) > 0) + hoverImg.setRotation(angle); + Utils.draw(hoverImg, x - xRadius, y - yRadius, filter); + if (hoverImg != img) { + hoverImg.setAlpha(oldAlpha); + hoverImg.setRotation(oldAngle); + } + } } else { - if (hoverAction == HoverAction.FADE) { + if (hoverEffect == 0) { + Utils.draw(img, x - xRadius + imgL.getWidth(), y - yRadius, filter); + Utils.draw(imgL, x - xRadius, y - yRadius, filter); + Utils.draw(imgR, x + xRadius - imgR.getWidth(), y - yRadius, filter); + } else if ((hoverEffect & EFFECT_FADE) > 0) { float a = img.getAlpha(), aL = imgL.getAlpha(), aR = imgR.getAlpha(); img.setAlpha(alpha); imgL.setAlpha(alpha); imgR.setAlpha(alpha); - img.draw(x - xRadius + imgL.getWidth(), y - yRadius, filter); - imgL.draw(x - xRadius, y - yRadius, filter); - imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter); + Utils.draw(img, x - xRadius + imgL.getWidth(), y - yRadius, filter); + Utils.draw(imgL, x - xRadius, y - yRadius, filter); + Utils.draw(imgR, x + xRadius - imgR.getWidth(), y - yRadius, filter); img.setAlpha(a); imgL.setAlpha(aL); imgR.setAlpha(aR); - } else { - img.draw(x - xRadius + imgL.getWidth(), y - yRadius, filter); - imgL.draw(x - xRadius, y - yRadius, filter); - imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter); } } } else - anim.draw(x - xRadius, y - yRadius, filter); + Utils.draw(anim, x - xRadius, y - yRadius, filter); if (text != null) font.drawString(x - font.getWidth(text) / 2f, y - font.getLineHeight() / 2f, text, color); } @@ -256,55 +236,77 @@ public class MenuButton { * Resets the hover fields for the button. */ public void resetHover() { - if (hoverAction == HoverAction.EXPAND) { + if ((hoverEffect & EFFECT_EXPAND) > 0) { this.scale = 1f; setHoverRadius(); - } else if (hoverAction == HoverAction.FADE) + } + if ((hoverEffect & EFFECT_FADE) > 0) this.alpha = baseAlpha; + if ((hoverEffect & EFFECT_ROTATE) > 0) + this.angle = 0f; } /** - * Sets the hover action to "expand". + * Removes all hover effects that have been set for the button. */ - public void setHoverExpand() { this.hoverAction = HoverAction.EXPAND; } + public void removeHoverEffects() { hoverEffect = 0; } /** - * Sets the hover action to "expand". + * Sets the "expand" hover effect. + */ + public void setHoverExpand() { hoverEffect |= EFFECT_EXPAND; } + + /** + * Sets the "expand" hover effect. * @param scale the maximum scale factor (default 1.25f) */ public void setHoverExpand(float scale) { setHoverExpand(scale, this.dir); } /** - * Sets the hover action to "expand". + * Sets the "expand" hover effect. * @param dir the expansion direction */ public void setHoverExpand(Expand dir) { setHoverExpand(this.hoverScale, dir); } /** - * Sets the hover action to "expand". + * Sets the "expand" hover effect. * @param scale the maximum scale factor (default 1.25f) * @param dir the expansion direction */ public void setHoverExpand(float scale, Expand dir) { - this.hoverAction = HoverAction.EXPAND; + hoverEffect |= EFFECT_EXPAND; this.hoverScale = scale; this.dir = dir; } /** - * Sets the hover action to "fade". + * Sets the "fade" hover effect. */ - public void setHoverFade() { this.hoverAction = HoverAction.FADE; } + public void setHoverFade() { hoverEffect |= EFFECT_FADE; } /** - * Sets the hover action to "fade". + * Sets the "fade" hover effect. * @param baseAlpha the base alpha level to fade in from (default 0.7f) */ public void setHoverFade(float baseAlpha) { - this.hoverAction = HoverAction.FADE; + hoverEffect |= EFFECT_FADE; this.baseAlpha = baseAlpha; } + /** + * Sets the "rotate" hover effect. + */ + public void setHoverRotate() { hoverEffect |= EFFECT_ROTATE; } + + /** + * Sets the "rotate" hover effect. + * @param maxAngle the maximum rotation angle, in degrees (default 30f) + */ + public void setHoverRotate(float maxAngle) { + hoverEffect |= EFFECT_ROTATE; + this.maxAngle = maxAngle; + } + /** * Processes a hover action depending on whether or not the cursor * is hovering over the button. @@ -313,31 +315,48 @@ public class MenuButton { * @param cy the y coordinate */ public void hoverUpdate(int delta, float cx, float cy) { - if (hoverAction == HoverAction.NONE) + if (hoverEffect == 0) return; - boolean isHover = contains(cx, cy); - if (hoverAction == HoverAction.EXPAND) { - // scale the button - int sign; + + // scale the button + if ((hoverEffect & EFFECT_EXPAND) > 0) { + int sign = 0; if (isHover && scale < hoverScale) sign = 1; else if (!isHover && scale > 1f) sign = -1; - else - return; - scale = Utils.getBoundedValue(scale, sign * (hoverScale - 1f) * delta / 100f, 1, hoverScale); - setHoverRadius(); - } else { - // fade the button - int sign; + if (sign != 0) { + scale = Utils.getBoundedValue(scale, sign * (hoverScale - 1f) * delta / 100f, 1, hoverScale); + 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; - else - return; - alpha = Utils.getBoundedValue(alpha, sign * (1f - baseAlpha) * delta / 200f, baseAlpha, 1f); + if (sign != 0) + alpha = Utils.getBoundedValue(alpha, sign * (1f - baseAlpha) * delta / 200f, baseAlpha, 1f); + } + + // 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); + } } } diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 2d126c37..9d3ce39c 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -213,8 +213,7 @@ public class Utils { } // initialize game mods - for (GameMod mod : GameMod.values()) - mod.init(width, height); + GameMod.init(width, height); // initialize hit objects OsuHitObject.init(width, height); @@ -281,6 +280,34 @@ public class Utils { anim.draw(x - (anim.getWidth() / 2f), y - (anim.getHeight() / 2f)); } + /** + * Draws an image at the given location. + * @param img the image to draw + * @param x the x coordinate + * @param y the y coordinate + * @param color the color filter to apply + */ + public static void draw(Image img, float x, float y, Color color) { + if (color == null) + img.draw(x, y); + else + img.draw(x, y, color); + } + + /** + * Draws an animation at the given location. + * @param anim the animation to draw + * @param x the x coordinate + * @param y the y coordinate + * @param color the color filter to apply + */ + public static void draw(Animation anim, float x, float y, Color color) { + if (color == null) + anim.draw(x, y); + else + anim.draw(x, y, color); + } + /** * Returns a bounded value for a base value and displacement. * @param base the initial value diff --git a/src/itdelatrisu/opsu/states/ButtonMenu.java b/src/itdelatrisu/opsu/states/ButtonMenu.java index f9efe3d4..76d33a53 100644 --- a/src/itdelatrisu/opsu/states/ButtonMenu.java +++ b/src/itdelatrisu/opsu/states/ButtonMenu.java @@ -19,6 +19,7 @@ package itdelatrisu.opsu.states; import itdelatrisu.opsu.GameImage; +import itdelatrisu.opsu.GameMod; import itdelatrisu.opsu.MenuButton; import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.OsuGroupList; @@ -121,6 +122,95 @@ public class ButtonMenu extends BasicGameState { public void leave(GameContainer container, StateBasedGame game) { Button.CLOSE.click(container, game); } + }, + MODS (new Button[] { Button.RESET_MODS, Button.CLOSE }) { + @Override + public String[] getTitle(GameContainer container, StateBasedGame game) { + return new String[] { + "Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play. Others are just for fun." + }; + } + + @Override + protected float getBaseY(GameContainer container, StateBasedGame game) { + return container.getHeight() * 2f / 3; + } + + @Override + public void enter(GameContainer container, StateBasedGame game) { + super.enter(container, game); + for (GameMod mod : GameMod.values()) + mod.resetHover(); + } + + @Override + public void leave(GameContainer container, StateBasedGame game) { + Button.CLOSE.click(container, game); + } + + @Override + public void draw(GameContainer container, StateBasedGame game, Graphics g) { + super.draw(container, game, g); + + int width = container.getWidth(); + int height = container.getHeight(); + + // score multiplier (TODO: fade in color changes) + float mult = GameMod.getScoreMultiplier(); + String multString = String.format("Score Multiplier: %.2fx", mult); + Color multColor = (mult == 1f) ? Color.white : (mult > 1f) ? Color.green : Color.red; + float multY = Utils.FONT_LARGE.getLineHeight() * 2 + height * 0.06f; + Utils.FONT_LARGE.drawString( + (width - Utils.FONT_LARGE.getWidth(multString)) / 2f, + multY, multString, multColor); + + // category text + for (GameMod.Category category : GameMod.Category.values()) { + Utils.FONT_LARGE.drawString(category.getX(), + category.getY() - Utils.FONT_LARGE.getLineHeight() / 2f, + category.getName(), category.getColor()); + } + + // buttons (TODO: draw descriptions when hovering) + for (GameMod mod : GameMod.values()) + mod.draw(); + } + + @Override + public void update(GameContainer container, int delta, int mouseX, int mouseY) { + super.update(container, delta, mouseX, mouseY); + for (GameMod mod : GameMod.values()) { + if (mod.isActive()) + mod.hoverUpdate(delta, mod.getButtonX(), mod.getButtonY()); + else + mod.hoverUpdate(delta, -1, -1); + } + } + + @Override + public void keyPress(GameContainer container, StateBasedGame game, int key, char c) { + super.keyPress(container, game, key, c); + for (GameMod mod : GameMod.values()) { + if (key == mod.getKey()) { + mod.toggle(true); + break; + } + } + } + + @Override + public void click(GameContainer container, StateBasedGame game, int cx, int cy) { + super.click(container, game, cx, cy); + for (GameMod mod : GameMod.values()) { + if (mod.contains(cx, cy)) { + boolean prevState = mod.isActive(); + mod.toggle(true); + if (mod.isActive() != prevState) + SoundController.playSound(SoundEffect.MENUCLICK); + return; + } + } + } }; /** The buttons in the state. */ @@ -153,23 +243,29 @@ public class ButtonMenu extends BasicGameState { */ public void init(GameContainer container, StateBasedGame game, Image button, Image buttonL, Image buttonR) { float center = container.getWidth() / 2f; - float centerOffset = container.getWidth() * OFFSET_WIDTH_RATIO; - float baseY = container.getHeight() * 0.2f; - baseY += ((getTitle(container, game).length - 1) * Utils.FONT_LARGE.getLineHeight()); + float baseY = getBaseY(container, game); float offsetY = button.getHeight() * 1.25f; menuButtons = new MenuButton[buttons.length]; for (int i = 0; i < buttons.length; i++) { - MenuButton b = new MenuButton(button, buttonL, buttonR, - center + ((i % 2 == 0) ? centerOffset * -1 : centerOffset), - baseY + (i * offsetY)); - b.setText(String.format("%d. %s", i + 1, buttons[i].getText()), - Utils.FONT_XLARGE, Color.white); + MenuButton b = new MenuButton(button, buttonL, buttonR, center, baseY + (i * offsetY)); + b.setText(String.format("%d. %s", i + 1, buttons[i].getText()), Utils.FONT_XLARGE, Color.white); b.setHoverFade(); menuButtons[i] = b; } } + /** + * Returns the base Y coordinate for the buttons. + * @param container the game container + * @param game the game + */ + protected float getBaseY(GameContainer container, StateBasedGame game) { + float baseY = container.getHeight() * 0.2f; + baseY += ((getTitle(container, game).length - 1) * Utils.FONT_LARGE.getLineHeight()); + return baseY; + } + /** * Draws the title and buttons to the graphics context. * @param container the game container @@ -231,13 +327,14 @@ public class ButtonMenu extends BasicGameState { } /** - * Processes a key press action (numeric digits only). + * Processes a key press action. * @param container the game container * @param game the game - * @param digit the digit pressed + * @param key the key code that was pressed (see {@link org.newdawn.slick.Input}) + * @param c the character of the key that was pressed */ - public void keyPress(GameContainer container, StateBasedGame game, int digit) { - int index = digit - 1; + public void keyPress(GameContainer container, StateBasedGame game, int key, char c) { + int index = Character.getNumericValue(c) - 1; if (index >= 0 && index < buttons.length) buttons[index].click(container, game); } @@ -384,6 +481,16 @@ public class ButtonMenu extends BasicGameState { public void click(GameContainer container, StateBasedGame game) { CANCEL.click(container, game); } + }, + RESET_MODS ("Reset All Mods", Color.red) { + @Override + public void click(GameContainer container, StateBasedGame game) { + SoundController.playSound(SoundEffect.MENUHIT); + for (GameMod mod : GameMod.values()) { + if (mod.isActive()) + mod.toggle(false); + } + } }; /** The text to show on the button. */ @@ -500,7 +607,7 @@ public class ButtonMenu extends BasicGameState { break; default: if (menuState != null) - menuState.keyPress(container, game, Character.getNumericValue(c)); + menuState.keyPress(container, game, key, c); break; } } diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java index 11848d09..ab578dc0 100644 --- a/src/itdelatrisu/opsu/states/DownloadsMenu.java +++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java @@ -229,6 +229,7 @@ public class DownloadsMenu extends BasicGameState { // search g.setColor(Color.white); + g.setLineWidth(2f); search.render(container, g); Utils.FONT_BOLD.drawString( search.getX() + search.getWidth() * 0.01f, search.getY() + search.getHeight() * 1.3f, diff --git a/src/itdelatrisu/opsu/states/OptionsMenu.java b/src/itdelatrisu/opsu/states/OptionsMenu.java index 11fae815..4e4afc5a 100644 --- a/src/itdelatrisu/opsu/states/OptionsMenu.java +++ b/src/itdelatrisu/opsu/states/OptionsMenu.java @@ -19,7 +19,6 @@ package itdelatrisu.opsu.states; import itdelatrisu.opsu.GameImage; -import itdelatrisu.opsu.GameMod; import itdelatrisu.opsu.MenuButton; import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.Options; @@ -171,10 +170,6 @@ public class OptionsMenu extends BasicGameState { int width = container.getWidth(); int height = container.getHeight(); - // game option coordinate modifiers - textY = 20 + (Utils.FONT_XLARGE.getLineHeight() * 3 / 2); - offsetY = (int) (((height * 0.8f) - textY) / maxOptionsScreen); - // option tabs Image tabImage = GameImage.MENU_TAB.getImage(); int subtextWidth = Utils.FONT_DEFAULT.getWidth("Click or drag an option to change it."); @@ -184,6 +179,10 @@ public class OptionsMenu extends BasicGameState { ((width - subtextWidth - tabImage.getWidth()) / 2) / OptionTab.SIZE); for (OptionTab tab : OptionTab.values()) tab.button = new MenuButton(tabImage, tabX + (tab.ordinal() * tabOffset), tabY); + + // game option coordinate modifiers + textY = (int) (tabY + tabImage.getHeight()); + offsetY = (height - textY - GameImage.MENU_BACK.getImage().getHeight()) / maxOptionsScreen; } @Override @@ -232,21 +231,6 @@ public class OptionsMenu extends BasicGameState { g.drawLine(0, lineY, width, lineY); g.resetLineWidth(); - // game mods - Utils.FONT_LARGE.drawString(width / 30, height * 0.8f, "Game Mods:", Color.white); - boolean descDrawn = false; - for (GameMod mod : GameMod.values()) { - mod.draw(); - if (!descDrawn && mod.contains(mouseX, mouseY)) { - Utils.FONT_DEFAULT.drawString( - (width - Utils.FONT_DEFAULT.getWidth(mod.getDescription())) / 2, - height * 0.975f - Utils.FONT_DEFAULT.getLineHeight(), - mod.getDescription(), Color.white - ); - descDrawn = true; - } - } - Utils.getBackButton().draw(); // key entry state @@ -272,8 +256,6 @@ public class OptionsMenu extends BasicGameState { Utils.updateVolumeDisplay(delta); int mouseX = input.getMouseX(), mouseY = input.getMouseY(); Utils.getBackButton().hoverUpdate(delta, mouseX, mouseY); - for (GameMod mod : GameMod.values()) - mod.hoverUpdate(delta, mouseX, mouseY); } @Override @@ -309,17 +291,6 @@ public class OptionsMenu extends BasicGameState { } } - // game mods - for (GameMod mod : GameMod.values()) { - if (mod.contains(x, y)) { - boolean prevState = mod.isActive(); - mod.toggle(true); - if (mod.isActive() != prevState) - SoundController.playSound(SoundEffect.MENUCLICK); - return; - } - } - // options (click only) GameOption option = getClickedOption(y); if (option != GameOption.NULL) @@ -400,15 +371,6 @@ public class OptionsMenu extends BasicGameState { currentTab = currentTab.next(); SoundController.playSound(SoundEffect.MENUCLICK); break; - default: - // check mod shortcut keys - for (GameMod mod : GameMod.values()) { - if (key == mod.getKey()) { - mod.toggle(true); - break; - } - } - break; } } @@ -417,8 +379,6 @@ public class OptionsMenu extends BasicGameState { throws SlickException { currentTab = OptionTab.DISPLAY; Utils.getBackButton().resetHover(); - for (GameMod mod : GameMod.values()) - mod.resetHover(); } /** diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index d616125c..84f0c1cd 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -359,9 +359,8 @@ public class SongMenu extends BasicGameState { } // selection buttons - // TODO -// GameImage.SELECTION_MODS.getImage().drawCentered(selectModsButton.getX(), selectModsButton.getY()); -// selectModsButton.draw(); + GameImage.SELECTION_MODS.getImage().drawCentered(selectModsButton.getX(), selectModsButton.getY()); + selectModsButton.draw(); GameImage.SELECTION_RANDOM.getImage().drawCentered(selectRandomButton.getX(), selectRandomButton.getY()); selectRandomButton.draw(); GameImage.SELECTION_OPTIONS.getImage().drawCentered(selectMapOptionsButton.getX(), selectMapOptionsButton.getY()); @@ -714,9 +713,9 @@ public class SongMenu extends BasicGameState { } break; case Input.KEY_F1: - // TODO: mods menu -// SoundController.playSound(SoundEffect.MENUHIT); -// game.enterState(); + SoundController.playSound(SoundEffect.MENUHIT); + ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.MODS); + game.enterState(Opsu.STATE_BUTTONMENU); break; case Input.KEY_F2: if (focusNode == null)