diff --git a/res/inputoverlay-background.png b/res/inputoverlay-background.png new file mode 100644 index 00000000..3ca2e6ee Binary files /dev/null and b/res/inputoverlay-background.png differ diff --git a/res/inputoverlay-key.png b/res/inputoverlay-key.png new file mode 100644 index 00000000..8add7a4e Binary files /dev/null and b/res/inputoverlay-key.png differ diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index 1793d5a9..23a50906 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -40,6 +40,8 @@ public enum GameImage { CURSOR_TRAIL ("cursortrail", "png"), // Game + INPUTOVERLAY_KEY ("inputoverlay-key", "png"), + INPUTOVERLAY_BACKGROUND ("inputoverlay-background", "png"), SECTION_PASS ("section-pass", "png"), SECTION_FAIL ("section-fail", "png"), WARNINGARROW ("play-warningarrow", "png"), diff --git a/src/itdelatrisu/opsu/options/OptionGroup.java b/src/itdelatrisu/opsu/options/OptionGroup.java index b68e6b70..268358a2 100644 --- a/src/itdelatrisu/opsu/options/OptionGroup.java +++ b/src/itdelatrisu/opsu/options/OptionGroup.java @@ -69,6 +69,7 @@ public class OptionGroup { GameOption.BACKGROUND_DIM, GameOption.FORCE_DEFAULT_PLAYFIELD, GameOption.SHOW_HIT_ERROR_BAR, + GameOption.ALWAYS_SHOW_KEY_OVERLAY, }), new OptionGroup("Audio", GameImage.MENU_NAV_AUDIO), new OptionGroup("VOLUME", new GameOption[] { diff --git a/src/itdelatrisu/opsu/options/Options.java b/src/itdelatrisu/opsu/options/Options.java index 010d5709..24019b17 100644 --- a/src/itdelatrisu/opsu/options/Options.java +++ b/src/itdelatrisu/opsu/options/Options.java @@ -562,6 +562,7 @@ public void setValue(int value) { SHOW_PERFECT_HIT ("Perfect hits", "PerfectHit", "Shows perfect hit result bursts (300s, slider ticks).", true), SHOW_FOLLOW_POINTS ("Follow points", "FollowPoints", "Shows follow points between hit objects.", true), SHOW_HIT_ERROR_BAR ("Hit error bar", "ScoreMeter", "Shows precisely how accurate you were with each hit.", false), + ALWAYS_SHOW_KEY_OVERLAY ("Always show key overlay", "KeyOverlay", "Show the key overlay when playing instead of only on replays.", false), LOAD_HD_IMAGES ("Load HD images", "LoadHDImages", String.format("Loads HD (%s) images when available.\nIncreases memory usage and loading times.", GameImage.HD_SUFFIX), true), FIXED_CS ("Fixed CS", "FixedCS", "Determines the size of circles and sliders.", 0, 0, 100) { @Override @@ -1290,6 +1291,12 @@ public static boolean setCheckpoint(int time) { */ public static boolean isHitErrorBarEnabled() { return GameOption.SHOW_HIT_ERROR_BAR.getBooleanValue(); } + /** + * Returns whether or not to show the key overlay on non-replay game sessions. + * @return true if enabled + */ + public static boolean alwaysShowKeyOverlay() { return GameOption.ALWAYS_SHOW_KEY_OVERLAY.getBooleanValue(); } + /** * Returns whether or not to load HD (@2x) images. * @return true if HD images are enabled, false if only SD images should be loaded diff --git a/src/itdelatrisu/opsu/skins/Skin.java b/src/itdelatrisu/opsu/skins/Skin.java index 48c91332..f2655628 100644 --- a/src/itdelatrisu/opsu/skins/Skin.java +++ b/src/itdelatrisu/opsu/skins/Skin.java @@ -73,6 +73,9 @@ public class Skin { /** The default color of the stars that fall from the cursor during breaks. */ private static final Color DEFAULT_STAR_BREAK_ADDITIVE = new Color(255, 182, 193); + /** The default color of the text on the input overlay. */ + private static final Color DEFAULT_INPUT_OVERLAY_TEXT = new Color(0, 0, 0); + /** The skin directory. */ private File dir; @@ -165,6 +168,9 @@ public class Skin { /** The color of the stars that fall from the cursor (star2 sprite) in breaks. */ protected Color starBreakAdditive = DEFAULT_STAR_BREAK_ADDITIVE; + /** The color of the text on the input overlay. */ + protected Color inputOverlayText = DEFAULT_INPUT_OVERLAY_TEXT; + /** * [Fonts] */ @@ -344,6 +350,11 @@ public Skin(File dir) { */ public Color getStarBreakAdditiveColor() { return starBreakAdditive; } + /** + * Returns the color of the text on the input overlay. + */ + public Color getInputOverlayText() { return inputOverlayText; } + /** * Returns the prefix for the hit circle font sprites. */ diff --git a/src/itdelatrisu/opsu/skins/SkinLoader.java b/src/itdelatrisu/opsu/skins/SkinLoader.java index 49db3e82..ae26a51a 100644 --- a/src/itdelatrisu/opsu/skins/SkinLoader.java +++ b/src/itdelatrisu/opsu/skins/SkinLoader.java @@ -212,6 +212,9 @@ public static Skin loadSkin(File dir) { case "StarBreakAdditive": skin.starBreakAdditive = color; break; + case "InputOverlayText": + skin.inputOverlayText = color; + break; default: break; } diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 22051a41..b5aa35e7 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -52,6 +52,7 @@ import itdelatrisu.opsu.replay.ReplayFrame; import itdelatrisu.opsu.ui.Colors; import itdelatrisu.opsu.ui.Fonts; +import itdelatrisu.opsu.ui.InputOverlayKey; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.StarStream; import itdelatrisu.opsu.ui.UI; @@ -325,6 +326,9 @@ public enum PlayState { /** The single merged slider (if enabled). */ private FakeCombinedCurve mergedSlider; + /** The objects holding data for the input overlay. */ + private InputOverlayKey[] inputOverlayKeys; + /** Music position bar background colors. */ private static final Color MUSICBAR_NORMAL = new Color(12, 9, 10, 0.25f), @@ -339,6 +343,12 @@ public enum PlayState { public Game(int state) { this.state = state; + inputOverlayKeys = new InputOverlayKey[] { + new InputOverlayKey("K1", ReplayFrame.KEY_K1, 0, new Color(248, 216, 0)), + new InputOverlayKey("K2", ReplayFrame.KEY_K2, 0, new Color(248, 216, 0)), + new InputOverlayKey("M1", ReplayFrame.KEY_M1, 4, new Color(248, 0, 158)), + new InputOverlayKey("M2", ReplayFrame.KEY_M2, 8, new Color(248, 0, 158)), + }; } @Override @@ -725,6 +735,23 @@ else if (breakIndex > 1) { } } + // key overlay + if (isReplay || Options.alwaysShowKeyOverlay()) { + final float BTNSIZE = container.getHeight() * 0.0615f; + int x = (int) (container.getWidth() - BTNSIZE / 2f); + int y = (int) (container.getHeight() / 2f - BTNSIZE - BTNSIZE / 2f); + Image bg = GameImage.INPUTOVERLAY_BACKGROUND.getImage(); + bg = bg.getScaledCopy(BTNSIZE * 4.3f / bg.getWidth()); + bg.rotate(90f); + bg.drawCentered(container.getWidth() - bg.getHeight() / 2, container.getHeight() / 2); + Image keyimg = + GameImage.INPUTOVERLAY_KEY.getImage().getScaledCopy((int) BTNSIZE, (int) BTNSIZE); + for (int i = 0; i < 4; i++) { + inputOverlayKeys[i].render(g, x, y, keyimg); + y += BTNSIZE; + } + } + // returning from pause screen if (pauseTime > -1 && pausedMousePosition != null) { // darken the screen @@ -904,6 +931,16 @@ else if (!gameFinished) { } } + // update key overlay + if (isReplay || Options.alwaysShowKeyOverlay()) { + for (int i = 0; i < 4; i++) { + int keys = autoMousePressed ? 1 : lastKeysPressed; + boolean countpresses = breakTime == 0 && !isLeadIn() && + trackPosition > firstObjectTime; + inputOverlayKeys[i].update(keys, countpresses, delta); + } + } + lastTrackPosition = trackPosition; // update in-game scoreboard @@ -1443,6 +1480,11 @@ public void enter(GameContainer container, StateBasedGame game) // restart the game if (playState != PlayState.NORMAL) { + // reset key states + lastKeysPressed = 0; + for (int i = 0; i < 4; i++) + inputOverlayKeys[i].reset(); + // update play stats if (playState == PlayState.FIRST_LOAD) { beatmap.incrementPlayCounter(); diff --git a/src/itdelatrisu/opsu/ui/InputOverlayKey.java b/src/itdelatrisu/opsu/ui/InputOverlayKey.java new file mode 100644 index 00000000..99877b19 --- /dev/null +++ b/src/itdelatrisu/opsu/ui/InputOverlayKey.java @@ -0,0 +1,124 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014-2017 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.options.Options; +import org.newdawn.slick.Color; +import org.newdawn.slick.Graphics; +import org.newdawn.slick.Image; + +public class InputOverlayKey { + /** Time, in ms, of the shrink/expand key animation. */ + private static final int ANIMTIME = 100; + /** The final scale of the input keys when the key is pressed. */ + private static final float ACTIVESCALE = 0.75f; + + /** The bits in the keystate that corresponds to this key. */ + private final int targetKey; + /** The bits that may not be set for this key to be down. */ + private final int ignoredKey; + /** The color of the button when the key is down. */ + private final Color activeColor; + /** The initial text of the button. */ + private final String initialText; + + /** How long the key has been down, used for the scale animation. */ + private int downtime; + /** Whether or not this key is currently down */ + private boolean down; + /** The text that will be displayed on this button.*/ + private String text; + /** The amount of times this key has been pressed. */ + private int presses; + + /** + * @param initialText The initial text of the button + * @param targetKey The bits in the keystate that corresponds to this key + * @param ignoredKey The bits that may not be set for this key to be down + * @param activeColor The color of the button when the key is down + */ + public InputOverlayKey(String initialText, int targetKey, int ignoredKey, Color activeColor) { + this.initialText = initialText; + this.targetKey = targetKey; + this.ignoredKey = ignoredKey; + this.activeColor = activeColor; + } + + /** + * Resets all data + */ + public void reset() { + down = false; + downtime = 0; + presses = 0; + text = initialText; + } + + /** + * Update this key + * @param keystates the current key states + * @param delta framedelta + */ + public void update(int keystates, boolean countkeys, int delta) { + boolean wasdown = down; + down = (keystates & targetKey) == targetKey && (keystates & ignoredKey) == 0; + if (!wasdown && down) { + if (countkeys) + presses++; + text = Integer.toString(presses); + } + if (down && downtime < ANIMTIME) + downtime = Math.min(ANIMTIME, downtime + delta); + else if (!down && downtime > 0) + downtime = Math.max(0, downtime - delta); + } + + /** + * Render this key + * @param g graphics context + * @param x x position + * @param y y position + * @param baseImage the key image + */ + public void render(Graphics g, int x, int y, Image baseImage) { + g.pushTransform(); + float scale = 1f; + if (downtime > 0) { + float progress = downtime / (float) ANIMTIME; + scale -= (1f - ACTIVESCALE) * progress; + g.scale(scale, scale); + x /= scale; + y /= scale; + } + baseImage.drawCentered(x, y, down ? activeColor : Color.white); + x -= Fonts.MEDIUMBOLD.getWidth(text) / 2; + y -= Fonts.MEDIUMBOLD.getLineHeight() / 2; + /* + // shadow + g.pushTransform(); + g.scale (1.1f, 1.1f); + float shadowx = x / 1.1f - Fonts.MEDIUMBOLD.getWidth(text) * 0.05f; + float shadowy = y / 1.1f - Fonts.MEDIUMBOLD.getLineHeight() * 0.05f; + Fonts.MEDIUMBOLD.drawString(shadowx, shadowy, text, Color.black); + g.popTransform(); + */ + Fonts.MEDIUMBOLD.drawString(x, y, text, Options.getSkin().getInputOverlayText()); + g.popTransform(); + } +}