opsu-dance/src/itdelatrisu/opsu/states/ButtonMenu.java
Jeffrey Han c15cd5cb4c Follow-up to a6ece30: allow changing volume anywhere with ALT key.
The master volume can now be changed (using the mouse wheel) with the ALT key pressed in any menu, as in osu!.

Also improved the handling of dimmed tracks.

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
2015-03-26 20:45:33 -04:00

725 lines
23 KiB
Java

/*
* 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 <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OsuGroupList;
import itdelatrisu.opsu.OsuGroupNode;
import itdelatrisu.opsu.ScoreData;
import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import java.util.ArrayList;
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);
}
@Override
public void scroll(GameContainer container, StateBasedGame game, int newValue) {
Input input = container.getInput();
if (input.isKeyDown(Input.KEY_LALT) || input.isKeyDown(Input.KEY_RALT))
super.scroll(container, game, newValue);
}
},
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);
}
@Override
public void scroll(GameContainer container, StateBasedGame game, int newValue) {
MenuState.BEATMAP.scroll(container, game, newValue);
}
},
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);
}
@Override
public void scroll(GameContainer container, StateBasedGame game, int newValue) {
MenuState.BEATMAP.scroll(container, game, newValue);
}
},
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);
}
@Override
public void scroll(GameContainer container, StateBasedGame game, int newValue) {
MenuState.BEATMAP.scroll(container, game, newValue);
}
},
SCORE (new Button[] { Button.DELETE_SCORE, Button.CLOSE }) {
@Override
public String[] getTitle(GameContainer container, StateBasedGame game) {
return new String[] { "Score Management" };
}
@Override
public void leave(GameContainer container, StateBasedGame game) {
Button.CLOSE.click(container, game);
}
@Override
public void scroll(GameContainer container, StateBasedGame game, int newValue) {
MenuState.BEATMAP.scroll(container, game, newValue);
}
},
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) {
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
for (GameMod mod : GameMod.values())
mod.draw();
super.draw(container, game, g);
}
@Override
public void update(GameContainer container, int delta, int mouseX, int mouseY) {
super.update(container, delta, mouseX, mouseY);
GameMod hoverMod = null;
for (GameMod mod : GameMod.values()) {
mod.hoverUpdate(delta, mod.isActive());
if (hoverMod == null && mod.contains(mouseX, mouseY))
hoverMod = mod;
}
// tooltips
if (hoverMod != null && hoverMod.isImplemented())
UI.updateTooltip(delta, hoverMod.getDescription(), true);
}
@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;
}
}
}
@Override
public void scroll(GameContainer container, StateBasedGame game, int newValue) {
MenuState.BEATMAP.scroll(container, game, newValue);
}
};
/** The buttons in the state. */
private Button[] buttons;
/** The associated MenuButton objects. */
private MenuButton[] menuButtons;
/** The actual title string list, generated upon entering the state. */
private List<String> actualTitle;
/** 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 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, 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
* @param game the game
* @param g the graphics context
*/
public void draw(GameContainer container, StateBasedGame game, Graphics g) {
// draw title
if (actualTitle != null) {
float c = container.getWidth() * 0.02f;
int lineHeight = Utils.FONT_LARGE.getLineHeight();
for (int i = 0, size = actualTitle.size(); i < size; i++)
Utils.FONT_LARGE.drawString(c, c + (i * lineHeight), actualTitle.get(i), Color.white);
}
// draw buttons
for (int i = 0; i < buttons.length; i++)
menuButtons[i].draw(buttons[i].getColor());
UI.draw(g);
}
/**
* 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.
* @param container the game container
* @param game the game
* @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 key, char c) {
int index = Character.getNumericValue(c) - 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 mouse wheel movement.
* @param container the game container
* @param game the game
* @param newValue the amount that the mouse wheel moved
*/
public void scroll(GameContainer container, StateBasedGame game, int newValue) {
UI.changeVolume((newValue < 0) ? -1 : 1);
}
/**
* 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();
}
// create title string list
actualTitle = new ArrayList<String>();
String[] title = getTitle(container, game);
int maxLineWidth = (int) (container.getWidth() * 0.96f);
for (int i = 0; i < title.length; i++) {
// wrap text if too long
if (Utils.FONT_LARGE.getWidth(title[i]) > maxLineWidth) {
List<String> list = Utils.wrap(title[i], Utils.FONT_LARGE, maxLineWidth);
actualTitle.addAll(list);
} else
actualTitle.add(title[i]);
}
}
/**
* 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);
}
},
DELETE_SCORE ("Delete score", Color.green) {
@Override
public void click(GameContainer container, StateBasedGame game) {
SoundController.playSound(SoundEffect.MENUHIT);
ScoreData scoreData = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getScoreData();
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).doStateActionOnLoad(MenuState.SCORE, scoreData);
game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black));
}
},
CLOSE ("Close", Color.gray) {
@Override
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.MENUCLICK);
for (GameMod mod : GameMod.values()) {
if (mod.isActive())
mod.toggle(false);
}
}
};
/** 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;
/** The score data to process in the state. */
private ScoreData scoreData;
// 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);
}
@Override
public void update(GameContainer container, StateBasedGame game, int delta)
throws SlickException {
UI.update(delta);
MusicController.loopTrackIfEnded(false);
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_MIDDLE_BUTTON)
return;
if (menuState != null)
menuState.click(container, game, x, y);
}
@Override
public void mouseWheelMoved(int newValue) {
if (menuState != null)
menuState.scroll(container, game, newValue);
}
@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_F7:
Options.setNextFPS(container);
break;
case Input.KEY_F10:
Options.toggleMouseDisabled();
break;
case Input.KEY_F12:
Utils.takeScreenShot();
break;
default:
if (menuState != null)
menuState.keyPress(container, game, key, c);
break;
}
}
@Override
public void enter(GameContainer container, StateBasedGame game)
throws SlickException {
UI.enter();
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, 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) { setMenuState(menuState, node, null); }
/**
* Changes the menu state.
* @param menuState the new menu state
* @param scoreData the score scoreData
*/
public void setMenuState(MenuState menuState, ScoreData scoreData) { setMenuState(menuState, null, scoreData); }
/**
* Changes the menu state.
* @param menuState the new menu state
* @param node the song node to process in the state
* @param scoreData the score scoreData
*/
private void setMenuState(MenuState menuState, OsuGroupNode node, ScoreData scoreData) {
this.menuState = menuState;
this.node = node;
this.scoreData = scoreData;
}
/**
* Returns the song node being processed, or null if none.
*/
private OsuGroupNode getNode() { return node; }
/**
* Returns the score data being processed, or null if none.
*/
private ScoreData getScoreData() { return scoreData; }
}