diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index b38d92e7..e1a7b01f 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -20,12 +20,12 @@ package itdelatrisu.opsu; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.downloads.DownloadList; +import itdelatrisu.opsu.states.ButtonMenu; import itdelatrisu.opsu.states.DownloadsMenu; import itdelatrisu.opsu.states.Game; import itdelatrisu.opsu.states.GamePauseMenu; import itdelatrisu.opsu.states.GameRanking; import itdelatrisu.opsu.states.MainMenu; -import itdelatrisu.opsu.states.MainMenuExit; import itdelatrisu.opsu.states.OptionsMenu; import itdelatrisu.opsu.states.SongMenu; import itdelatrisu.opsu.states.Splash; @@ -59,7 +59,7 @@ public class Opsu extends StateBasedGame { public static final int STATE_SPLASH = 0, STATE_MAINMENU = 1, - STATE_MAINMENUEXIT = 2, + STATE_BUTTONMENU = 2, STATE_SONGMENU = 3, STATE_GAME = 4, STATE_GAMEPAUSEMENU = 5, @@ -82,7 +82,7 @@ public class Opsu extends StateBasedGame { public void initStatesList(GameContainer container) throws SlickException { addState(new Splash(STATE_SPLASH)); addState(new MainMenu(STATE_MAINMENU)); - addState(new MainMenuExit(STATE_MAINMENUEXIT)); + addState(new ButtonMenu(STATE_BUTTONMENU)); addState(new SongMenu(STATE_SONGMENU)); addState(new Game(STATE_GAME)); addState(new GamePauseMenu(STATE_GAMEPAUSEMENU)); diff --git a/src/itdelatrisu/opsu/OsuGroupList.java b/src/itdelatrisu/opsu/OsuGroupList.java index d3375134..86614914 100644 --- a/src/itdelatrisu/opsu/OsuGroupList.java +++ b/src/itdelatrisu/opsu/OsuGroupList.java @@ -177,6 +177,7 @@ public class OsuGroupList { if (audioFile != null && audioFile.equals(osu.audioFilename)) { MusicController.reset(); System.gc(); // TODO: why can't files be deleted without calling this? + // TODO 2: this is broken as of 800014e } } diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 9984e6fb..1914c9c2 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -29,11 +29,13 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; +import java.util.List; import javax.imageio.ImageIO; @@ -184,7 +186,7 @@ public class Utils { Font font = javaFont.deriveFont(Font.PLAIN, (int) (fontBase * 4 / 3)); FONT_DEFAULT = new UnicodeFont(font); FONT_BOLD = new UnicodeFont(font.deriveFont(Font.BOLD)); - FONT_XLARGE = new UnicodeFont(font.deriveFont(fontBase * 4)); + FONT_XLARGE = new UnicodeFont(font.deriveFont(fontBase * 3)); FONT_LARGE = new UnicodeFont(font.deriveFont(fontBase * 2)); FONT_MEDIUM = new UnicodeFont(font.deriveFont(fontBase * 3 / 2)); FONT_SMALL = new UnicodeFont(font.deriveFont(fontBase)); @@ -797,4 +799,43 @@ public class Utils { // delete the directory dir.delete(); } + + /** + * Wraps the given string into a list of split lines based on the width. + * @param text the text to split + * @param font the font used to draw the string + * @param width the maximum width of a line + * @return the list of split strings + * @author davedes (http://slick.ninjacave.com/forum/viewtopic.php?t=3778) + */ + public static List wrap(String text, org.newdawn.slick.Font font, int width) { + List list = new ArrayList(); + String str = text; + String line = ""; + int i = 0; + int lastSpace = -1; + while (i < str.length()) { + char c = str.charAt(i); + if (Character.isWhitespace(c)) + lastSpace = i; + String append = line + c; + if (font.getWidth(append) > width) { + int split = (lastSpace != -1) ? lastSpace : i; + int splitTrimmed = split; + if (lastSpace != -1 && split < str.length() - 1) + splitTrimmed++; + list.add(str.substring(0, split)); + str = str.substring(splitTrimmed); + line = ""; + i = 0; + lastSpace = -1; + } else { + line = append; + i++; + } + } + if (str.length() != 0) + list.add(str); + return list; + } } \ No newline at end of file diff --git a/src/itdelatrisu/opsu/states/ButtonMenu.java b/src/itdelatrisu/opsu/states/ButtonMenu.java new file mode 100644 index 00000000..96b1561d --- /dev/null +++ b/src/itdelatrisu/opsu/states/ButtonMenu.java @@ -0,0 +1,495 @@ +/* + * 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.states; + +import itdelatrisu.opsu.GameImage; +import itdelatrisu.opsu.MenuButton; +import itdelatrisu.opsu.Opsu; +import itdelatrisu.opsu.OsuGroupList; +import itdelatrisu.opsu.OsuGroupNode; +import itdelatrisu.opsu.Utils; +import itdelatrisu.opsu.audio.SoundController; +import itdelatrisu.opsu.audio.SoundEffect; + +import java.util.List; + +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; +import org.newdawn.slick.state.StateBasedGame; +import org.newdawn.slick.state.transition.EmptyTransition; +import org.newdawn.slick.state.transition.FadeInTransition; + +/** + * Generic button menu state. + */ +public class ButtonMenu extends BasicGameState { + /** Menu states. */ + public enum MenuState { + EXIT (new Button[] { Button.YES, Button.NO }) { + @Override + public String[] getTitle(GameContainer container, StateBasedGame game) { + return new String[] { "Are you sure you want to exit opsu!?" }; + } + + @Override + public void leave(GameContainer container, StateBasedGame game) { + Button.NO.click(container, game); + } + }, + BEATMAP (new Button[] { Button.CLEAR_SCORES, Button.DELETE, Button.CANCEL }) { + @Override + public String[] getTitle(GameContainer container, StateBasedGame game) { + OsuGroupNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode(); + String osuString = (node != null) ? OsuGroupList.get().getBaseNode(node.index).toString() : ""; + return new String[] { osuString, "What do you want to do with this beatmap?" }; + } + + @Override + public void leave(GameContainer container, StateBasedGame game) { + Button.CANCEL.click(container, game); + } + }, + BEATMAP_DELETE_SELECT (new Button[] { Button.DELETE_GROUP, Button.DELETE_SONG, Button.CANCEL_DELETE }) { + @Override + public String[] getTitle(GameContainer container, StateBasedGame game) { + OsuGroupNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode(); + String osuString = (node != null) ? node.toString() : ""; + return new String[] { String.format("Are you sure you wish to delete '%s' from disk?", osuString) }; + } + + @Override + public void leave(GameContainer container, StateBasedGame game) { + Button.CANCEL_DELETE.click(container, game); + } + }, + BEATMAP_DELETE_CONFIRM (new Button[] { Button.DELETE_CONFIRM, Button.CANCEL_DELETE }) { + @Override + public String[] getTitle(GameContainer container, StateBasedGame game) { + return BEATMAP_DELETE_SELECT.getTitle(container, game); + } + + @Override + public void leave(GameContainer container, StateBasedGame game) { + Button.CANCEL_DELETE.click(container, game); + } + }, + RELOAD (new Button[] { Button.RELOAD_CONFIRM, Button.RELOAD_CANCEL }) { + @Override + public String[] getTitle(GameContainer container, StateBasedGame game) { + return new String[] { + "You have requested a full process of your beatmaps.", + "This could take a few minutes.", + "Are you sure you wish to continue?" + }; + } + + @Override + public void leave(GameContainer container, StateBasedGame game) { + Button.RELOAD_CANCEL.click(container, game); + } + }; + + /** The buttons in the state. */ + private Button[] buttons; + + /** The associated MenuButton objects. */ + private MenuButton[] menuButtons; + + /** Initial x coordinate offsets left/right of center (for shifting animation), times width. (TODO) */ + private static final float OFFSET_WIDTH_RATIO = 1 / 18f; + + /** + * Constructor. + * @param buttons the ordered list of buttons in the state + */ + MenuState(Button[] buttons) { + this.buttons = buttons; + } + + /** + * Initializes the menu state. + * @param container the game container + * @param game the game + * @param button the center button image + * @param buttonL the left button image + * @param buttonR the right button image + */ + 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 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); + b.setHoverFade(); + menuButtons[i] = b; + } + } + + /** + * Draws the title and buttons to the graphics context. + * @param container the game container + * @param game the game + * @param g the graphics context + */ + public void draw(GameContainer container, StateBasedGame game, Graphics g) { + // draw title + String[] title = getTitle(container, game); + float c = container.getWidth() * 0.02f; + int maxLineWidth = container.getWidth() - (int) (c * 2); + int lineHeight = Utils.FONT_LARGE.getLineHeight(); + for (int i = 0, j = 0; i < title.length; i++, j++) { + // wrap text if too long + if (Utils.FONT_LARGE.getWidth(title[i]) > maxLineWidth) { + List list = Utils.wrap(title[i], Utils.FONT_LARGE, maxLineWidth); + for (String str : list) + Utils.FONT_LARGE.drawString(c, c + (j++ * lineHeight), str, Color.white); + } else + Utils.FONT_LARGE.drawString(c, c + (j * lineHeight), title[i], Color.white); + } + + // draw buttons + for (int i = 0; i < buttons.length; i++) + menuButtons[i].draw(buttons[i].getColor()); + } + + /** + * Updates the menu state. + * @param container the game container + * @param delta the delta interval + * @param mouseX the mouse x coordinate + * @param mouseY the mouse y coordinate + */ + public void update(GameContainer container, int delta, int mouseX, int mouseY) { + float center = container.getWidth() / 2f; + 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)); + } + } + } + + /** + * Processes a mouse click action. + * @param container the game container + * @param game the game + * @param cx the x coordinate + * @param cy the y coordinate + */ + public void click(GameContainer container, StateBasedGame game, int cx, int cy) { + for (int i = 0; i < buttons.length; i++) { + if (menuButtons[i].contains(cx, cy)) { + buttons[i].click(container, game); + break; + } + } + } + + /** + * Processes a key press action (numeric digits only). + * @param container the game container + * @param game the game + * @param digit the digit pressed + */ + public void keyPress(GameContainer container, StateBasedGame game, int digit) { + int index = digit - 1; + if (index >= 0 && index < buttons.length) + buttons[index].click(container, game); + } + + /** + * Retrieves the title strings for the menu state (via override). + * @param container the game container + * @param game the game + */ + public String[] getTitle(GameContainer container, StateBasedGame game) { return new String[0]; } + + /** + * Processes a state enter request. + * @param container the game container + * @param game the game + */ + public void enter(GameContainer container, StateBasedGame game) { + float center = container.getWidth() / 2f; + float centerOffset = container.getWidth() * OFFSET_WIDTH_RATIO; + for (int i = 0; i < buttons.length; i++) { + menuButtons[i].setX(center + ((i % 2 == 0) ? centerOffset * -1 : centerOffset)); + menuButtons[i].resetHover(); + } + } + + /** + * Processes a state exit request (via override). + * @param container the game container + * @param game the game + */ + public void leave(GameContainer container, StateBasedGame game) {} + }; + + /** Button types. */ + private enum Button { + YES ("Yes", Color.green) { + @Override + public void click(GameContainer container, StateBasedGame game) { + container.exit(); + } + }, + NO ("No", Color.red) { + @Override + public void click(GameContainer container, StateBasedGame game) { + SoundController.playSound(SoundEffect.MENUBACK); + game.enterState(Opsu.STATE_MAINMENU, new EmptyTransition(), new FadeInTransition(Color.black)); + } + }, + CLEAR_SCORES ("Clear local scores", Color.magenta) { + @Override + public void click(GameContainer container, StateBasedGame game) { + SoundController.playSound(SoundEffect.MENUHIT); + OsuGroupNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode(); + ((SongMenu) game.getState(Opsu.STATE_SONGMENU)).doStateActionOnLoad(MenuState.BEATMAP, node); + game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black)); + } + }, + DELETE ("Delete...", Color.red) { + @Override + public void click(GameContainer container, StateBasedGame game) { + SoundController.playSound(SoundEffect.MENUHIT); + OsuGroupNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode(); + MenuState ms = (node.osuFileIndex == -1 || node.osuFiles.size() == 1) ? + MenuState.BEATMAP_DELETE_CONFIRM : MenuState.BEATMAP_DELETE_SELECT; + ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(ms, node); + game.enterState(Opsu.STATE_BUTTONMENU); + } + }, + CANCEL ("Cancel", Color.gray) { + @Override + public void click(GameContainer container, StateBasedGame game) { + SoundController.playSound(SoundEffect.MENUBACK); + game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black)); + } + }, + DELETE_CONFIRM ("Yes, delete this beatmap!", Color.red) { + @Override + public void click(GameContainer container, StateBasedGame game) { + SoundController.playSound(SoundEffect.MENUHIT); + OsuGroupNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode(); + ((SongMenu) game.getState(Opsu.STATE_SONGMENU)).doStateActionOnLoad(MenuState.BEATMAP_DELETE_CONFIRM, node); + game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black)); + } + }, + DELETE_GROUP ("Yes, delete all difficulties!", Color.red) { + @Override + public void click(GameContainer container, StateBasedGame game) { + DELETE_CONFIRM.click(container, game); + } + }, + DELETE_SONG ("Yes, but only this difficulty", Color.red) { + @Override + public void click(GameContainer container, StateBasedGame game) { + SoundController.playSound(SoundEffect.MENUHIT); + OsuGroupNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode(); + ((SongMenu) game.getState(Opsu.STATE_SONGMENU)).doStateActionOnLoad(MenuState.BEATMAP_DELETE_SELECT, node); + game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black)); + } + }, + CANCEL_DELETE ("Nooooo! I didn't mean to!", Color.gray) { + @Override + public void click(GameContainer container, StateBasedGame game) { + CANCEL.click(container, game); + } + }, + RELOAD_CONFIRM ("Let's do it!", Color.green) { + @Override + public void click(GameContainer container, StateBasedGame game) { + SoundController.playSound(SoundEffect.MENUHIT); + ((SongMenu) game.getState(Opsu.STATE_SONGMENU)).doStateActionOnLoad(MenuState.RELOAD); + game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black)); + } + }, + RELOAD_CANCEL ("Cancel", Color.red) { + @Override + public void click(GameContainer container, StateBasedGame game) { + CANCEL.click(container, game); + } + }; + + /** The text to show on the button. */ + private String text; + + /** The button color. */ + private Color color; + + /** + * Constructor. + * @param text the text to show on the button + * @param color the button color + */ + Button(String text, Color color) { + this.text = text; + this.color = color; + } + + /** + * Returns the button text. + */ + public String getText() { return text; } + + /** + * Returns the button color. + */ + public Color getColor() { return color; } + + /** + * Processes a mouse click action (via override). + * @param container the game container + * @param game the game + */ + public void click(GameContainer container, StateBasedGame game) {} + } + + /** The current menu state. */ + private MenuState menuState; + + /** The song node to process in the state. */ + private OsuGroupNode node; + + // game-related variables + private GameContainer container; + private StateBasedGame game; + private Input input; + private int state; + + public ButtonMenu(int state) { + this.state = state; + } + + @Override + public void init(GameContainer container, StateBasedGame game) + throws SlickException { + this.container = container; + this.game = game; + this.input = container.getInput(); + + // initialize buttons + Image button = GameImage.MENU_BUTTON_MID.getImage(); + button = button.getScaledCopy(container.getWidth() / 2, button.getHeight()); + Image buttonL = GameImage.MENU_BUTTON_LEFT.getImage(); + Image buttonR = GameImage.MENU_BUTTON_RIGHT.getImage(); + for (MenuState ms : MenuState.values()) + ms.init(container, game, button, buttonL, buttonR); + } + + @Override + public void render(GameContainer container, StateBasedGame game, Graphics g) + throws SlickException { + g.setBackground(Color.black); + if (menuState != null) + menuState.draw(container, game, g); + Utils.drawVolume(g); + Utils.drawFPS(); + Utils.drawCursor(); + } + + @Override + public void update(GameContainer container, StateBasedGame game, int delta) + throws SlickException { + Utils.updateCursor(delta); + Utils.updateVolumeDisplay(delta); + if (menuState != null) + menuState.update(container, delta, input.getMouseX(), input.getMouseY()); + } + + @Override + public int getID() { return state; } + + @Override + public void mousePressed(int button, int x, int y) { + // check mouse button + if (button != Input.MOUSE_LEFT_BUTTON) + return; + + if (menuState != null) + menuState.click(container, game, x, y); + } + + @Override + public void keyPressed(int key, char c) { + switch (key) { + case Input.KEY_ESCAPE: + if (menuState != null) + menuState.leave(container, game); + break; + case Input.KEY_F12: + Utils.takeScreenShot(); + break; + default: + if (menuState != null) + menuState.keyPress(container, game, Character.getNumericValue(c)); + break; + } + } + + @Override + public void enter(GameContainer container, StateBasedGame game) + throws SlickException { + if (menuState != null) + menuState.enter(container, game); + } + + /** + * Changes the menu state. + * @param menuState the new menu state + */ + public void setMenuState(MenuState menuState) { setMenuState(menuState, null); } + + /** + * Changes the menu state. + * @param menuState the new menu state + * @param node the song node to process in the state + */ + public void setMenuState(MenuState menuState, OsuGroupNode node) { + this.menuState = menuState; + this.node = node; + } + + /** + * Returns the song node being processed, or null if none. + */ + public OsuGroupNode getNode() { return node; } +} diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index 09efc648..2e35621f 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -31,6 +31,7 @@ import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundEffect; +import itdelatrisu.opsu.states.ButtonMenu.MenuState; import java.awt.Desktop; import java.io.IOException; @@ -417,7 +418,8 @@ public class MainMenu extends BasicGameState { switch (key) { case Input.KEY_ESCAPE: case Input.KEY_Q: - game.enterState(Opsu.STATE_MAINMENUEXIT); + ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.EXIT); + game.enterState(Opsu.STATE_BUTTONMENU); break; case Input.KEY_P: if (!logoClicked) { diff --git a/src/itdelatrisu/opsu/states/MainMenuExit.java b/src/itdelatrisu/opsu/states/MainMenuExit.java deleted file mode 100644 index 05d62fde..00000000 --- a/src/itdelatrisu/opsu/states/MainMenuExit.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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.states; - -import itdelatrisu.opsu.GameImage; -import itdelatrisu.opsu.MenuButton; -import itdelatrisu.opsu.Opsu; -import itdelatrisu.opsu.Utils; - -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; -import org.newdawn.slick.state.StateBasedGame; -import org.newdawn.slick.state.transition.EmptyTransition; -import org.newdawn.slick.state.transition.FadeInTransition; - -/** - * "Confirm Exit" state. - *
    - *
  • [Yes] - quit game - *
  • [No] - return to main menu - *
- */ -public class MainMenuExit extends BasicGameState { - /** "Yes" and "No" buttons. */ - private MenuButton yesButton, noButton; - - /** Initial x coordinate offsets left/right of center (for shifting animation). */ - private float centerOffset; - - // game-related variables - private GameContainer container; - private StateBasedGame game; - private Input input; - private int state; - - public MainMenuExit(int state) { - this.state = state; - } - - @Override - public void init(GameContainer container, StateBasedGame game) - throws SlickException { - this.container = container; - this.game = game; - this.input = container.getInput(); - - int width = container.getWidth(); - int height = container.getHeight(); - - centerOffset = width / 18f; - - // initialize buttons - Image button = GameImage.MENU_BUTTON_MID.getImage(); - button = button.getScaledCopy(width / 2, button.getHeight()); - Image buttonL = GameImage.MENU_BUTTON_LEFT.getImage(); - Image buttonR = GameImage.MENU_BUTTON_RIGHT.getImage(); - yesButton = new MenuButton(button, buttonL, buttonR, - width / 2f + centerOffset, height * 0.2f - ); - yesButton.setText("1. Yes", Utils.FONT_XLARGE, Color.white); - noButton = new MenuButton(button, buttonL, buttonR, - width / 2f - centerOffset, height * 0.2f + (button.getHeight() * 1.25f) - ); - noButton.setText("2. No", Utils.FONT_XLARGE, Color.white); - yesButton.setHoverFade(); - noButton.setHoverFade(); - } - - @Override - public void render(GameContainer container, StateBasedGame game, Graphics g) - throws SlickException { - g.setBackground(Color.black); - - // draw text - float c = container.getWidth() * 0.02f; - Utils.FONT_LARGE.drawString(c, c, "Are you sure you want to exit opsu!?", Color.white); - - // draw buttons - yesButton.draw(Color.green); - noButton.draw(Color.red); - - Utils.drawVolume(g); - Utils.drawFPS(); - Utils.drawCursor(); - } - - @Override - public void update(GameContainer container, StateBasedGame game, int delta) - throws SlickException { - Utils.updateCursor(delta); - Utils.updateVolumeDisplay(delta); - int mouseX = input.getMouseX(), mouseY = input.getMouseY(); - yesButton.hoverUpdate(delta, mouseX, mouseY); - noButton.hoverUpdate(delta, mouseX, mouseY); - - // move buttons to center - float yesX = yesButton.getX(), noX = noButton.getX(); - float center = container.getWidth() / 2f; - if (yesX < center) - yesButton.setX(Math.min(yesX + (delta / 5f), center)); - if (noX > center) - noButton.setX(Math.max(noX - (delta / 5f), center)); - } - - @Override - public int getID() { return state; } - - @Override - public void mousePressed(int button, int x, int y) { - // check mouse button - if (button != Input.MOUSE_LEFT_BUTTON) - return; - - if (yesButton.contains(x, y)) - container.exit(); - else if (noButton.contains(x, y)) - game.enterState(Opsu.STATE_MAINMENU, new EmptyTransition(), new FadeInTransition(Color.black)); - } - - @Override - public void keyPressed(int key, char c) { - switch (key) { - case Input.KEY_1: - container.exit(); - break; - case Input.KEY_2: - case Input.KEY_ESCAPE: - game.enterState(Opsu.STATE_MAINMENU, new EmptyTransition(), new FadeInTransition(Color.black)); - break; - case Input.KEY_F12: - Utils.takeScreenShot(); - break; - } - } - - @Override - public void enter(GameContainer container, StateBasedGame game) - throws SlickException { - float center = container.getWidth() / 2f; - yesButton.setX(center - centerOffset); - noButton.setX(center + centerOffset); - yesButton.resetHover(); - noButton.resetHover(); - } -} diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index 5a1ee591..85512ed7 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -38,6 +38,7 @@ import itdelatrisu.opsu.audio.HitSound; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundEffect; +import itdelatrisu.opsu.states.ButtonMenu.MenuState; import java.io.File; import java.util.Map; @@ -76,6 +77,9 @@ public class SongMenu extends BasicGameState { /** Delay time, in milliseconds, between each search. */ private static final int SEARCH_DELAY = 500; + /** Delay time, in milliseconds, before moving to the beatmap menu after a right click. */ + private static final int BEATMAP_MENU_DELAY = 600; + /** Maximum x offset of song buttons for mouse hover, in pixels. */ private static final float MAX_HOVER_OFFSET = 30f; @@ -158,6 +162,15 @@ public class SongMenu extends BasicGameState { /** Whether or not to reset music track upon entering the state. */ private boolean resetTrack = false; + /** If non-null, determines the action to perform upon entering the state. */ + private MenuState stateAction; + + /** If non-null, the node that stateAction acts upon. */ + private OsuGroupNode stateActionNode; + + /** Timer before moving to the beatmap menu with the current focus node. */ + private int beatmapMenuTimer = -1; + /** Beatmap reloading thread. */ private Thread reloadThread; @@ -376,10 +389,23 @@ public class SongMenu extends BasicGameState { Utils.getBackButton().hoverUpdate(delta, mouseX, mouseY); optionsButton.hoverUpdate(delta, mouseX, mouseY); + // beatmap menu timer + if (beatmapMenuTimer > -1) { + beatmapMenuTimer += delta; + if (beatmapMenuTimer >= BEATMAP_MENU_DELAY) { + beatmapMenuTimer = -1; + if (focusNode != null) { + ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.BEATMAP, focusNode); + game.enterState(Opsu.STATE_BUTTONMENU); + } + return; + } + } + // search search.setFocus(true); searchTimer += delta; - if (searchTimer >= SEARCH_DELAY && reloadThread == null) { + if (searchTimer >= SEARCH_DELAY && reloadThread == null && beatmapMenuTimer == -1) { searchTimer = 0; // store the start/focus nodes @@ -464,11 +490,11 @@ public class SongMenu extends BasicGameState { @Override public void mousePressed(int button, int x, int y) { // check mouse button - if (button != Input.MOUSE_LEFT_BUTTON) + if (button == Input.MOUSE_MIDDLE_BUTTON) return; - // block input during beatmap reloading - if (reloadThread != null) + // block input + if (reloadThread != null || beatmapMenuTimer > -1) return; // back @@ -520,8 +546,10 @@ public class SongMenu extends BasicGameState { if (node.index == expandedIndex) { if (node.osuFileIndex == focusNode.osuFileIndex) { // if already focused, load the beatmap - startGame(); - + if (button != Input.MOUSE_RIGHT_BUTTON) + startGame(); + else + SoundController.playSound(SoundEffect.MENUCLICK); } else { // focus the node SoundController.playSound(SoundEffect.MENUCLICK); @@ -539,6 +567,10 @@ public class SongMenu extends BasicGameState { hoverOffset = oldHoverOffset; hoverIndex = oldHoverIndex; + // open beatmap menu + if (button == Input.MOUSE_RIGHT_BUTTON) + beatmapMenuTimer = (node.index == expandedIndex) ? BEATMAP_MENU_DELAY * 4 / 5 : 0; + return; } } @@ -562,8 +594,8 @@ public class SongMenu extends BasicGameState { @Override public void keyPressed(int key, char c) { - // block input during beatmap reloading - if (reloadThread != null && !(key == Input.KEY_ESCAPE || key == Input.KEY_F12)) + // block input + if ((reloadThread != null && !(key == Input.KEY_ESCAPE || key == Input.KEY_F12)) || beatmapMenuTimer > -1) return; switch (key) { @@ -601,44 +633,17 @@ public class SongMenu extends BasicGameState { setFocus(OsuGroupList.get().getRandomNode(), -1, true); } break; + case Input.KEY_F3: + if (focusNode == null) + break; + SoundController.playSound(SoundEffect.MENUHIT); + ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.BEATMAP, focusNode); + game.enterState(Opsu.STATE_BUTTONMENU); + break; case Input.KEY_F5: - // TODO: osu! has a confirmation menu - SoundController.playSound(SoundEffect.MENUCLICK); - - // reset state and node references - MusicController.reset(); - startNode = focusNode = null; - scoreMap = null; - focusScores = null; - oldFocusNode = null; - randomStack = new Stack(); - songInfo = null; - hoverOffset = 0f; - hoverIndex = -1; - search.setText(""); - searchTimer = SEARCH_DELAY; - searchResultString = "Type to search!"; - - // reload songs in new thread - reloadThread = new Thread() { - @Override - public void run() { - // invoke unpacker and parser - File beatmapDir = Options.getBeatmapDir(); - OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir); - OsuParser.parseAllFiles(beatmapDir); - - // initialize song list - if (OsuGroupList.get().size() > 0) { - OsuGroupList.get().init(); - setFocus(OsuGroupList.get().getRandomNode(), -1, true); - } else - MusicController.playThemeSong(); - - reloadThread = null; - } - }; - reloadThread.start(); + SoundController.playSound(SoundEffect.MENUHIT); + ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.RELOAD); + game.enterState(Opsu.STATE_BUTTONMENU); break; case Input.KEY_F12: Utils.takeScreenShot(); @@ -707,8 +712,8 @@ public class SongMenu extends BasicGameState { @Override public void mouseDragged(int oldx, int oldy, int newx, int newy) { - // block input during beatmap reloading - if (reloadThread != null) + // block input + if (reloadThread != null || beatmapMenuTimer > -1) return; int diff = newy - oldy; @@ -739,8 +744,8 @@ public class SongMenu extends BasicGameState { @Override public void mouseWheelMoved(int newValue) { - // block input during beatmap reloading - if (reloadThread != null) + // block input + if (reloadThread != null || beatmapMenuTimer > -1) return; int shift = (newValue < 0) ? 1 : -1; @@ -767,6 +772,10 @@ public class SongMenu extends BasicGameState { hoverOffset = 0f; hoverIndex = -1; startScore = 0; + beatmapMenuTimer = -1; + + // reset song stack + randomStack = new Stack(); // set focus node if not set (e.g. theme song playing) if (focusNode == null && OsuGroupList.get().size() > 0) @@ -787,9 +796,6 @@ public class SongMenu extends BasicGameState { if (MusicController.isTrackDimmed()) MusicController.toggleTrackDimmed(1f); - // reset song stack - randomStack = new Stack(); - // reset game data if (resetGame) { ((Game) game.getState(Opsu.STATE_GAME)).resetGameData(); @@ -808,6 +814,119 @@ public class SongMenu extends BasicGameState { resetGame = false; } + + // state-based action + if (stateAction != null) { + switch (stateAction) { + case BEATMAP: // clear scores + if (stateActionNode == null || stateActionNode.osuFileIndex == -1) + break; + OsuFile osu = stateActionNode.osuFiles.get(stateActionNode.osuFileIndex); + ScoreDB.deleteScore(osu); + if (stateActionNode == focusNode) { + focusScores = null; + scoreMap.remove(osu.version); + } + break; + case BEATMAP_DELETE_CONFIRM: // delete song group + if (stateActionNode == null) + break; + OsuGroupNode + prev = OsuGroupList.get().getBaseNode(stateActionNode.index - 1), + next = OsuGroupList.get().getBaseNode(stateActionNode.index + 1); + int oldIndex = stateActionNode.index, focusNodeIndex = focusNode.index, startNodeIndex = startNode.index; + OsuGroupList.get().deleteSongGroup(stateActionNode); + if (oldIndex == focusNodeIndex) { + if (prev != null) + setFocus(prev, -1, true); + else if (next != null) + setFocus(next, -1, true); + else { + startNode = focusNode = null; + oldFocusNode = null; + randomStack = new Stack(); + songInfo = null; + scoreMap = null; + focusScores = null; + } + } else if (oldIndex == startNodeIndex) { + if (startNode.prev != null) + startNode = startNode.prev; + else if (startNode.next != null) + startNode = startNode.next; + else { + startNode = null; + songInfo = null; + } + } + break; + case BEATMAP_DELETE_SELECT: // delete single song + if (stateActionNode == null) + break; + int index = stateActionNode.index; + OsuGroupList.get().deleteSong(stateActionNode); + if (stateActionNode == focusNode) { + if (stateActionNode.prev != null && + !(stateActionNode.next != null && stateActionNode.next.index == index)) { + if (stateActionNode.prev.index == index) + setFocus(stateActionNode.prev, 0, true); + else + setFocus(stateActionNode.prev, -1, true); + } else if (stateActionNode.next != null) { + if (stateActionNode.next.index == index) + setFocus(stateActionNode.next, 0, true); + else + setFocus(stateActionNode.next, -1, true); + } + } else if (stateActionNode == startNode) { + if (startNode.prev != null) + startNode = startNode.prev; + else if (startNode.next != null) + startNode = startNode.next; + } + break; + case RELOAD: // reload beatmaps + // reset state and node references + MusicController.reset(); + startNode = focusNode = null; + scoreMap = null; + focusScores = null; + oldFocusNode = null; + randomStack = new Stack(); + songInfo = null; + hoverOffset = 0f; + hoverIndex = -1; + search.setText(""); + searchTimer = SEARCH_DELAY; + searchResultString = "Type to search!"; + + // reload songs in new thread + reloadThread = new Thread() { + @Override + public void run() { + // invoke unpacker and parser + File beatmapDir = Options.getBeatmapDir(); + OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir); + OsuParser.parseAllFiles(beatmapDir); + + // initialize song list + if (OsuGroupList.get().size() > 0) { + OsuGroupList.get().init(); + setFocus(OsuGroupList.get().getRandomNode(), -1, true); + } else + MusicController.playThemeSong(); + + reloadThread = null; + } + }; + reloadThread.start(); + break; + default: + break; + } + stateAction = null; + stateActionNode = null; + } } @Override @@ -938,6 +1057,22 @@ public class SongMenu extends BasicGameState { */ public void resetTrackOnLoad() { resetTrack = true; } + /** + * Performs an action based on a menu state upon entering this state. + * @param menuState the menu state determining the action + */ + public void doStateActionOnLoad(MenuState menuState) { doStateActionOnLoad(menuState, null); } + + /** + * Performs an action based on a menu state upon entering this state. + * @param menuState the menu state determining the action + * @param node the song node to perform the action on + */ + public void doStateActionOnLoad(MenuState menuState, OsuGroupNode node) { + stateAction = menuState; + stateActionNode = node; + } + /** * Returns all the score data for an OsuGroupNode from scoreMap. * If no score data is available for the node, return null.