diff --git a/src/itdelatrisu/opsu/Container.java b/src/itdelatrisu/opsu/Container.java index a320bc9d..05cb3282 100644 --- a/src/itdelatrisu/opsu/Container.java +++ b/src/itdelatrisu/opsu/Container.java @@ -116,7 +116,7 @@ public class Container extends AppGameContainer { Options.saveOptions(); // reset cursor - UI.resetCursor(); + UI.getCursor().reset(); // destroy images InternalTextureLoader.get().clear(); diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index 1aa57d65..1f89dd5b 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -202,7 +202,8 @@ public class Opsu extends StateBasedGame { } else songMenu.resetTrackOnLoad(); } - UI.resetCursor(); + if (UI.getCursor().isSkinned()) + UI.getCursor().reset(); this.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); return false; } diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index b70e20d1..e2436cbe 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -225,7 +225,7 @@ public class Options { @Override public void click(GameContainer container) { super.click(container); - UI.resetCursor(); + UI.getCursor().reset(); } }, DYNAMIC_BACKGROUND ("Enable Dynamic Backgrounds", "The song background will be used as the main menu background.", true), diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index a494cfeb..abc53636 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -544,7 +544,6 @@ public class Game extends BasicGameState { UI.draw(g, autoMouseX, autoMouseY, Utils.isGameKeyPressed()); else UI.draw(g); - } @Override @@ -1105,7 +1104,7 @@ public class Game extends BasicGameState { // unhide cursor for "auto" mod and replays if (GameMod.AUTO.isActive() || isReplay) - UI.showCursor(); + UI.getCursor().show(); // load replay frames if (isReplay) { @@ -1163,7 +1162,7 @@ public class Game extends BasicGameState { // re-hide cursor if (GameMod.AUTO.isActive() || isReplay) - UI.hideCursor(); + UI.getCursor().hide(); // replays if (isReplay) diff --git a/src/itdelatrisu/opsu/states/GamePauseMenu.java b/src/itdelatrisu/opsu/states/GamePauseMenu.java index 99a84199..e73e5a2a 100644 --- a/src/itdelatrisu/opsu/states/GamePauseMenu.java +++ b/src/itdelatrisu/opsu/states/GamePauseMenu.java @@ -133,7 +133,8 @@ public class GamePauseMenu extends BasicGameState { SoundController.playSound(SoundEffect.MENUBACK); ((SongMenu) game.getState(Opsu.STATE_SONGMENU)).resetGameDataOnLoad(); MusicController.playAt(MusicController.getBeatmap().previewTime, true); - UI.resetCursor(); + if (UI.getCursor().isSkinned()) + UI.getCursor().reset(); game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); } else { SoundController.playSound(SoundEffect.MENUBACK); @@ -186,7 +187,8 @@ public class GamePauseMenu extends BasicGameState { MusicController.playAt(MusicController.getBeatmap().previewTime, true); else MusicController.resume(); - UI.resetCursor(); + if (UI.getCursor().isSkinned()) + UI.getCursor().reset(); game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); } } diff --git a/src/itdelatrisu/opsu/states/GameRanking.java b/src/itdelatrisu/opsu/states/GameRanking.java index c40b849c..4fcc18ec 100644 --- a/src/itdelatrisu/opsu/states/GameRanking.java +++ b/src/itdelatrisu/opsu/states/GameRanking.java @@ -244,7 +244,8 @@ public class GameRanking extends BasicGameState { songMenu.resetGameDataOnLoad(); songMenu.resetTrackOnLoad(); } - UI.resetCursor(); + if (UI.getCursor().isSkinned()) + UI.getCursor().reset(); game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); } diff --git a/src/itdelatrisu/opsu/ui/Cursor.java b/src/itdelatrisu/opsu/ui/Cursor.java new file mode 100644 index 00000000..d990205c --- /dev/null +++ b/src/itdelatrisu/opsu/ui/Cursor.java @@ -0,0 +1,309 @@ +/* + * 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.ui; + +import itdelatrisu.opsu.ErrorHandler; +import itdelatrisu.opsu.GameImage; +import itdelatrisu.opsu.Opsu; +import itdelatrisu.opsu.Options; +import itdelatrisu.opsu.Utils; +import itdelatrisu.opsu.skins.Skin; + +import java.nio.IntBuffer; +import java.util.Iterator; +import java.util.LinkedList; + +import org.lwjgl.BufferUtils; +import org.lwjgl.LWJGLException; +import org.newdawn.slick.GameContainer; +import org.newdawn.slick.Image; +import org.newdawn.slick.Input; +import org.newdawn.slick.SlickException; +import org.newdawn.slick.state.StateBasedGame; + +/** + * Updates and draws the cursor. + */ +public class Cursor { + /** Empty cursor. */ + private static org.lwjgl.input.Cursor emptyCursor; + + /** Last cursor coordinates. */ + private int lastX = -1, lastY = -1; + + /** Cursor rotation angle. */ + private float cursorAngle = 0f; + + /** Stores all previous cursor locations to display a trail. */ + private LinkedList cursorX, cursorY; + + // game-related variables + private static GameContainer container; + private static StateBasedGame game; + private static Input input; + + /** + * Initializes the class. + * @param container the game container + * @param game the game object + */ + public static void init(GameContainer container, StateBasedGame game) { + Cursor.container = container; + Cursor.game = game; + Cursor.input = container.getInput(); + + // create empty cursor to simulate hiding the cursor + try { + int min = org.lwjgl.input.Cursor.getMinCursorSize(); + IntBuffer tmp = BufferUtils.createIntBuffer(min * min); + emptyCursor = new org.lwjgl.input.Cursor(min, min, min/2, min/2, 1, tmp, null); + } catch (LWJGLException e) { + ErrorHandler.error("Failed to create hidden cursor.", e, true); + } + } + + /** + * Constructor. + */ + public Cursor() { + cursorX = new LinkedList(); + cursorY = new LinkedList(); + } + + /** + * Draws the cursor. + */ + public void draw() { + int state = game.getCurrentStateID(); + boolean mousePressed = + (((state == Opsu.STATE_GAME || state == Opsu.STATE_GAMEPAUSEMENU) && Utils.isGameKeyPressed()) || + ((input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON) || input.isMouseButtonDown(Input.MOUSE_RIGHT_BUTTON)) && + !(state == Opsu.STATE_GAME && Options.isMouseDisabled()))); + draw(input.getMouseX(), input.getMouseY(), mousePressed); + } + + /** + * Draws the cursor. + * @param mouseX the mouse x coordinate + * @param mouseY the mouse y coordinate + * @param mousePressed whether or not the mouse button is pressed + */ + public void draw(int mouseX, int mouseY, boolean mousePressed) { + // determine correct cursor image + Image cursor = null, cursorMiddle = null, cursorTrail = null; + boolean skinned = GameImage.CURSOR.hasSkinImage(); + boolean newStyle, hasMiddle; + if (skinned) { + newStyle = true; // osu! currently treats all beatmap cursors as new-style cursors + hasMiddle = GameImage.CURSOR_MIDDLE.hasSkinImage(); + } else + newStyle = hasMiddle = Options.isNewCursorEnabled(); + if (skinned || newStyle) { + cursor = GameImage.CURSOR.getImage(); + cursorTrail = GameImage.CURSOR_TRAIL.getImage(); + } else { + cursor = GameImage.CURSOR_OLD.getImage(); + cursorTrail = GameImage.CURSOR_TRAIL_OLD.getImage(); + } + if (hasMiddle) + cursorMiddle = GameImage.CURSOR_MIDDLE.getImage(); + + int removeCount = 0; + int FPSmod = (Options.getTargetFPS() / 60); + Skin skin = Options.getSkin(); + + // TODO: use an image buffer + if (newStyle) { + // new style: add all points between cursor movements + if (lastX < 0) { + lastX = mouseX; + lastY = mouseY; + return; + } + addCursorPoints(lastX, lastY, mouseX, mouseY); + lastX = mouseX; + lastY = mouseY; + + removeCount = (cursorX.size() / (6 * FPSmod)) + 1; + } else { + // old style: sample one point at a time + cursorX.add(mouseX); + cursorY.add(mouseY); + + int max = 10 * FPSmod; + if (cursorX.size() > max) + removeCount = cursorX.size() - max; + } + + // remove points from the lists + for (int i = 0; i < removeCount && !cursorX.isEmpty(); i++) { + cursorX.remove(); + cursorY.remove(); + } + + // draw a fading trail + float alpha = 0f; + float t = 2f / cursorX.size(); + if (skin.isCursorTrailRotated()) + cursorTrail.setRotation(cursorAngle); + Iterator iterX = cursorX.iterator(); + Iterator iterY = cursorY.iterator(); + while (iterX.hasNext()) { + int cx = iterX.next(); + int cy = iterY.next(); + alpha += t; + cursorTrail.setAlpha(alpha); +// if (cx != x || cy != y) + cursorTrail.drawCentered(cx, cy); + } + cursorTrail.drawCentered(mouseX, mouseY); + + // increase the cursor size if pressed + if (mousePressed && skin.isCursorExpanded()) { + final float scale = 1.25f; + cursor = cursor.getScaledCopy(scale); + if (hasMiddle) + cursorMiddle = cursorMiddle.getScaledCopy(scale); + } + + // draw the other components + if (newStyle && skin.isCursorRotated()) + cursor.setRotation(cursorAngle); + cursor.drawCentered(mouseX, mouseY); + if (hasMiddle) + cursorMiddle.drawCentered(mouseX, mouseY); + } + + /** + * Adds all points between (x1, y1) and (x2, y2) to the cursor point lists. + * @author http://rosettacode.org/wiki/Bitmap/Bresenham's_line_algorithm#Java + */ + private void addCursorPoints(int x1, int y1, int x2, int y2) { + // delta of exact value and rounded value of the dependent variable + int d = 0; + int dy = Math.abs(y2 - y1); + int dx = Math.abs(x2 - x1); + + int dy2 = (dy << 1); // slope scaling factors to avoid floating + int dx2 = (dx << 1); // point + int ix = x1 < x2 ? 1 : -1; // increment direction + int iy = y1 < y2 ? 1 : -1; + + int k = 5; // sample size + if (dy <= dx) { + for (int i = 0; ; i++) { + if (i == k) { + cursorX.add(x1); + cursorY.add(y1); + i = 0; + } + if (x1 == x2) + break; + x1 += ix; + d += dy2; + if (d > dx) { + y1 += iy; + d -= dx2; + } + } + } else { + for (int i = 0; ; i++) { + if (i == k) { + cursorX.add(x1); + cursorY.add(y1); + i = 0; + } + if (y1 == y2) + break; + y1 += iy; + d += dx2; + if (d > dy) { + x1 += ix; + d -= dy2; + } + } + } + } + + /** + * Rotates the cursor by a degree determined by a delta interval. + * If the old style cursor is being used, this will do nothing. + * @param delta the delta interval since the last call + */ + public void update(int delta) { + cursorAngle += delta / 40f; + cursorAngle %= 360; + } + + /** + * Resets all cursor data and skins. + */ + public void reset() { + // destroy skin images + GameImage.CURSOR.destroySkinImage(); + GameImage.CURSOR_MIDDLE.destroySkinImage(); + GameImage.CURSOR_TRAIL.destroySkinImage(); + + // reset locations + resetLocations(); + + // reset angles + cursorAngle = 0f; + GameImage.CURSOR.getImage().setRotation(0f); + GameImage.CURSOR_TRAIL.getImage().setRotation(0f); + } + + /** + * Resets all cursor location data. + */ + public void resetLocations() { + lastX = lastY = -1; + cursorX.clear(); + cursorY.clear(); + } + + /** + * Returns whether or not the cursor is skinned. + */ + public boolean isSkinned() { + return (GameImage.CURSOR.hasSkinImage() || + GameImage.CURSOR_MIDDLE.hasSkinImage() || + GameImage.CURSOR_TRAIL.hasSkinImage()); + } + + /** + * Hides the cursor, if possible. + */ + public void hide() { + if (emptyCursor != null) { + try { + container.setMouseCursor(emptyCursor, 0, 0); + } catch (SlickException e) { + ErrorHandler.error("Failed to hide the cursor.", e, true); + } + } + } + + /** + * Unhides the cursor. + */ + public void show() { + container.setDefaultMouseCursor(); + } +} diff --git a/src/itdelatrisu/opsu/ui/UI.java b/src/itdelatrisu/opsu/ui/UI.java index c486f082..80e4cd3b 100644 --- a/src/itdelatrisu/opsu/ui/UI.java +++ b/src/itdelatrisu/opsu/ui/UI.java @@ -20,24 +20,15 @@ package itdelatrisu.opsu.ui; import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.GameImage; -import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.Options; import itdelatrisu.opsu.OszUnpacker; import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.beatmap.BeatmapParser; -import itdelatrisu.opsu.skins.Skin; - -import java.nio.IntBuffer; -import java.util.Iterator; -import java.util.LinkedList; import javax.swing.JOptionPane; import javax.swing.UIManager; -import org.lwjgl.BufferUtils; -import org.lwjgl.LWJGLException; -import org.lwjgl.input.Cursor; import org.newdawn.slick.Animation; import org.newdawn.slick.Color; import org.newdawn.slick.GameContainer; @@ -48,26 +39,15 @@ import org.newdawn.slick.SlickException; import org.newdawn.slick.state.StateBasedGame; /** - * Class primarily used for drawing UI components. + * Draws common UI components. */ public class UI { + /** Cursor. */ + private static Cursor cursor = new Cursor(); + /** Back button. */ private static MenuButton backButton; - /** Empty cursor. */ - private static Cursor emptyCursor; - - /** Last cursor coordinates. */ - private static int lastX = -1, lastY = -1; - - /** Cursor rotation angle. */ - private static float cursorAngle = 0f; - - /** Stores all previous cursor locations to display a trail. */ - private static LinkedList - cursorX = new LinkedList(), - cursorY = new LinkedList(); - /** Time to show volume image, in milliseconds. */ private static final int VOLUME_DISPLAY_TIME = 1500; @@ -97,7 +77,6 @@ public class UI { // game-related variables private static GameContainer container; - private static StateBasedGame game; private static Input input; // This class should not be instantiated. @@ -112,18 +91,11 @@ public class UI { public static void init(GameContainer container, StateBasedGame game) throws SlickException { UI.container = container; - UI.game = game; UI.input = container.getInput(); - // hide native cursor - try { - int min = Cursor.getMinCursorSize(); - IntBuffer tmp = BufferUtils.createIntBuffer(min * min); - emptyCursor = new Cursor(min, min, min/2, min/2, 1, tmp, null); - hideCursor(); - } catch (LWJGLException e) { - ErrorHandler.error("Failed to create hidden cursor.", e, true); - } + // initialize cursor + Cursor.init(container, game); + cursor.hide(); // back button if (GameImage.MENU_BACK.getImages() != null) { @@ -141,7 +113,7 @@ public class UI { * @param delta the delta interval since the last call. */ public static void update(int delta) { - updateCursor(delta); + cursor.update(delta); updateVolumeDisplay(delta); updateBarNotification(delta); if (tooltipTimer > 0) @@ -156,7 +128,7 @@ public class UI { drawBarNotification(g); drawVolume(g); drawFPS(); - drawCursor(); + cursor.draw(); drawTooltip(g); } @@ -171,7 +143,7 @@ public class UI { drawBarNotification(g); drawVolume(g); drawFPS(); - drawCursor(mouseX, mouseY, mousePressed); + cursor.draw(mouseX, mouseY, mousePressed); drawTooltip(g); } @@ -180,11 +152,16 @@ public class UI { */ public static void enter() { backButton.resetHover(); + cursor.resetLocations(); resetBarNotification(); - resetCursorLocations(); resetTooltip(); } + /** + * Returns the game cursor. + */ + public static Cursor getCursor() { return cursor; } + /** * Returns the 'menu-back' MenuButton. */ @@ -214,212 +191,6 @@ public class UI { Utils.FONT_MEDIUM.drawString(tabTextX, tabTextY, text, textColor); } - /** - * Draws the cursor. - */ - public static void drawCursor() { - int state = game.getCurrentStateID(); - boolean mousePressed = - (((state == Opsu.STATE_GAME || state == Opsu.STATE_GAMEPAUSEMENU) && Utils.isGameKeyPressed()) || - ((input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON) || input.isMouseButtonDown(Input.MOUSE_RIGHT_BUTTON)) && - !(state == Opsu.STATE_GAME && Options.isMouseDisabled()))); - drawCursor(input.getMouseX(), input.getMouseY(), mousePressed); - } - - /** - * Draws the cursor. - * @param mouseX the mouse x coordinate - * @param mouseY the mouse y coordinate - * @param mousePressed whether or not the mouse button is pressed - */ - public static void drawCursor(int mouseX, int mouseY, boolean mousePressed) { - // determine correct cursor image - Image cursor = null, cursorMiddle = null, cursorTrail = null; - boolean skinned = GameImage.CURSOR.hasSkinImage(); - boolean newStyle, hasMiddle; - if (skinned) { - newStyle = true; // osu! currently treats all beatmap cursors as new-style cursors - hasMiddle = GameImage.CURSOR_MIDDLE.hasSkinImage(); - } else - newStyle = hasMiddle = Options.isNewCursorEnabled(); - if (skinned || newStyle) { - cursor = GameImage.CURSOR.getImage(); - cursorTrail = GameImage.CURSOR_TRAIL.getImage(); - } else { - cursor = GameImage.CURSOR_OLD.getImage(); - cursorTrail = GameImage.CURSOR_TRAIL_OLD.getImage(); - } - if (hasMiddle) - cursorMiddle = GameImage.CURSOR_MIDDLE.getImage(); - - int removeCount = 0; - int FPSmod = (Options.getTargetFPS() / 60); - Skin skin = Options.getSkin(); - - // TODO: use an image buffer - if (newStyle) { - // new style: add all points between cursor movements - if (lastX < 0) { - lastX = mouseX; - lastY = mouseY; - return; - } - addCursorPoints(lastX, lastY, mouseX, mouseY); - lastX = mouseX; - lastY = mouseY; - - removeCount = (cursorX.size() / (6 * FPSmod)) + 1; - } else { - // old style: sample one point at a time - cursorX.add(mouseX); - cursorY.add(mouseY); - - int max = 10 * FPSmod; - if (cursorX.size() > max) - removeCount = cursorX.size() - max; - } - - // remove points from the lists - for (int i = 0; i < removeCount && !cursorX.isEmpty(); i++) { - cursorX.remove(); - cursorY.remove(); - } - - // draw a fading trail - float alpha = 0f; - float t = 2f / cursorX.size(); - if (skin.isCursorTrailRotated()) - cursorTrail.setRotation(cursorAngle); - Iterator iterX = cursorX.iterator(); - Iterator iterY = cursorY.iterator(); - while (iterX.hasNext()) { - int cx = iterX.next(); - int cy = iterY.next(); - alpha += t; - cursorTrail.setAlpha(alpha); -// if (cx != x || cy != y) - cursorTrail.drawCentered(cx, cy); - } - cursorTrail.drawCentered(mouseX, mouseY); - - // increase the cursor size if pressed - if (mousePressed && skin.isCursorExpanded()) { - final float scale = 1.25f; - cursor = cursor.getScaledCopy(scale); - if (hasMiddle) - cursorMiddle = cursorMiddle.getScaledCopy(scale); - } - - // draw the other components - if (newStyle && skin.isCursorRotated()) - cursor.setRotation(cursorAngle); - cursor.drawCentered(mouseX, mouseY); - if (hasMiddle) - cursorMiddle.drawCentered(mouseX, mouseY); - } - - /** - * Adds all points between (x1, y1) and (x2, y2) to the cursor point lists. - * @author http://rosettacode.org/wiki/Bitmap/Bresenham's_line_algorithm#Java - */ - private static void addCursorPoints(int x1, int y1, int x2, int y2) { - // delta of exact value and rounded value of the dependent variable - int d = 0; - int dy = Math.abs(y2 - y1); - int dx = Math.abs(x2 - x1); - - int dy2 = (dy << 1); // slope scaling factors to avoid floating - int dx2 = (dx << 1); // point - int ix = x1 < x2 ? 1 : -1; // increment direction - int iy = y1 < y2 ? 1 : -1; - - int k = 5; // sample size - if (dy <= dx) { - for (int i = 0; ; i++) { - if (i == k) { - cursorX.add(x1); - cursorY.add(y1); - i = 0; - } - if (x1 == x2) - break; - x1 += ix; - d += dy2; - if (d > dx) { - y1 += iy; - d -= dx2; - } - } - } else { - for (int i = 0; ; i++) { - if (i == k) { - cursorX.add(x1); - cursorY.add(y1); - i = 0; - } - if (y1 == y2) - break; - y1 += iy; - d += dx2; - if (d > dy) { - x1 += ix; - d -= dy2; - } - } - } - } - - /** - * Rotates the cursor by a degree determined by a delta interval. - * If the old style cursor is being used, this will do nothing. - * @param delta the delta interval since the last call - */ - private static void updateCursor(int delta) { - cursorAngle += delta / 40f; - cursorAngle %= 360; - } - - /** - * Resets all cursor data and skins. - */ - public static void resetCursor() { - GameImage.CURSOR.destroySkinImage(); - GameImage.CURSOR_MIDDLE.destroySkinImage(); - GameImage.CURSOR_TRAIL.destroySkinImage(); - cursorAngle = 0f; - GameImage.CURSOR.getImage().setRotation(0f); - GameImage.CURSOR_TRAIL.getImage().setRotation(0f); - } - - /** - * Resets all cursor location data. - */ - private static void resetCursorLocations() { - lastX = lastY = -1; - cursorX.clear(); - cursorY.clear(); - } - - /** - * Hides the cursor, if possible. - */ - public static void hideCursor() { - if (emptyCursor != null) { - try { - container.setMouseCursor(emptyCursor, 0, 0); - } catch (SlickException e) { - ErrorHandler.error("Failed to hide the cursor.", e, true); - } - } - } - - /** - * Unhides the cursor. - */ - public static void showCursor() { - container.setDefaultMouseCursor(); - } - /** * Draws the FPS at the bottom-right corner of the game container. * If the option is not activated, this will do nothing.