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 <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han
2015-02-15 21:38:54 -05:00
parent 4826798fba
commit 66016160a4
17 changed files with 308 additions and 160 deletions

View File

@@ -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<SongNode>();
@@ -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