diff --git a/CREDITS.md b/CREDITS.md index 0423c10f..39d96694 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -26,7 +26,9 @@ The images included in opsu! belong to their respective authors. * kouyang * teinecthel * Teddy Kelley - https://unsplash.com/photos/weuWmzv7xnU (main menu background) -* User icons ("User", "Male User", "Female User") by To Uyen from the Noun Project +* User icons designed by Freepik - https://www.freepik.com/ + * https://www.freepik.com/free-vector/flat-lovely-animal-avatar-collection_845660.htm + * https://www.freepik.com/free-vector/wild-and-marine-animal-collection_845661.htm * Font Awesome by Dave Gandy - http://fontawesome.io Projects diff --git a/res/user0.png b/res/user0.png index 6680b817..0bfbecc0 100644 Binary files a/res/user0.png and b/res/user0.png differ diff --git a/res/user1.png b/res/user1.png index c66ce5b6..46b5c829 100644 Binary files a/res/user1.png and b/res/user1.png differ diff --git a/res/user10.png b/res/user10.png new file mode 100644 index 00000000..24cb5912 Binary files /dev/null and b/res/user10.png differ diff --git a/res/user11.png b/res/user11.png new file mode 100644 index 00000000..3fd0a9ee Binary files /dev/null and b/res/user11.png differ diff --git a/res/user12.png b/res/user12.png new file mode 100644 index 00000000..4137ba66 Binary files /dev/null and b/res/user12.png differ diff --git a/res/user13.png b/res/user13.png new file mode 100644 index 00000000..e267005e Binary files /dev/null and b/res/user13.png differ diff --git a/res/user14.png b/res/user14.png new file mode 100644 index 00000000..5aca8ede Binary files /dev/null and b/res/user14.png differ diff --git a/res/user2.png b/res/user2.png index 61863a8a..ea8aab24 100644 Binary files a/res/user2.png and b/res/user2.png differ diff --git a/res/user3.png b/res/user3.png new file mode 100644 index 00000000..d358d90f Binary files /dev/null and b/res/user3.png differ diff --git a/res/user4.png b/res/user4.png new file mode 100644 index 00000000..95061ed8 Binary files /dev/null and b/res/user4.png differ diff --git a/res/user5.png b/res/user5.png new file mode 100644 index 00000000..2fec60f7 Binary files /dev/null and b/res/user5.png differ diff --git a/res/user6.png b/res/user6.png new file mode 100644 index 00000000..e9764f29 Binary files /dev/null and b/res/user6.png differ diff --git a/res/user7.png b/res/user7.png new file mode 100644 index 00000000..86e586b1 Binary files /dev/null and b/res/user7.png differ diff --git a/res/user8.png b/res/user8.png new file mode 100644 index 00000000..2caba93e Binary files /dev/null and b/res/user8.png differ diff --git a/res/user9.png b/res/user9.png new file mode 100644 index 00000000..adbc07be Binary files /dev/null and b/res/user9.png differ diff --git a/src/itdelatrisu/opsu/user/UserButton.java b/src/itdelatrisu/opsu/user/UserButton.java index cf68daab..a5903c83 100644 --- a/src/itdelatrisu/opsu/user/UserButton.java +++ b/src/itdelatrisu/opsu/user/UserButton.java @@ -62,6 +62,9 @@ public class UserButton { /** The user. */ private User user; + /** Placeholder text to display if {@link #user} is null. */ + private String placeholderText; + /** * Initializes the user buttons. * @param width the container width @@ -124,6 +127,14 @@ public void setUser(User user) { /** Returns the user. */ public User getUser() { return user; } + /** Sets the text to display if no user is set. */ + public void setPlaceholderText(String placeholderText) { + this.placeholderText = placeholderText; + } + + /** Returns the placeholder text. */ + public String getPlaceholderText() { return placeholderText; } + /** * Returns true if the coordinates are within the button bounds. * @param cx the x coordinate @@ -164,12 +175,11 @@ public void draw(Graphics g, float alpha) { g.fillRoundRect(cx, cy, buttonWidth - padding * 2, buttonHeight - padding * 2, 4); // no user? - if (user == null) { - String text = "Add User"; + if (user == null && placeholderText != null) { Fonts.LARGE.drawString( - x + (buttonWidth - Fonts.LARGE.getWidth(text)) / 2, + x + (buttonWidth - Fonts.LARGE.getWidth(placeholderText)) / 2, y + (buttonHeight - Fonts.LARGE.getLineHeight()) / 2, - text, Colors.WHITE_FADE + placeholderText, Colors.WHITE_FADE ); Colors.WHITE_FADE.a = oldWhiteAlpha; return; @@ -248,6 +258,16 @@ public void setHoverAnimationEquation(AnimationEquation eqn) { bgAlpha.setEquation(eqn); } + /** + * Sets the hover animation base value. + * @param base the base value + */ + public void setHoverAnimationBase(float base) { + AnimatedValue value = new AnimatedValue(bgAlpha.getDuration(), base, 1f, bgAlpha.getEquation()); + value.setTime(bgAlpha.getTime()); + bgAlpha = value; + } + /** Resets the hover fields for the button. */ public void resetHover() { bgAlpha.setTime(0); diff --git a/src/itdelatrisu/opsu/user/UserList.java b/src/itdelatrisu/opsu/user/UserList.java index 68af839a..0d84aea1 100644 --- a/src/itdelatrisu/opsu/user/UserList.java +++ b/src/itdelatrisu/opsu/user/UserList.java @@ -80,6 +80,9 @@ public UserList() { } } + /** Returns the number of users. */ + public int size() { return users.size(); } + /** Returns all users. */ public List getUsers() { List l = new ArrayList(users.values()); @@ -126,6 +129,20 @@ public User createNewUser(String name, int icon) { return user; } + /** + * Deletes the given user. + * @param name the user's name + * @return true if the user was deleted, false otherwise + */ + public boolean deleteUser(String name) { + if (!userExists(name) || name.equals(currentUser.getName())) + return false; + + ScoreDB.deleteUser(name); + users.remove(name.toLowerCase()); + return true; + } + /** Returns whether the given name is a valid user name. */ public boolean isValidUserName(String name) { return !name.isEmpty() && name.length() <= MAX_USER_NAME_LENGTH && diff --git a/src/itdelatrisu/opsu/user/UserSelectOverlay.java b/src/itdelatrisu/opsu/user/UserSelectOverlay.java index abfbdead..64e7023f 100644 --- a/src/itdelatrisu/opsu/user/UserSelectOverlay.java +++ b/src/itdelatrisu/opsu/user/UserSelectOverlay.java @@ -20,6 +20,7 @@ import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundEffect; +import itdelatrisu.opsu.db.ScoreDB; import itdelatrisu.opsu.ui.Colors; import itdelatrisu.opsu.ui.Fonts; import itdelatrisu.opsu.ui.KineticScrolling; @@ -107,15 +108,24 @@ public interface UserSelectOverlayListener { /** New user button. */ private UserButton newUserButton; - /** New user icons. */ - private MenuButton[] newUserIcons; + /** User icons. */ + private MenuButton[] userIcons; + + /** Edit user button. */ + private UserButton editUserButton; + + /** Delete user button. */ + private UserButton deleteUserButton; /** States. */ - private enum State { USER_SELECT, CREATE_USER } + private enum State { USER_SELECT, CREATE_USER, EDIT_USER } /** Current state. */ private State state = State.USER_SELECT; + /** Previous state. */ + private State prevState; + /** State change progress. */ private AnimatedValue stateChangeProgress = new AnimatedValue(500, 0f, 1f, AnimationEquation.LINEAR); @@ -170,13 +180,28 @@ public UserSelectOverlay(GameContainer container, UserSelectOverlayListener list this.textField = new TextField(container, null, 0, 0, 0, 0); textField.setMaxLength(UserList.MAX_USER_NAME_LENGTH); - // new user icons - this.newUserIcons = new MenuButton[UserButton.getIconCount()]; - for (int i = 0; i < newUserIcons.length; i++) { - newUserIcons[i] = new MenuButton(UserButton.getIconImage(i), 0, 0); - newUserIcons[i].setHoverFade(0.5f); + // user icons + this.userIcons = new MenuButton[UserButton.getIconCount()]; + for (int i = 0; i < userIcons.length; i++) { + userIcons[i] = new MenuButton(UserButton.getIconImage(i), 0, 0); + userIcons[i].setHoverFade(0.5f); } + // edit user + this.editUserButton = new UserButton( + (int) (this.x + usersStartX), + (int) (this.y + usersStartY), + Color.white + ); + + // delete user + deleteUserButton = new UserButton( + (int) (this.x + usersStartX), + (int) (this.y + usersStartY + UserButton.getHeight() + usersPaddingY), + Colors.RED_HOVER + ); + deleteUserButton.setHoverAnimationBase(0.5f); + // kinetic scrolling this.scrolling = new KineticScrolling(); scrolling.setAllowOverScroll(true); @@ -223,6 +248,7 @@ public void activate() { // set initial state state = State.USER_SELECT; + prevState = null; stateChangeProgress.setTime(stateChangeProgress.getDuration()); prepareUserSelect(); } @@ -248,14 +274,19 @@ public void render(GUIContext container, Graphics g) throws SlickException { if (!stateChangeProgress.isFinished()) { // blend states float t = stateChangeProgress.getValue(); - if (state == State.CREATE_USER) + if (prevState == State.USER_SELECT) t = 1f - t; renderUserSelect(g, t); - renderUserCreate(g, 1f - t); + if (state == State.CREATE_USER || prevState == State.CREATE_USER) + renderUserCreate(g, 1f - t); + else if (state == State.EDIT_USER || prevState == State.EDIT_USER) + renderUserEdit(g, 1f - t); } else if (state == State.USER_SELECT) renderUserSelect(g, globalAlpha); else if (state == State.CREATE_USER) renderUserCreate(g, globalAlpha); + else if (state == State.EDIT_USER) + renderUserEdit(g, globalAlpha); g.clearClip(); } @@ -332,29 +363,56 @@ private void renderUserCreate(Graphics g, float alpha) { cy += Fonts.MEDIUMBOLD.getLineHeight(); // user icons - String iconHeader = "Icon"; - Fonts.MEDIUMBOLD.drawString(x + (width - Fonts.MEDIUMBOLD.getWidth(iconHeader)) / 2, cy, iconHeader, COLOR_WHITE); + renderUserIcons(g, newUser.getIconId(), "Icon", cy, alpha); + } + + /** Renders the user edit menu. */ + private void renderUserEdit(Graphics g, float alpha) { + COLOR_WHITE.a = COLOR_RED.a = alpha; + + // title + String title = "Edit User"; + Fonts.XLARGE.drawString( + x + (width - Fonts.XLARGE.getWidth(title)) / 2, + (int) (y + titleY), + title, COLOR_WHITE + ); + + // edit user button + editUserButton.draw(g, alpha); + + // delete button + deleteUserButton.draw(g, alpha); + + // user icons + int cy = (int) (y + usersStartY + (UserButton.getHeight() + usersPaddingY) * 2); + renderUserIcons(g, editUserButton.getUser().getIconId(), "Change Icon", cy, alpha); + } + + /** Renders the user icons. */ + private void renderUserIcons(Graphics g, int iconId, String header, int cy, float alpha) { + Fonts.MEDIUMBOLD.drawString(x + (width - Fonts.MEDIUMBOLD.getWidth(header)) / 2, cy, header, COLOR_WHITE); cy += Fonts.MEDIUMBOLD.getLineHeight() + usersPaddingY; int iconSize = UserButton.getIconSize(); int paddingX = iconSize / 4; int maxPerLine = UserButton.getWidth() / (iconSize + paddingX); // start scroll area here g.setClip((int) x, cy, width, height - (int) (cy - y)); - int scrollOffset = ((newUserIcons.length - 1) / maxPerLine + 1) * (iconSize + usersPaddingY); + int scrollOffset = ((userIcons.length - 1) / maxPerLine + 1) * (iconSize + usersPaddingY); scrollOffset -= height - cy; scrollOffset = Math.max(scrollOffset, 0); scrolling.setMinMax(0, scrollOffset); cy += -scrolling.getPosition(); - for (int i = 0; i < newUserIcons.length; i += maxPerLine) { + for (int i = 0; i < userIcons.length; i += maxPerLine) { // draw line-by-line - int n = Math.min(maxPerLine, newUserIcons.length - i); + int n = Math.min(maxPerLine, userIcons.length - i); int cx = (int) (x + usersStartX + (UserButton.getWidth() - iconSize * n - paddingX * (n - 1)) / 2); for (int j = 0; j < n; j++) { - MenuButton button = newUserIcons[i + j]; + MenuButton button = userIcons[i + j]; button.setX(cx + iconSize / 2); button.setY(cy + iconSize / 2); if (cy < height) { - button.getImage().setAlpha((newUser.getIconId() == i + j) ? + button.getImage().setAlpha((iconId == i + j) ? alpha : alpha * button.getHoverAlpha() * 0.9f ); button.getImage().draw(cx, cy); @@ -383,10 +441,15 @@ public void update(int delta) { for (UserButton button : userButtons) button.hoverUpdate(delta, button == hover); } - if (state == State.CREATE_USER || !stateChangeProgress.isFinished()) { + if (state == State.CREATE_USER) { newUserButton.hoverUpdate(delta, UserList.get().isValidUserName(newUser.getName())); - for (int i = 0; i < newUserIcons.length; i++) - newUserIcons[i].hoverUpdate(delta, mouseX, mouseY); + for (int i = 0; i < userIcons.length; i++) + userIcons[i].hoverUpdate(delta, mouseX, mouseY); + } + if (state == State.EDIT_USER) { + deleteUserButton.hoverUpdate(delta, deleteUserButton.contains(mouseX, mouseY)); + for (int i = 0; i < userIcons.length; i++) + userIcons[i].hoverUpdate(delta, mouseX, mouseY); } } @@ -437,19 +500,33 @@ public void mouseReleased(int button, int x, int y) { if (state == State.USER_SELECT) { if (selectedButton != null) { SoundController.playSound(SoundEffect.MENUCLICK); - if (selectedButton.getUser() == null) { + User user = selectedButton.getUser(); + if (user == null) { // new user state = State.CREATE_USER; + prevState = State.USER_SELECT; stateChangeProgress.setTime(0); prepareUserCreate(); } else { - // select user - String name = selectedButton.getUser().getName(); - if (!name.equals(UserList.get().getCurrentUser().getName())) { - UserList.get().changeUser(name); - listener.close(true); - } else - listener.close(false); + String name = user.getName(); + if (button == Input.MOUSE_RIGHT_BUTTON) { + // right click: edit user + if (name.equals(UserList.DEFAULT_USER_NAME)) { + UI.getNotificationManager().sendBarNotification("This user can't be edited."); + } else { + state = State.EDIT_USER; + prevState = State.USER_SELECT; + stateChangeProgress.setTime(0); + prepareUserEdit(selectedButton.getUser()); + } + } else { + // left click: select user + if (!name.equals(UserList.get().getCurrentUser().getName())) { + UserList.get().changeUser(name); + listener.close(true); + } else + listener.close(false); + } } } } else if (state == State.CREATE_USER) { @@ -458,14 +535,49 @@ public void mouseReleased(int button, int x, int y) { createNewUser(); else { // change user icons - for (int i = 0; i < newUserIcons.length; i++) { - if (newUserIcons[i].contains(x, y)) { + for (int i = 0; i < userIcons.length; i++) { + if (userIcons[i].contains(x, y)) { SoundController.playSound(SoundEffect.MENUCLICK); newUser.setIconId(i); break; } } } + } else if (state == State.EDIT_USER) { + if (deleteUserButton.contains(x, y)) { + SoundController.playSound(SoundEffect.MENUCLICK); + String name = editUserButton.getUser().getName(); + if (name.equals(UserList.get().getCurrentUser().getName())) + UI.getNotificationManager().sendBarNotification("You can't delete the current user!"); + else { + String confirmationText = "Confirm User Deletion"; + if (!deleteUserButton.getPlaceholderText().equals(confirmationText)) { + // ask for confirmation first + deleteUserButton.setPlaceholderText(confirmationText); + } else { + // actually delete the user + if (UserList.get().deleteUser(name)) { + UI.getNotificationManager().sendNotification(String.format("User '%s' was deleted.", name), Colors.GREEN); + } else { + UI.getNotificationManager().sendNotification("The user could not be deleted.", Color.red); + } + listener.close(false); + } + } + } else { + // change user icons + for (int i = 0; i < userIcons.length; i++) { + if (userIcons[i].contains(x, y)) { + SoundController.playSound(SoundEffect.MENUCLICK); + User user = editUserButton.getUser(); + if (i != user.getIconId()) { + user.setIconId(i); + ScoreDB.updateUser(user); + } + break; + } + } + } } selectedButton = null; @@ -599,9 +711,16 @@ private void prepareUserSelect() { else userButtons.add(button); } + + // add default user at the end if (defaultUser != null) - userButtons.add(defaultUser); // add default user at the end - userButtons.add(new UserButton(0, 0, Color.white)); // create new user + userButtons.add(defaultUser); + + // add button to create new user + UserButton addUserButton = new UserButton(0, 0, Color.white); + addUserButton.setPlaceholderText("Add User"); + userButtons.add(addUserButton); + maxScrollOffset = Math.max(0, (UserButton.getHeight() + usersPaddingY) * userButtons.size() - (int) ((height - usersStartY) * 0.9f)); @@ -617,8 +736,24 @@ private void prepareUserCreate() { newUser.setIconId(UserList.DEFAULT_ICON); textField.setText(""); newUserButton.resetHover(); - for (int i = 0; i < newUserIcons.length; i++) - newUserIcons[i].resetHover(); + + prepareUserIcons(); + } + + /** Prepares the user edit state. */ + private void prepareUserEdit(User user) { + editUserButton.setUser(user); + editUserButton.resetHover(); + deleteUserButton.setPlaceholderText("Delete User"); + deleteUserButton.resetHover(); + + prepareUserIcons(); + } + + /** Prepares the user icons. */ + private void prepareUserIcons() { + for (int i = 0; i < userIcons.length; i++) + userIcons[i].resetHover(); scrolling.setPosition(0f); scrolling.setAllowOverScroll(false);