diff --git a/src/client/java/minicraft/core/Game.java b/src/client/java/minicraft/core/Game.java index f2a97ab69..65fb7c970 100644 --- a/src/client/java/minicraft/core/Game.java +++ b/src/client/java/minicraft/core/Game.java @@ -10,8 +10,10 @@ import minicraft.saveload.Load; import minicraft.saveload.Version; import minicraft.screen.Display; +import minicraft.screen.AppToast; import minicraft.screen.ResourcePackDisplay; import minicraft.screen.TitleDisplay; +import minicraft.screen.Toast; import minicraft.util.Logging; import org.jetbrains.annotations.Nullable; @@ -30,7 +32,9 @@ protected Game() { public static InputHandler input; // Input used in Game, Player, and just about all the *Menu classes. public static Player player; - public static List notifications = new ArrayList<>(); + public static List inGameNotifications = new ArrayList<>(); + public static ArrayDeque inAppToasts = new ArrayDeque<>(); + public static ArrayDeque inGameToasts = new ArrayDeque<>(); // Canvas size is limited, so handled one by one public static int MAX_FPS; diff --git a/src/client/java/minicraft/core/Renderer.java b/src/client/java/minicraft/core/Renderer.java index 8d92bab9d..0f247491d 100644 --- a/src/client/java/minicraft/core/Renderer.java +++ b/src/client/java/minicraft/core/Renderer.java @@ -27,8 +27,10 @@ import minicraft.level.Level; import minicraft.screen.LoadingDisplay; import minicraft.screen.Menu; +import minicraft.screen.AppToast; import minicraft.screen.QuestsDisplay; import minicraft.screen.RelPos; +import minicraft.screen.Toast; import minicraft.screen.SignDisplayMenu; import minicraft.screen.TutorialDisplayHandler; import minicraft.screen.entry.ListEntry; @@ -36,6 +38,8 @@ import minicraft.util.Logging; import minicraft.util.Quest; import minicraft.util.Quest.QuestSeries; +import org.intellij.lang.annotations.MagicConstant; +import org.jetbrains.annotations.NotNull; import javax.imageio.ImageIO; @@ -55,6 +59,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.UnaryOperator; public class Renderer extends Game { private Renderer() { @@ -108,6 +113,7 @@ public static void initScreen() { hudSheet = new LinkedSprite(SpriteType.Gui, "hud"); canvas.createBufferStrategy(3); + appStatusBar.initialize(); } @@ -127,6 +133,13 @@ public static void render() { if (currentDisplay != null) // Renders menu, if present. currentDisplay.render(screen); + appStatusBar.render(); + + AppToast toast; + if ((toast = inAppToasts.peek()) != null) { + toast.render(screen); + } + if (!canvas.hasFocus()) renderFocusNagger(); // Calls the renderFocusNagger() method, which creates the "Click to Focus" message. @@ -185,6 +198,259 @@ public static void render() { } } + public static final AppStatusBar appStatusBar = new AppStatusBar(); + + public static class AppStatusBar { + private static final int DURATION_ON_UPDATE = 90; // 1.5s + + public final AppStatusElement HARDWARE_ACCELERATION_STATUS = new HardwareAccelerationElementStatus(); + public final AppStatusElement CONTROLLER_STATUS = new ControllerElementStatus(); + public final AppStatusElement INPUT_METHOD_STATUS = new InputMethodElementStatus(); + + private AppStatusBar() {} + + private int duration = 120; // Shows for 2 seconds initially. + + private void render() { + if (duration == 0) return; + MinicraftImage sheet = spriteLinker.getSheet(SpriteType.Gui, "app_status_bar"); // Obtains sheet. + + // Background + for (int x = 0; x < 12; ++x) { + for (int y = 0; y < 2; ++y) { + screen.render(Screen.w - 16 * 8 + x * 8, y * 8, x, y, 0, sheet); + } + } + + // Hardware Acceleration Status (width = 16) + HARDWARE_ACCELERATION_STATUS.render(Screen.w - 16 * 8 + 5, 2, sheet); + // Controller Status (width = 14) + CONTROLLER_STATUS.render(Screen.w - 16 * 8 + 21 - 1, 2, sheet); + // Input Method Status (width = 14) + INPUT_METHOD_STATUS.render(Screen.w - 16 * 8 + 35 - 1, 2, sheet); + } + + void tick() { + if (duration > 0) + duration--; + HARDWARE_ACCELERATION_STATUS.tick(); + CONTROLLER_STATUS.tick(); + INPUT_METHOD_STATUS.tick(); + } + + void show(int duration) { + this.duration = Math.max(this.duration, duration); + } + + private void onStatusUpdate() { + show(DURATION_ON_UPDATE); + } + + private void initialize() { + HARDWARE_ACCELERATION_STATUS.initialize(); + } + + public abstract class AppStatusElement { + // width == 16 - size * 2 + protected final int size; // 0: largest, 1: smaller, etc. (gradually) + + private AppStatusElement(int size) { + this.size = size; + } + + private static final int BLINK_PERIOD = 10; // 6 Hz + + private int durationUpdated = 0; + private boolean blinking = false; + private int blinkTick = 0; + + protected void render(int x, int y, MinicraftImage sheet) { + if (durationUpdated > 0) { + if (blinking) { + for (int xx = 0; xx < 2; ++xx) + screen.render(x + xx * 8, y, 10 + xx, 3 + size, 0, sheet); + } + } + } + + protected void tick() { + if (durationUpdated > 0) { + durationUpdated--; + if (blinkTick == 0) { + blinkTick = BLINK_PERIOD; + blinking = !blinking; + } else blinkTick--; + } + } + + protected void updateStatus() { + durationUpdated = DURATION_ON_UPDATE; + blinking = false; + blinkTick = 0; + onStatusUpdate(); + } + + public abstract void updateStatus(int status); + + public void notifyStatusIf(UnaryOperator<@NotNull Integer> operator) {} + + protected void initialize() {} + } + + public class HardwareAccelerationElementStatus extends AppStatusElement { + public static final int ACCELERATION_ON = 0; + public static final int ACCELERATION_OFF = 1; + + @MagicConstant(intValues = {ACCELERATION_ON, ACCELERATION_OFF}) + private int status; + + private HardwareAccelerationElementStatus() { + super(0); + } + + @Override + protected void initialize() { + status = Boolean.parseBoolean(System.getProperty("sun.java2d.opengl")) ? + ACCELERATION_ON : ACCELERATION_OFF; + } + + @Override + protected void render(int x, int y, MinicraftImage sheet) { + super.render(x, y, sheet); + if (status == ACCELERATION_ON) { + for (int xx = 0; xx < 2; ++xx) + screen.render(x + xx * 8, y, xx, 4, 0, sheet); + } else if (status == ACCELERATION_OFF) { + for (int xx = 0; xx < 2; ++xx) + screen.render(x + xx * 8, y, 2 + xx, 4, 0, sheet); + } + } + + @Override + public void updateStatus(int status) { + super.updateStatus(); + this.status = status; + } + } + + public class ControllerElementStatus extends AppStatusElement { + public static final int CONTROLLER_CONNECTED = 0; // Normal state, usable controller + public static final int CONTROLLER_UNAVAILABLE = 1; // Current controller becoming unusable + public static final int CONTROLLER_DISCONNECTED = 2; // System normal, no controller connected + public static final int CONTROLLER_PENDING = 3; // Temporary unusable or controller hanging/delaying + public static final int CONTROLLER_UNKNOWN = 4; // Unknown state or unknown controller (unsupported) + // TODO #614 tacking and referring this state + public static final int CONTROLLER_FUNCTION_UNAVAILABLE = 5; // System not available/malfunctioning +// public static final int CONTROLLER_UNDER_CONFIGURATION = 6; // Alternating 0 and 3 // Reserved + + @MagicConstant(intValues = {CONTROLLER_CONNECTED, CONTROLLER_UNAVAILABLE, CONTROLLER_DISCONNECTED, + CONTROLLER_PENDING, CONTROLLER_UNKNOWN, CONTROLLER_FUNCTION_UNAVAILABLE}) + private int status; + + private ControllerElementStatus() { + super(1); + status = CONTROLLER_DISCONNECTED; // Default state + } + + @Override + protected void render(int x, int y, MinicraftImage sheet) { + super.render(x, y, sheet); + if (status == CONTROLLER_CONNECTED) { + for (int xx = 0; xx < 2; ++xx) + screen.render(x + xx * 8, y, xx, 2, 0, sheet); + } else if (status == CONTROLLER_UNAVAILABLE) { + for (int xx = 0; xx < 2; ++xx) + screen.render(x + xx * 8, y, 2 + xx, 2, 0, sheet); + } else if (status == CONTROLLER_DISCONNECTED) { + for (int xx = 0; xx < 2; ++xx) + screen.render(x + xx * 8, y, 4 + xx, 2, 0, sheet); + } else if (status == CONTROLLER_PENDING) { + for (int xx = 0; xx < 2; ++xx) + screen.render(x + xx * 8, y, 6 + xx, 2, 0, sheet); + } else if (status == CONTROLLER_UNKNOWN) { + for (int xx = 0; xx < 2; ++xx) + screen.render(x + xx * 8, y, 8 + xx, 2, 0, sheet); + } else if (status == CONTROLLER_FUNCTION_UNAVAILABLE) { + for (int xx = 0; xx < 2; ++xx) + screen.render(x + xx * 8, y, 10 + xx, 2, 0, sheet); + } + } + + @Override + protected void tick() { + super.tick(); + // Reserved + } + + @Override + public void updateStatus(int status) { + super.updateStatus(); + this.status = status; + } + + @Override + public void notifyStatusIf(UnaryOperator<@NotNull Integer> operator) { + int status = this.status; + //noinspection MagicConstant + if ((status = operator.apply(status)) != this.status) + updateStatus(status); + } + } + + public class InputMethodElementStatus extends AppStatusElement { // Only 2 methods: keyboard and controller + public static final int INPUT_KEYBOARD_ONLY = 0; // Only keyboard is accepted + public static final int INPUT_CONTROLLER_PRIOR = 1; // Both accepted, but controller is used priorly + public static final int INPUT_KEYBOARD_PRIOR = 2; // Both accepted, but keyboard is used priorly + // Controller is enabled, but only keyboard can be used as controller is currently unusable. + public static final int INPUT_CONTROLLER_UNUSABLE = 3; + + @MagicConstant(intValues = {INPUT_KEYBOARD_ONLY, INPUT_CONTROLLER_PRIOR, + INPUT_KEYBOARD_PRIOR, INPUT_CONTROLLER_UNUSABLE}) + private int status; + + private InputMethodElementStatus() { + super(1); + status = INPUT_KEYBOARD_ONLY; + } + + @Override + protected void render(int x, int y, MinicraftImage sheet) { + super.render(x, y, sheet); + if (status == INPUT_KEYBOARD_ONLY) { + for (int xx = 0; xx < 2; ++xx) + screen.render(x + xx * 8, y, xx, 5, 0, sheet); + } else if (status == INPUT_CONTROLLER_PRIOR) { + for (int xx = 0; xx < 2; ++xx) + screen.render(x + xx * 8, y, 2 + xx, 5, 0, sheet); + } else if (status == INPUT_KEYBOARD_PRIOR) { + for (int xx = 0; xx < 2; ++xx) + screen.render(x + xx * 8, y, 4 + xx, 5, 0, sheet); + } else if (status == INPUT_CONTROLLER_UNUSABLE) { + for (int xx = 0; xx < 2; ++xx) + screen.render(x + xx * 8, y, 6 + xx, 5, 0, sheet); + } + } + + @Override + protected void tick() { + super.tick(); + if (status == INPUT_CONTROLLER_PRIOR || status == INPUT_KEYBOARD_PRIOR) { + if (!input.isControllerInUse()) + updateStatus(INPUT_CONTROLLER_UNUSABLE); + } else if (status == INPUT_CONTROLLER_UNUSABLE) { + if (input.isControllerInUse()) + updateStatus(INPUT_CONTROLLER_PRIOR); // As controller was enabled. + } + } + + @Override + public void updateStatus(int status) { + super.updateStatus(); + this.status = status; + } + } + } + private static void renderLevel() { Level level = levels[currentLevel]; @@ -269,18 +535,18 @@ private static void renderGui() { // NOTIFICATIONS Updater.updateNoteTick = false; - if (permStatus.size() == 0 && notifications.size() > 0) { + if (permStatus.size() == 0 && inGameNotifications.size() > 0) { Updater.updateNoteTick = true; - if (notifications.size() > 3) { // Only show 3 notifs max at one time; erase old notifs. - notifications = notifications.subList(notifications.size() - 3, notifications.size()); + if (inGameNotifications.size() > 3) { // Only show 3 notifs max at one time; erase old notifs. + inGameNotifications = inGameNotifications.subList(inGameNotifications.size() - 3, inGameNotifications.size()); } if (Updater.notetick > 180) { // Display time per notification. - notifications.remove(0); + inGameNotifications.remove(0); Updater.notetick = 0; } List print = new ArrayList<>(); - for (String n : notifications) { + for (String n : inGameNotifications) { for (String l : Font.getLines(n, Screen.w, Screen.h, 0)) print.add(l); } @@ -424,6 +690,11 @@ private static void renderGui() { renderQuestsDisplay(); if (signDisplayMenu != null) signDisplayMenu.render(screen); renderDebugInfo(); + + Toast toast; + if ((toast = inGameToasts.peek()) != null) { + toast.render(screen); + } } public static void renderBossbar(int length, String title) { diff --git a/src/client/java/minicraft/core/Updater.java b/src/client/java/minicraft/core/Updater.java index c0355e0f9..f885f9bd6 100644 --- a/src/client/java/minicraft/core/Updater.java +++ b/src/client/java/minicraft/core/Updater.java @@ -12,7 +12,9 @@ import minicraft.screen.Display; import minicraft.screen.EndGameDisplay; import minicraft.screen.LevelTransitionDisplay; +import minicraft.screen.AppToast; import minicraft.screen.PlayerDeathDisplay; +import minicraft.screen.Toast; import minicraft.screen.TutorialDisplayHandler; import minicraft.screen.WorldSelectDisplay; import minicraft.util.AdvancementElement; @@ -170,7 +172,20 @@ public static void tick() { scoreTime--; } + Renderer.appStatusBar.tick(); + if (input.getMappedKey("BACK_QUOTE").isDown()) + Renderer.appStatusBar.show(1); if (updateNoteTick) notetick++; + AppToast appToast; + if ((appToast = inAppToasts.peek()) != null) { + boolean refresh = true; + if (appToast.isExpired()) { + inAppToasts.pop(); // Removes + refresh = (appToast = inAppToasts.peek()) != null; // Tries getting new + } + + if (refresh) appToast.tick(); + } Sound.tick(); @@ -208,6 +223,16 @@ public static void tick() { } player.tick(); // Ticks the player when there's no menu. + Toast gameToast; + if ((gameToast = inGameToasts.peek()) != null) { + boolean refresh = true; + if (gameToast.isExpired()) { + inGameToasts.pop(); // Removes + refresh = (gameToast = inGameToasts.peek()) != null; // Tries getting new + } + + if (refresh) gameToast.tick(); + } if (level != null) { level.tick(true); @@ -340,7 +365,7 @@ public static void notifyAll(String msg) { public static void notifyAll(String msg, int notetick) { msg = Localization.getLocalized(msg); - notifications.add(msg); + inGameNotifications.add(msg); Updater.notetick = notetick; } } diff --git a/src/client/java/minicraft/core/WatcherThreadController.java b/src/client/java/minicraft/core/WatcherThreadController.java new file mode 100644 index 000000000..5943c7e1d --- /dev/null +++ b/src/client/java/minicraft/core/WatcherThreadController.java @@ -0,0 +1,18 @@ +package minicraft.core; + +/** + * This provides an ability to control over a watcher thread to ensure content synchronization and thread safety. + */ +public interface WatcherThreadController { + /** + * Suspends the watcher thread from running. + * @throws IllegalStateException if the thread has already been suspended + */ + void suspend(); + + /** + * Resumes the watcher thread from suspended. + * @throws IllegalStateException if the thread has already been resumed or not suspended + */ + void resume(); +} diff --git a/src/client/java/minicraft/core/World.java b/src/client/java/minicraft/core/World.java index c46195c54..bf8891980 100644 --- a/src/client/java/minicraft/core/World.java +++ b/src/client/java/minicraft/core/World.java @@ -81,7 +81,7 @@ public static void resetGame(boolean keepPlayer) { playerDeadTime = 0; currentLevel = 3; Updater.asTick = 0; - Updater.notifications.clear(); + Updater.inGameNotifications.clear(); // Adds a new player if (keepPlayer) { diff --git a/src/client/java/minicraft/core/io/InputHandler.java b/src/client/java/minicraft/core/io/InputHandler.java index 0123050d9..c534dde51 100644 --- a/src/client/java/minicraft/core/io/InputHandler.java +++ b/src/client/java/minicraft/core/io/InputHandler.java @@ -1,11 +1,13 @@ package minicraft.core.io; import com.badlogic.gdx.utils.SharedLibraryLoadRuntimeException; +import com.studiohartman.jamepad.Configuration; import com.studiohartman.jamepad.ControllerAxis; import com.studiohartman.jamepad.ControllerButton; import com.studiohartman.jamepad.ControllerIndex; import com.studiohartman.jamepad.ControllerManager; import com.studiohartman.jamepad.ControllerUnpluggedException; +import minicraft.core.Renderer; import minicraft.util.Logging; import org.jetbrains.annotations.Nullable; import org.tinylog.Logger; @@ -14,6 +16,7 @@ import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.lang.reflect.Field; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -51,12 +54,7 @@ public class InputHandler implements KeyListener { public String keyToChange = null; // This is used when listening to change key bindings. private String keyChanged = null; // This is used when listening to change key bindings. private boolean overwrite = false; - private final boolean controllersSupported; - private ControllerManager controllerManager; - private ControllerIndex controllerIndex; // Please prevent getting button states directly from this object. - private HashMap controllerButtonBooleanMapJust = new HashMap<>(); - private HashMap controllerButtonBooleanMap = new HashMap<>(); public String getChangedKey() { String key = keyChanged + ";" + keymap.get(keyChanged); @@ -88,12 +86,17 @@ public String getChangedKey() { keyNames.put(KeyEvent.VK_CONTROL, "CTRL"); } - private HashMap keymap; // The symbolic map of actions to physical key names. - private HashMap keyboard; // The actual map of key names to Key objects. + private final HashMap keymap; // The symbolic map of actions to physical key names. + private final HashMap keyboard; // The actual map of key names to Key objects. private String lastKeyTyped = ""; // Used for things like typing world names. private String keyTypedBuffer = ""; // Used to store the last key typed before putting it into the main var during tick(). - private final LastInputActivityListener lastInputActivityListener = new LastInputActivityListener(); + // This controller library currently does not support controller specification (customization) with settings. + private final ControllerManager controllerManager; + private final HashMap controllerKeys = new HashMap<>(); + private final @Nullable ControllerIndex controllerIndex; + private @Nullable Controller controller = null; + private boolean controllerEnabled = false; public InputHandler() { keymap = new LinkedHashMap<>(); // Stores custom key name with physical key name in keyboard. @@ -102,8 +105,7 @@ public InputHandler() { initKeyMap(); // This is seperate so I can make a "restore defaults" option. initButtonMap(); for (ControllerButton btn : ControllerButton.values()) { - controllerButtonBooleanMap.put(btn, false); - controllerButtonBooleanMapJust.put(btn, false); + controllerKeys.put(btn, new ControllerKey(btn)); } // I'm not entirely sure if this is necessary... but it doesn't hurt. @@ -112,21 +114,30 @@ public InputHandler() { keyboard.put("ALT", new PhysicalKey(true)); boolean controllerInit = false; + ControllerManager controllerManager = null; + ControllerIndex controllerIndex = null; try { - controllerManager = new ControllerManager(); + Configuration configuration = new Configuration(); + configuration.maxNumControllers = 1; // Only handles one controller connection at the same time. + controllerManager = new ControllerManager(configuration); controllerManager.initSDLGamepad(); - controllerIndex = controllerManager.getControllerIndex(0); controllerManager.update(); - try { - Logging.CONTROLLER.debug("Controller Detected: " + controllerManager.getControllerIndex(0).getName()); - } catch (ControllerUnpluggedException e) { - Logging.CONTROLLER.debug("No Controllers Detected, moving on."); - } + controllerIndex = controllerManager.getControllerIndex(0); controllerInit = true; } catch (IllegalStateException | SharedLibraryLoadRuntimeException | UnsatisfiedLinkError e) { Logging.CONTROLLER.error(e, "Controllers are not support, being disabled."); } + this.controllerManager = controllerManager; + this.controllerIndex = controllerIndex; controllersSupported = controllerInit; + if (controllerInit) { + searchController(true); + controllerPortWatcher.start(); + } + + if (!controllersSupported) + Renderer.appStatusBar.CONTROLLER_STATUS.updateStatus( + Renderer.AppStatusBar.ControllerElementStatus.CONTROLLER_FUNCTION_UNAVAILABLE); } public InputHandler(Component inputSource) { @@ -134,6 +145,110 @@ public InputHandler(Component inputSource) { inputSource.addKeyListener(this); // Add key listener to game } + // Searches if there is any controller connected. + private void searchController(boolean initial) { + if (controllerIndex == null) return; + if (controllerIndex.isConnected()) { + if (controller == null) { + controller = new Controller(controllerIndex); + Renderer.appStatusBar.CONTROLLER_STATUS.updateStatus( + Renderer.AppStatusBar.ControllerElementStatus.CONTROLLER_CONNECTED); + testController(); + } // else no effect + } else { + controller = null; + if (!initial) Renderer.appStatusBar.CONTROLLER_STATUS.updateStatus( + Renderer.AppStatusBar.ControllerElementStatus.CONTROLLER_DISCONNECTED); + } + } + + private void testController() { + if (isControllerInUse()) { + controller.doVibration(.25F, .25F, 400); + } + } + + public boolean isControllerEnabled() { + return controllerEnabled; + } + + public void setControllerEnabled(boolean controllerEnabled) { + if (this.controllerEnabled ^ controllerEnabled) { // If there is a change in configuration + this.controllerEnabled = controllerEnabled; + Renderer.appStatusBar.INPUT_METHOD_STATUS.updateStatus(controllerEnabled ? + Renderer.AppStatusBar.InputMethodElementStatus.INPUT_CONTROLLER_PRIOR : + Renderer.AppStatusBar.InputMethodElementStatus.INPUT_KEYBOARD_ONLY); + testController(); + } + } + + private void handleControllerUnplugged() { + Renderer.appStatusBar.CONTROLLER_STATUS.updateStatus( + Renderer.AppStatusBar.ControllerElementStatus.CONTROLLER_UNAVAILABLE); + } + + private void handleControllerNormal() { + Renderer.appStatusBar.CONTROLLER_STATUS.notifyStatusIf(status -> + status == Renderer.AppStatusBar.ControllerElementStatus.CONTROLLER_UNAVAILABLE ? + Renderer.AppStatusBar.ControllerElementStatus.CONTROLLER_CONNECTED : status); + } + + private class Controller { + private final ControllerIndex index; + + public Controller(ControllerIndex index) { + this.index = index; + } + + public boolean isButtonPressed(ControllerButton button) { + try { + boolean v = index.isButtonPressed(button); + handleControllerNormal(); + return v; + } catch (ControllerUnpluggedException e) { + handleControllerUnplugged(); + return false; + } + } + + public boolean isButtonJustPressed(ControllerButton button) { + try { + boolean v = index.isButtonJustPressed(button); + handleControllerNormal(); + return v; + } catch (ControllerUnpluggedException e) { + handleControllerUnplugged(); + return false; + } + } + + public boolean doVibration(float leftMagnitude, float rightMagnitude, int duration_ms) { + try { + boolean v = index.doVibration(leftMagnitude, rightMagnitude, duration_ms); + handleControllerNormal(); + return v; + } catch (ControllerUnpluggedException e) { + handleControllerUnplugged(); + return false; + } + } + + public float getAxisState(ControllerAxis axis) { + try { + float v = index.getAxisState(axis); + handleControllerNormal(); + return v; + } catch (ControllerUnpluggedException e) { + handleControllerUnplugged(); + return 0; + } + } + } + + public final boolean isControllerInUse() { + return controllersSupported && controllerEnabled && controller != null; + } + private void initKeyMap() { keymap.put("MOVE-UP", "UP|W"); keymap.put("MOVE-DOWN", "DOWN|S"); @@ -219,26 +334,29 @@ public void tick() { keyTypedBuffer = ""; inputMask = null; synchronized ("lock") { - for (PhysicalKey key : keyboard.values()) - key.tick(); // Call tick() for each key. + // Call tick() for each key. + keyboard.values().forEach(PhysicalKey::tick); + } + + synchronized (controllerPortEvents) { + ControllerPortEvent event; + while ((event = controllerPortEvents.poll()) != null) { + switch (event) { + case PLUGGED: + searchController(false); + break; + case UNPLUGGED: + controller = null; + Renderer.appStatusBar.CONTROLLER_STATUS.updateStatus( + Renderer.AppStatusBar.ControllerElementStatus.CONTROLLER_DISCONNECTED); + break; + } + } } - lastInputActivityListener.tick(); - // Also update the controller button state. if (controllersSupported) { - for (ControllerButton btn : ControllerButton.values()) { - try { - controllerButtonBooleanMapJust.put(btn, controllerIndex.isButtonJustPressed(btn)); - } catch (ControllerUnpluggedException e) { - controllerButtonBooleanMapJust.put(btn, false); - } - try { - controllerButtonBooleanMap.put(btn, controllerIndex.isButtonPressed(btn)); - } catch (ControllerUnpluggedException e) { - controllerButtonBooleanMap.put(btn, false); - } - } + controllerKeys.values().forEach(ControllerKey::tick); } if (leftTriggerCooldown > 0) leftTriggerCooldown--; @@ -327,6 +445,35 @@ public String toString() { } } + private class ControllerKey extends Key { + private final ControllerButton button; + + private boolean down, clicked; + + public ControllerKey(ControllerButton button) { + this.button = button; + } + + public void tick() { + clicked = false; + down = false; + if (isControllerInUse()) { + clicked = controller.isButtonJustPressed(button); + down = controller.isButtonPressed(button); + } + } + + @Override + public boolean isDown() { + return down; + } + + @Override + public boolean isClicked() { + return clicked; + } + } + private static class CompoundedKey extends Key { private final HashSet keys; @@ -402,7 +549,7 @@ public void setKey(String keymapKey, String keyboardKey) { */ public String getMapping(String actionKey) { actionKey = actionKey.toUpperCase(); - if (lastInputActivityListener.lastButtonActivityTimestamp > lastInputActivityListener.lastKeyActivityTimestamp) { + if (isControllerInUse()) { if (buttonMap.containsKey(actionKey)) return buttonMap.get(actionKey).toString().replace("_", "-"); } @@ -414,13 +561,13 @@ public String getMapping(String actionKey) { } /** - * Returning the corresponding mapping depends on the device last acted. + * Returning the corresponding mapping depends on the device set. * @param keyMap The keyboard mapping. * @param buttonMap The controller mapping * @return The selected mapping. */ public String selectMapping(String keyMap, String buttonMap) { - if (lastInputActivityListener.lastButtonActivityTimestamp > lastInputActivityListener.lastKeyActivityTimestamp) + if (isControllerInUse()) return buttonMap; else return keyMap; @@ -431,7 +578,7 @@ public String selectMapping(String keyMap, String buttonMap) { * @return The input device type: 0 for keyboard, 1 for controller. */ public int getLastInputType() { - if (lastInputActivityListener.lastButtonActivityTimestamp > lastInputActivityListener.lastKeyActivityTimestamp) + if (isControllerInUse()) return 1; else return 0; @@ -450,7 +597,7 @@ public Key getMappedKey(String keyText) { if (keyText.contains("|")) { /// Multiple key possibilities exist for this action; so, combine the results of each one! ArrayList keys = new ArrayList<>(); - for (String keyposs : keyText.split("\\|")) { // String.split() uses regex, and "|" is a special character, so it must be escaped; but the backslash must be passed in, so it needs escaping. + for (String keyposs: keyText.split("\\|")) { // String.split() uses regex, and "|" is a special character, so it must be escaped; but the backslash must be passed in, so it needs escaping. // It really does combine using "or": keys.add(getMappedKey(keyposs)); } @@ -469,7 +616,6 @@ public Key getMappedKey(String keyText) { //if(key.clicked && Game.debug) System.out.println("Processed key: " + keytext + " is clicked; tickNum=" + ticks); return new CompoundedKey(keys); // Return the Key object. } - // Physical keys only private Key getKey(String keytext) { // If the passed-in key is blank, or null, then return null. @@ -704,26 +850,12 @@ public static String handleBackspaceChars(String typing, boolean keepUnevaluated return builder.toString(); } - public boolean anyControllerConnected() { - return controllersSupported && controllerManager.getNumControllers() > 0; - } - public boolean buttonPressed(ControllerButton button) { - return controllerButtonBooleanMapJust.get(button); + return controllerKeys.get(button).isClicked(); } public boolean buttonDown(ControllerButton button) { - return controllerButtonBooleanMap.get(button); - } - - public ArrayList getAllPressedButtons() { - ArrayList btnList = new ArrayList<>(); - for (ControllerButton btn : ControllerButton.values()) { - if (controllerButtonBooleanMap.get(btn)) - btnList.add(btn); - } - - return btnList; + return controllerKeys.get(button).isDown(); } public boolean inputPressed(String mapping) { @@ -746,12 +878,8 @@ public boolean inputDown(String mapping) { * @return Whether or not the controller was able to be vibrated (i.e. if haptics are supported) or controller not connected. */ public boolean controllerVibration(float leftMagnitude, float rightMagnitude, int duration_ms) { - if (controllersSupported) { - try { - return controllerIndex.doVibration(leftMagnitude, rightMagnitude, duration_ms); - } catch (ControllerUnpluggedException ignored) { - return false; - } + if (isControllerInUse()) { + return controller.doVibration(leftMagnitude, rightMagnitude, duration_ms); } else return false; } @@ -759,42 +887,64 @@ public boolean controllerVibration(float leftMagnitude, float rightMagnitude, in private int rightTriggerCooldown = 0; public boolean leftTriggerPressed() { - if (controllersSupported) { - try { - if (leftTriggerCooldown == 0 && controllerIndex.getAxisState(ControllerAxis.TRIGGERLEFT) > 0.5) { - leftTriggerCooldown = 8; - return true; - } else - return false; - } catch (ControllerUnpluggedException e) { - return false; - } - } else return false; + if (leftTriggerCooldown == 0 && isControllerInUse() && + controller.getAxisState(ControllerAxis.TRIGGERLEFT) > 0.5) { + leftTriggerCooldown = 8; + return true; + } else + return false; } public boolean rightTriggerPressed() { - if (controllersSupported) { - try { - if (rightTriggerCooldown == 0 && controllerIndex.getAxisState(ControllerAxis.TRIGGERRIGHT) > 0.5) { - rightTriggerCooldown = 8; - return true; - } else - return false; - } catch (ControllerUnpluggedException e) { - return false; - } - } else return false; + if (rightTriggerCooldown == 0 && isControllerInUse() && + controller.getAxisState(ControllerAxis.TRIGGERRIGHT) > 0.5) { + rightTriggerCooldown = 8; + return true; + } else + return false; } - private class LastInputActivityListener { - public long lastKeyActivityTimestamp = 0; - public long lastButtonActivityTimestamp = 0; + private final ArrayDeque controllerPortEvents = new ArrayDeque<>(); + // Watcher should be terminated when application terminates. + private final ControllerPortWatcher controllerPortWatcher = new ControllerPortWatcher(); - public void tick() { - if (getAllPressedKeys().size() > 0) - lastKeyActivityTimestamp = System.currentTimeMillis(); - if (getAllPressedButtons().size() > 0) - lastButtonActivityTimestamp = System.currentTimeMillis(); + private enum ControllerPortEvent { + PLUGGED, UNPLUGGED + } + + private class ControllerPortWatcher extends Thread { + public ControllerPortWatcher() { + super("Controller Port Watcher"); + } + + @Override + public void run() { + if (controllerIndex == null) return; + //noinspection InfiniteLoopStatement + while (true) { + while (true) { // Ensures that the query is empty and thus synchronized. + synchronized (controllerPortEvents) { + if (controllerPortEvents.isEmpty()) break; + } + try { // Prevents unnecessary dry-running and using up of excessive CPU time. + //noinspection BusyWait + Thread.sleep(50); + } catch (InterruptedException ignored) {} + } + + synchronized (controllerPortEvents) { + boolean connected = controllerIndex.isConnected(); + controllerManager.update(); + if (connected ^ controllerIndex.isConnected()) + controllerPortEvents.add(connected ? ControllerPortEvent.UNPLUGGED : + ControllerPortEvent.PLUGGED); + } + + try { + //noinspection BusyWait + Thread.sleep(100); + } catch (InterruptedException ignored) {} + } } } } diff --git a/src/client/java/minicraft/entity/furniture/Bed.java b/src/client/java/minicraft/entity/furniture/Bed.java index f9515c00a..7d8f509b0 100644 --- a/src/client/java/minicraft/entity/furniture/Bed.java +++ b/src/client/java/minicraft/entity/furniture/Bed.java @@ -48,7 +48,7 @@ public static boolean checkCanSleep(Player player) { // It is too early to sleep; display how much time is remaining. int sec = (int) Math.ceil((Updater.sleepStartTime - Updater.tickCount) * 1.0 / Updater.normSpeed); // gets the seconds until sleeping is allowed. // normSpeed is in tiks/sec. String note = Localization.getLocalized("minicraft.notification.cannot_sleep", sec / 60, sec % 60); - Game.notifications.add(note); // Add the notification displaying the time remaining in minutes and seconds. + Game.inGameNotifications.add(note); // Add the notification displaying the time remaining in minutes and seconds. return false; } diff --git a/src/client/java/minicraft/entity/furniture/DeathChest.java b/src/client/java/minicraft/entity/furniture/DeathChest.java index a1fb4d030..577b3bc2f 100644 --- a/src/client/java/minicraft/entity/furniture/DeathChest.java +++ b/src/client/java/minicraft/entity/furniture/DeathChest.java @@ -104,7 +104,7 @@ public void touchedBy(Entity other) { Inventory playerInv = ((Player) other).getInventory(); for (Item i : inventory.getItems()) { if (playerInv.add(i) != null) { - Game.notifications.add("Your inventory is full!"); + Game.inGameNotifications.add("Your inventory is full!"); return; } @@ -112,7 +112,7 @@ public void touchedBy(Entity other) { } remove(); - Game.notifications.add(Localization.getLocalized("minicraft.notification.death_chest_retrieved")); + Game.inGameNotifications.add(Localization.getLocalized("minicraft.notification.death_chest_retrieved")); } } diff --git a/src/client/java/minicraft/entity/furniture/KnightStatue.java b/src/client/java/minicraft/entity/furniture/KnightStatue.java index e2a6f5219..950759ab7 100644 --- a/src/client/java/minicraft/entity/furniture/KnightStatue.java +++ b/src/client/java/minicraft/entity/furniture/KnightStatue.java @@ -22,10 +22,10 @@ public KnightStatue(int health) { public boolean interact(Player player, Item heldItem, Direction attackDir) { if (!ObsidianKnight.active) { if (touches == 0) { // Touched the first time. - Game.notifications.add(Localization.getLocalized("minicraft.notifications.statue_tapped")); + Game.inGameNotifications.add(Localization.getLocalized("minicraft.notifications.statue_tapped")); touches++; } else if (touches == 1) { // Touched the second time. - Game.notifications.add(Localization.getLocalized("minicraft.notifications.statue_touched")); + Game.inGameNotifications.add(Localization.getLocalized("minicraft.notifications.statue_touched")); touches++; } else { // Touched the third time. // Awoken notifications is in Boss class @@ -37,7 +37,7 @@ public boolean interact(Player player, Item heldItem, Direction attackDir) { return true; } else { // The boss is active. - Game.notifications.add(Localization.getLocalized("minicraft.notification.boss_limit")); + Game.inGameNotifications.add(Localization.getLocalized("minicraft.notification.boss_limit")); return false; } } diff --git a/src/client/java/minicraft/entity/mob/Player.java b/src/client/java/minicraft/entity/mob/Player.java index 385e78052..8408a3174 100644 --- a/src/client/java/minicraft/entity/mob/Player.java +++ b/src/client/java/minicraft/entity/mob/Player.java @@ -55,6 +55,7 @@ import minicraft.screen.WorldSelectDisplay; import minicraft.util.AdvancementElement; import minicraft.util.Logging; +import minicraft.util.MyUtils; import minicraft.util.Vector2; import org.jetbrains.annotations.Nullable; @@ -760,7 +761,7 @@ private void goFishing() { } if (itemData.startsWith(";")) { // For secret messages :=) - Game.notifications.add(itemData.substring(1)); + Game.inGameNotifications.add(itemData.substring(1)); } else { if (Items.get(itemData).equals(Items.get("Raw Fish"))) { AchievementsDisplay.setAchievement("minicraft.achievement.fish", true); @@ -1170,10 +1171,10 @@ protected void doHurt(int damage, Direction attackDir) { if (Game.isMode("minicraft.settings.mode.creative") || hurtTime > 0 || Bed.inBed(this)) return; // Can't get hurt in creative, hurt cooldown, or while someone is in bed - int healthDam = 0, armorDam = 0; + int healthDam = 0, armorDam = 0, suffered = 0; if (this == Game.player) { if (curArmor == null) { // No armor - healthDam = damage; // Subtract that amount + suffered = healthDam = damage; // Subtract that amount } else { // Has armor armorDamageBuffer += damage; armorDam += damage; @@ -1204,6 +1205,8 @@ protected void doHurt(int damage, Direction attackDir) { Sound.play("playerhurt"); hurtTime = playerHurtTime; + suffered = MyUtils.clamp(suffered, 1, 5); + Game.input.controllerVibration(suffered * .1F, suffered * .1F, playerHurtTime * 1000 / 60); } /** diff --git a/src/client/java/minicraft/gfx/Color.java b/src/client/java/minicraft/gfx/Color.java index c5f0b6fb4..e699c1eab 100644 --- a/src/client/java/minicraft/gfx/Color.java +++ b/src/client/java/minicraft/gfx/Color.java @@ -36,6 +36,7 @@ public class Color { public static final int YELLOW = Color.get(1, 255, 255, 0); public static final int MAGENTA = Color.get(1, 255, 0, 255); public static final int CYAN = Color.get(1, 90, 204, 204); + public static final int GOLD = Color.get(1, 255, 193, 37); public static final char COLOR_CHAR = '\u00A7'; diff --git a/src/client/java/minicraft/item/FishingRodItem.java b/src/client/java/minicraft/item/FishingRodItem.java index 957ed1017..341611de3 100644 --- a/src/client/java/minicraft/item/FishingRodItem.java +++ b/src/client/java/minicraft/item/FishingRodItem.java @@ -77,7 +77,7 @@ public boolean canAttack() { @Override public boolean isDepleted() { if (random.nextInt(100) > 120 - uses + level * 6) { // Breaking is random, the lower the level, and the more times you use it, the higher the chance - Game.notifications.add("Your Fishing rod broke."); + Game.inGameNotifications.add("Your Fishing rod broke."); return true; } return false; diff --git a/src/client/java/minicraft/item/PotionType.java b/src/client/java/minicraft/item/PotionType.java index 67714a2be..704805962 100644 --- a/src/client/java/minicraft/item/PotionType.java +++ b/src/client/java/minicraft/item/PotionType.java @@ -40,7 +40,7 @@ public boolean toggleEffect(Player player, boolean addEffect) { if (playerDepth == 0) { // player is in overworld - Game.notifications.add("You can't escape from here!"); + Game.inGameNotifications.add("You can't escape from here!"); return false; } diff --git a/src/client/java/minicraft/item/SummonItem.java b/src/client/java/minicraft/item/SummonItem.java index bf0d73df0..21269a142 100644 --- a/src/client/java/minicraft/item/SummonItem.java +++ b/src/client/java/minicraft/item/SummonItem.java @@ -60,10 +60,10 @@ public boolean interactOn(Tile tile, Level level, int xt, int yt, Player player, success = true; } } else { - Game.notifications.add(Localization.getLocalized("minicraft.notification.boss_limit")); + Game.inGameNotifications.add(Localization.getLocalized("minicraft.notification.boss_limit")); } } else { - Game.notifications.add(Localization.getLocalized("minicraft.notification.wrong_level_sky")); + Game.inGameNotifications.add(Localization.getLocalized("minicraft.notification.wrong_level_sky")); } break; @@ -89,16 +89,16 @@ public boolean interactOn(Tile tile, Level level, int xt, int yt, Player player, success = true; } } else { - Game.notifications.add(Localization.getLocalized("minicraft.notification.knight_statue_exists")); + Game.inGameNotifications.add(Localization.getLocalized("minicraft.notification.knight_statue_exists")); } } else { - Game.notifications.add(Localization.getLocalized("minicraft.notification.boss_limit")); + Game.inGameNotifications.add(Localization.getLocalized("minicraft.notification.boss_limit")); } } else { - Game.notifications.add(Localization.getLocalized("minicraft.notification.spawn_on_boss_tile")); + Game.inGameNotifications.add(Localization.getLocalized("minicraft.notification.spawn_on_boss_tile")); } } else { - Game.notifications.add(Localization.getLocalized("minicraft.notification.wrong_level_dungeon")); + Game.inGameNotifications.add(Localization.getLocalized("minicraft.notification.wrong_level_dungeon")); } break; default: diff --git a/src/client/java/minicraft/item/TileItem.java b/src/client/java/minicraft/item/TileItem.java index 4c4ea0d3e..a25b2f906 100644 --- a/src/client/java/minicraft/item/TileItem.java +++ b/src/client/java/minicraft/item/TileItem.java @@ -172,7 +172,7 @@ public boolean interactOn(Tile tile, Level level, int xt, int yt, Player player, } if (note.length() > 0) { - Game.notifications.add(note); + Game.inGameNotifications.add(note); } } diff --git a/src/client/java/minicraft/level/tile/BossDoorTile.java b/src/client/java/minicraft/level/tile/BossDoorTile.java index 581030dd5..700294d88 100644 --- a/src/client/java/minicraft/level/tile/BossDoorTile.java +++ b/src/client/java/minicraft/level/tile/BossDoorTile.java @@ -24,7 +24,7 @@ public boolean interact(Level level, int xt, int yt, Player player, Item item, D ToolItem tool = (ToolItem) item; if (tool.type == type.getRequiredTool()) { if (player.payStamina(1)) { - Game.notifications.add(Localization.getLocalized(doorMsg)); + Game.inGameNotifications.add(Localization.getLocalized(doorMsg)); Sound.play("monsterhurt"); return true; } @@ -41,7 +41,7 @@ public boolean interact(Level level, int xt, int yt, Player player, Item item, D public boolean hurt(Level level, int x, int y, Mob source, int dmg, Direction attackDir) { if (source instanceof Player) { if (ObsidianKnight.active && !Game.isMode("minicraft.settings.mode.creative")) { - Game.notifications.add(doorMsg); + Game.inGameNotifications.add(doorMsg); return true; } } diff --git a/src/client/java/minicraft/level/tile/BossFloorTile.java b/src/client/java/minicraft/level/tile/BossFloorTile.java index b7ac654c5..0624a9a82 100644 --- a/src/client/java/minicraft/level/tile/BossFloorTile.java +++ b/src/client/java/minicraft/level/tile/BossFloorTile.java @@ -23,7 +23,7 @@ public boolean interact(Level level, int xt, int yt, Player player, Item item, D ToolItem tool = (ToolItem) item; if (tool.type == type.getRequiredTool()) { if (player.payStamina(1)) { - Game.notifications.add(Localization.getLocalized(floorMsg)); + Game.inGameNotifications.add(Localization.getLocalized(floorMsg)); Sound.play("monsterhurt"); return true; } diff --git a/src/client/java/minicraft/level/tile/BossWallTile.java b/src/client/java/minicraft/level/tile/BossWallTile.java index 54c2d7f82..04bf03a8d 100644 --- a/src/client/java/minicraft/level/tile/BossWallTile.java +++ b/src/client/java/minicraft/level/tile/BossWallTile.java @@ -29,7 +29,7 @@ public boolean interact(Level level, int xt, int yt, Player player, Item item, D ToolItem tool = (ToolItem) item; if (tool.type == type.getRequiredTool()) { if (player.payStamina(1)) { - Game.notifications.add(Localization.getLocalized(wallMsg)); + Game.inGameNotifications.add(Localization.getLocalized(wallMsg)); Sound.play("monsterhurt"); return true; } diff --git a/src/client/java/minicraft/level/tile/HardRockTile.java b/src/client/java/minicraft/level/tile/HardRockTile.java index 84bc8f2b5..af3de788e 100644 --- a/src/client/java/minicraft/level/tile/HardRockTile.java +++ b/src/client/java/minicraft/level/tile/HardRockTile.java @@ -55,7 +55,7 @@ public boolean interact(Level level, int xt, int yt, Player player, Item item, D return true; } } else { - Game.notifications.add("minicraft.notification.gem_pickaxe_required"); + Game.inGameNotifications.add("minicraft.notification.gem_pickaxe_required"); } } return false; diff --git a/src/client/java/minicraft/level/tile/WallTile.java b/src/client/java/minicraft/level/tile/WallTile.java index 48466fd54..c30767d8f 100644 --- a/src/client/java/minicraft/level/tile/WallTile.java +++ b/src/client/java/minicraft/level/tile/WallTile.java @@ -60,7 +60,7 @@ public boolean hurt(Level level, int x, int y, Mob source, int dmg, Direction at hurt(level, x, y, 0); return true; } else { - Game.notifications.add(Localization.getLocalized(obrickMsg)); + Game.inGameNotifications.add(Localization.getLocalized(obrickMsg)); return false; } } @@ -81,7 +81,7 @@ public boolean interact(Level level, int xt, int yt, Player player, Item item, D return true; } } else { - Game.notifications.add(obrickMsg); + Game.inGameNotifications.add(obrickMsg); } } } diff --git a/src/client/java/minicraft/saveload/Load.java b/src/client/java/minicraft/saveload/Load.java index 787be58dc..65f19bb67 100644 --- a/src/client/java/minicraft/saveload/Load.java +++ b/src/client/java/minicraft/saveload/Load.java @@ -4,6 +4,7 @@ import minicraft.core.Updater; import minicraft.core.World; import minicraft.core.io.FileHandler; +import minicraft.core.io.InputHandler; import minicraft.core.io.Localization; import minicraft.core.io.Settings; import minicraft.entity.Arrow; @@ -584,6 +585,8 @@ private void loadPrefs(String filename, boolean partialLoad) { } ResourcePackDisplay.releaseUnloadedPacks(); + + Game.input.setControllerEnabled(json.optBoolean("controllerEnabled")); } private void loadUnlocksOld(String filename) { diff --git a/src/client/java/minicraft/saveload/Save.java b/src/client/java/minicraft/saveload/Save.java index 43d827550..5006516b4 100644 --- a/src/client/java/minicraft/saveload/Save.java +++ b/src/client/java/minicraft/saveload/Save.java @@ -4,6 +4,7 @@ import minicraft.core.Renderer; import minicraft.core.Updater; import minicraft.core.World; +import minicraft.core.io.InputHandler; import minicraft.core.io.Localization; import minicraft.core.io.Settings; import minicraft.entity.Arrow; @@ -207,6 +208,7 @@ private void writePrefs() { json.put("resourcePacks", new JSONArray(ResourcePackDisplay.getLoadedPacks())); json.put("showquests", String.valueOf(Settings.get("showquests"))); json.put("hwa", String.valueOf(Settings.get("hwa"))); + json.put("controllerEnabled", String.valueOf(Game.input.isControllerEnabled())); // Save json try { diff --git a/src/client/java/minicraft/screen/AchievementsDisplay.java b/src/client/java/minicraft/screen/AchievementsDisplay.java index 6703280f3..80c39db75 100644 --- a/src/client/java/minicraft/screen/AchievementsDisplay.java +++ b/src/client/java/minicraft/screen/AchievementsDisplay.java @@ -5,9 +5,13 @@ import minicraft.core.io.Localization; import minicraft.core.io.Sound; import minicraft.gfx.Color; +import minicraft.gfx.Dimension; import minicraft.gfx.Font; +import minicraft.gfx.Insets; +import minicraft.gfx.MinicraftImage; import minicraft.gfx.Point; import minicraft.gfx.Screen; +import minicraft.gfx.SpriteLinker; import minicraft.saveload.Save; import minicraft.screen.entry.ListEntry; import minicraft.screen.entry.SelectEntry; @@ -26,6 +30,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; public class AchievementsDisplay extends Display { @@ -168,9 +173,9 @@ private static boolean setAchievement(String id, boolean unlocked, boolean save, achievementScore += a.score; // Tells the player that they got an achievement. - Game.notifications.add(Localization.getLocalized("minicraft.notification.achievement_unlocked", Localization.getLocalized(id))); + Game.inGameToasts.add(new AchievementUnlockToast(id)); } else - achievementScore -= a.score; + achievementScore -= a.score; // Logical? // Save the new list of achievements stored in memory. if (save) new Save(); @@ -178,6 +183,55 @@ private static boolean setAchievement(String id, boolean unlocked, boolean save, return true; } + private static class AchievementUnlockToast extends Toast { + private static final int ICON_PADDING = 16; // 16 pixels wide space is reserved. + + private static final AchievementUnlockToastFrame FRAME = new AchievementUnlockToastFrame(); + + private static class AchievementUnlockToastFrame extends ToastFrame { + private static final SpriteLinker.LinkedSprite sprite = + new SpriteLinker.LinkedSprite(SpriteLinker.SpriteType.Gui, "toasts") + .setSpriteDim(0, 6, 3, 3); + + protected AchievementUnlockToastFrame() { + super(new Insets(3)); + } + + @Override + public void render(Screen screen, int x, int y, int w, int h) { + render(screen, x, y, w, h, sprite.getSprite()); + } + } + + private final String title = Localization.getLocalized( + "minicraft.toast.achievements.achievement_unlocked.title"); + private final String name; + private final Dimension size; + + public AchievementUnlockToast(String id) { + super(240); // 4 seconds + name = Localization.getLocalized(id); + size = new Dimension((Math.max(Font.textWidth(name), Font.textWidth(title)) + + FRAME.paddings.left + FRAME.paddings.right + ICON_PADDING + 7) / 8 * 8, + (Math.max(0, 2 * Font.textHeight() + SPACING + + FRAME.paddings.top + FRAME.paddings.bottom) + 7) / 8 * 8); + } + + @Override + public void render(Screen screen) { + // From the left + int x = -size.width * (ANIMATION_TIME - animationTick) / ANIMATION_TIME; // Shifting with animation (sliding) + int y = 0; + FRAME.render(screen, x, y, size.width / 8, size.height / 8); + screen.render(x + FRAME.paddings.left + (ICON_PADDING - MinicraftImage.boxWidth) / 2, + y + FRAME.paddings.top + MinicraftImage.boxWidth / 2, + Objects.requireNonNull(SpriteLinker.missingTexture(SpriteLinker.SpriteType.Item))); + Font.draw(title, screen, x + FRAME.paddings.left + ICON_PADDING, y + FRAME.paddings.top, Color.GOLD); + Font.draw(name, screen, x + FRAME.paddings.left + ICON_PADDING, + y + FRAME.paddings.top + SPACING + Font.textHeight(), Color.WHITE); + } + } + /** * Gets an array of all the unlocked achievements. * @return A string array with each unlocked achievement's id in it. diff --git a/src/client/java/minicraft/screen/AppToast.java b/src/client/java/minicraft/screen/AppToast.java new file mode 100644 index 000000000..1b1c863b8 --- /dev/null +++ b/src/client/java/minicraft/screen/AppToast.java @@ -0,0 +1,112 @@ +package minicraft.screen; + +import minicraft.gfx.Color; +import minicraft.gfx.Dimension; +import minicraft.gfx.Font; +import minicraft.gfx.FontStyle; +import minicraft.gfx.Insets; +import minicraft.gfx.Screen; +import minicraft.gfx.Sprite; +import minicraft.gfx.SpriteLinker; +import org.intellij.lang.annotations.MagicConstant; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.List; + +public class AppToast extends Toast { + private static final int TOP_PADDING = 30; + private static final int BOTTOM_PADDING = 30; + private static final int MAX_WIDTH = Screen.w * 3 / 8; + private static final int MAX_HEIGHT = Screen.w * 3 / 8; + + @MagicConstant(intValues = {0, 1}) + private static int positioning = 0; + + public static void setPositionTop() { + positioning = 0; + } + public static void setPositionBottom() { + positioning = 1; + } + + private final Dimension size; + private final List text; + private final AppToastFrame frame; + private final int color; + + public static abstract class AppToastFrame extends ToastFrame { + public static final AppToastFrame FRAME_GENERAL = new GeneralAppToastFrame(); + public static final AppToastFrame FRAME_URGENT = new UrgentAppToastFrame(); + public static final AppToastFrame FRAME_WINDOW = new WindowAppToastFrame(); + public static final AppToastFrame FRAME_BRICK = new BrickAppToastFrame(); + + protected final @Nullable SpriteLinker.LinkedSprite sprite; + + protected AppToastFrame(@Nullable SpriteLinker.LinkedSprite sprite, Insets paddings) { + super(paddings); + this.sprite = sprite; + } + + @Override + public void render(Screen screen, int x, int y, int w, int h) { + assert sprite != null; // If sprite == null, this implementation should be overridden. + render(screen, x, y, w, h, sprite.getSprite()); + } + + private static class GeneralAppToastFrame extends AppToastFrame { + private GeneralAppToastFrame() { + super(new SpriteLinker.LinkedSprite(SpriteLinker.SpriteType.Gui, "toasts") + .setSpriteDim(0, 0, 3, 3), new Insets(4)); + } + } + + private static class UrgentAppToastFrame extends AppToastFrame { + private UrgentAppToastFrame() { + super(new SpriteLinker.LinkedSprite(SpriteLinker.SpriteType.Gui, "toasts") + .setSpriteDim(3, 0, 3, 3), new Insets(4)); + } + } + + private static class WindowAppToastFrame extends AppToastFrame { + private WindowAppToastFrame() { + super(new SpriteLinker.LinkedSprite(SpriteLinker.SpriteType.Gui, "toasts") + .setSpriteDim(0, 3, 3, 3), new Insets(6, 4, 2, 8)); + } + } + + private static class BrickAppToastFrame extends AppToastFrame { + private BrickAppToastFrame() { + super(new SpriteLinker.LinkedSprite(SpriteLinker.SpriteType.Gui, "toasts") + .setSpriteDim(3, 3, 3, 3), new Insets(6, 6, 2, 6)); + } + } + } + + public AppToast(AppToastFrame frame, String message) { + this(frame, message, 180, Color.WHITE); // Default with 3 seconds + } + + public AppToast(AppToastFrame frame, String message, int expireTime, int color) { // Align left + super(expireTime); + this.frame = frame; + this.color = color; + String[] lines = Font.getLines(message, MAX_WIDTH - frame.paddings.left - frame.paddings.right, + MAX_HEIGHT - frame.paddings.top - frame.paddings.bottom, SPACING); + size = new Dimension((Font.textWidth(lines) + frame.paddings.left + frame.paddings.right + 7) / 8 * 8, + (Math.max(0, lines.length * Font.textHeight() + + SPACING * (lines.length - 1) + frame.paddings.top + frame.paddings.bottom) + 7) / 8 * 8); + // There is little a chance that the resultant height may still exceed the max height, but the problem is negligible. + text = Arrays.asList(lines); + } + + @Override + public void render(Screen screen) { + int x = Screen.w - size.width * animationTick / ANIMATION_TIME; // Shifting with animation (sliding) + int y = positioning == 0 ? TOP_PADDING : Screen.h - BOTTOM_PADDING - size.height; + frame.render(screen, x, y, size.width / 8, size.height / 8); + Font.drawParagraph(text, screen, new FontStyle(color) + .setAnchor(x + frame.paddings.left, y + frame.paddings.top) + .setRelTextPos(RelPos.BOTTOM_RIGHT, true), SPACING); + } +} diff --git a/src/client/java/minicraft/screen/ContainerDisplay.java b/src/client/java/minicraft/screen/ContainerDisplay.java index 05da5855e..716127258 100644 --- a/src/client/java/minicraft/screen/ContainerDisplay.java +++ b/src/client/java/minicraft/screen/ContainerDisplay.java @@ -54,7 +54,6 @@ protected void onSelectionChange(int oldSel, int newSel) { if (oldSel == newSel) return; // this also serves as a protection against access to menus[0] when such may not exist. - int shift = 0; if (newSel == 0) shift = padding - menus[0].getBounds().getLeft(); diff --git a/src/client/java/minicraft/screen/LoadingDisplay.java b/src/client/java/minicraft/screen/LoadingDisplay.java index 00e861791..e628c6e04 100644 --- a/src/client/java/minicraft/screen/LoadingDisplay.java +++ b/src/client/java/minicraft/screen/LoadingDisplay.java @@ -66,7 +66,7 @@ public void onExit() { progressType = "minicraft.displays.loading.message.world"; localizeProgressType = true; new Save(WorldSelectDisplay.getWorldName()); - Game.notifications.clear(); + Game.inGameNotifications.clear(); } } diff --git a/src/client/java/minicraft/screen/OnScreenKeyboardMenu.java b/src/client/java/minicraft/screen/OnScreenKeyboardMenu.java index 6d6f285dc..da2245412 100644 --- a/src/client/java/minicraft/screen/OnScreenKeyboardMenu.java +++ b/src/client/java/minicraft/screen/OnScreenKeyboardMenu.java @@ -40,7 +40,7 @@ public OnScreenKeyboardMenu() { */ @Nullable public static OnScreenKeyboardMenu checkAndCreateMenu() { - if (Game.input.anyControllerConnected()) { + if (Game.input.isControllerInUse()) { return new OnScreenKeyboardMenu(); } @@ -191,6 +191,9 @@ private void setShiftPressed(boolean pressed) { @Override public void tick(InputHandler input) throws OnScreenKeyboardMenuTickActionCompleted, OnScreenKeyboardMenuBackspaceButtonActed { if (keyPressed > 0) keyPressed--; // Resetting rendered pressing status. + if (!input.isControllerInUse()) { + setVisible(false); + } // This is only controllable by controller. if (visible) { diff --git a/src/client/java/minicraft/screen/OptionsMainMenuDisplay.java b/src/client/java/minicraft/screen/OptionsMainMenuDisplay.java index f01b53ef3..bde46e1d6 100644 --- a/src/client/java/minicraft/screen/OptionsMainMenuDisplay.java +++ b/src/client/java/minicraft/screen/OptionsMainMenuDisplay.java @@ -5,29 +5,36 @@ import minicraft.core.io.InputHandler; import minicraft.core.io.Settings; import minicraft.saveload.Save; +import minicraft.screen.entry.BooleanEntry; import minicraft.screen.entry.SelectEntry; import java.util.ArrayList; public class OptionsMainMenuDisplay extends Display { private final boolean prevHwaValue = (boolean) Settings.get("hwa"); + private final BooleanEntry controllersEntry = new BooleanEntry("minicraft.display.options_display.controller", + Game.input.isControllerEnabled()); public OptionsMainMenuDisplay() { - super(true, new Menu.Builder(false, 6, RelPos.LEFT, - Settings.getEntry("fps"), - Settings.getEntry("sound"), - Settings.getEntry("showquests"), - Settings.getEntry("hwa"), - new SelectEntry("minicraft.display.options_display.change_key_bindings", () -> Game.setDisplay(new KeyInputDisplay())), - new SelectEntry("minicraft.displays.controls", () -> Game.setDisplay(new ControlsDisplay())), - new SelectEntry("minicraft.display.options_display.language", () -> Game.setDisplay(new LanguageSettingsDisplay())), - new SelectEntry("minicraft.display.options_display.resource_packs", () -> Game.setDisplay(new ResourcePackDisplay())) - ) - .setTitle("minicraft.displays.options_main_menu") - .createMenu()); - } + super(true); + menus = new Menu[] { + new Menu.Builder(false, 6, RelPos.LEFT, + Settings.getEntry("fps"), + Settings.getEntry("sound"), + Settings.getEntry("showquests"), + Settings.getEntry("hwa"),new SelectEntry("minicraft.display.options_display.change_key_bindings", () -> Game.setDisplay(new KeyInputDisplay())), + new SelectEntry("minicraft.displays.controls", () -> Game.setDisplay(new ControlsDisplay())), + new SelectEntry("minicraft.display.options_display.language", () -> Game.setDisplay(new LanguageSettingsDisplay())), - @Override + controllersEntry, + new SelectEntry("minicraft.display.options_display.resource_packs", () -> Game.setDisplay(new ResourcePackDisplay())) + ) + .setTitle("minicraft.displays.options_main_menu") + .createMenu() + }; + } + + @Override public void tick(InputHandler input) { if (!prevHwaValue && (boolean) Settings.get("hwa") && FileHandler.OS.contains("windows") && input.inputPressed("EXIT")) { ArrayList callbacks = new ArrayList<>(); @@ -46,8 +53,9 @@ public void tick(InputHandler input) { } @Override - public void onExit() { - new Save(); - Game.MAX_FPS = (int) Settings.get("fps"); - } + public void onExit() { + new Save(); + Game.MAX_FPS = (int) Settings.get("fps"); + Game.input.setControllerEnabled(controllersEntry.getValue()); + } } diff --git a/src/client/java/minicraft/screen/OptionsWorldDisplay.java b/src/client/java/minicraft/screen/OptionsWorldDisplay.java index c297d1d52..eee4d1b4b 100644 --- a/src/client/java/minicraft/screen/OptionsWorldDisplay.java +++ b/src/client/java/minicraft/screen/OptionsWorldDisplay.java @@ -6,18 +6,23 @@ import minicraft.core.io.Settings; import minicraft.gfx.Color; import minicraft.saveload.Save; +import minicraft.screen.entry.ArrayEntry; import minicraft.screen.entry.BlankEntry; +import minicraft.screen.entry.BooleanEntry; import minicraft.screen.entry.ListEntry; import minicraft.screen.entry.SelectEntry; import minicraft.screen.entry.StringEntry; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.Executors; public class OptionsWorldDisplay extends Display { private final boolean prevHwaValue = (boolean) Settings.get("hwa"); + private final BooleanEntry controllersEntry = new BooleanEntry("minicraft.display.options_display.controller", + Game.input.isControllerEnabled()); public OptionsWorldDisplay() { super(true); @@ -88,6 +93,7 @@ private List getEntries() { new SelectEntry("minicraft.display.options_display.change_key_bindings", () -> Game.setDisplay(new KeyInputDisplay())), new SelectEntry("minicraft.displays.controls", () -> Game.setDisplay(new ControlsDisplay())), new SelectEntry("minicraft.display.options_display.language", () -> Game.setDisplay(new LanguageSettingsDisplay())), + controllersEntry, new SelectEntry("minicraft.display.options_display.resource_packs", () -> Game.setDisplay(new ResourcePackDisplay())) )); } @@ -96,5 +102,6 @@ private List getEntries() { public void onExit() { new Save(); Game.MAX_FPS = (int) Settings.get("fps"); + Game.input.setControllerEnabled(controllersEntry.getValue()); } } diff --git a/src/client/java/minicraft/screen/ResourcePackDisplay.java b/src/client/java/minicraft/screen/ResourcePackDisplay.java index 6ca66a950..bef2d8abb 100644 --- a/src/client/java/minicraft/screen/ResourcePackDisplay.java +++ b/src/client/java/minicraft/screen/ResourcePackDisplay.java @@ -401,7 +401,7 @@ public void render(Screen screen) { Font.drawCentered(Localization.getLocalized("minicraft.displays.resource_packs.display.title"), screen, 6, Color.WHITE); // Info text at the bottom. - if (Game.input.anyControllerConnected()) + if (Game.input.isControllerEnabled()) Font.drawCentered(Localization.getLocalized("minicraft.displays.resource_packs.display.help.keyboard_needed"), screen, Screen.h - 33, Color.DARK_GRAY); Font.drawCentered(Localization.getLocalized("minicraft.displays.resource_packs.display.help.move", Game.input.getMapping("cursor-down"), Game.input.getMapping("cursor-up")), screen, Screen.h - 25, Color.DARK_GRAY); Font.drawCentered(Localization.getLocalized("minicraft.displays.resource_packs.display.help.select", Game.input.getMapping("SELECT")), screen, Screen.h - 17, Color.DARK_GRAY); diff --git a/src/client/java/minicraft/screen/Toast.java b/src/client/java/minicraft/screen/Toast.java new file mode 100644 index 000000000..fd6105017 --- /dev/null +++ b/src/client/java/minicraft/screen/Toast.java @@ -0,0 +1,80 @@ +package minicraft.screen; + +import minicraft.gfx.Dimension; +import minicraft.gfx.Insets; +import minicraft.gfx.Screen; +import minicraft.gfx.Sprite; + +import java.util.List; + +public abstract class Toast { + protected static final int SPACING = 2; + protected static final int ANIMATION_TIME = 12; + + protected final int expireTime; + + protected int tick = 0; + protected int animationTick = 0; // 0 to ANIMATION_TIME, ANIMATION_TIME to 0 + + protected static abstract class ToastFrame { + protected final Insets paddings; + + protected ToastFrame(Insets paddings) { + this.paddings = paddings; + } + + public abstract void render(Screen screen, int x, int y, int w, int h); // w, h in units of cells + + /* + * Standard Frame Texture Format + * + * 3 * 3 cells for a sprite + * [Top-left corner] [Top frame edge (pattern)] [Top-right corner] + * [Left frame edge (pattern)] [content background (pattern)] [Right frame edge (pattern)] + * [Bottom-left corner] [Bottom frame edge] [Bottom-right corner] + */ + + // Acts as a helper method in case some implementations may still use the standard texture formats. + protected void render(Screen screen, int x, int y, int w, int h, Sprite sprite) { + for (int i = 0; i < w; ++i) { + for (int j = 0; j < h; ++j) { + Sprite.Px[][] pxs = sprite.spritePixels; + if (i == 0 && j == 0) + screen.render(x, y, pxs[0][0]); + else if (i == 0 && j == h - 1) + screen.render(x, y + j * 8, pxs[2][0]); + else if (i == w - 1 && j == 0) + screen.render(x + i * 8, y, pxs[0][2]); + else if (i == w - 1 && j == h - 1) + screen.render(x + i * 8, y + j * 8, pxs[2][2]); + else if (i == 0) + screen.render(x, y + j * 8, pxs[1][0]); + else if (j == 0) + screen.render(x + i * 8, y, pxs[0][1]); + else if (i == w - 1) + screen.render(x + i * 8, y + j * 8, pxs[1][2]); + else if (j == h - 1) + screen.render(x + i * 8, y + j * 8, pxs[2][1]); + else + screen.render(x + i * 8, y + j * 8, pxs[1][1]); + } + } + } + } + + protected Toast(int expireTime) { + this.expireTime = expireTime; + } + + public void tick() { + if (tick == 0 && animationTick < ANIMATION_TIME) animationTick++; + else if (tick < expireTime) tick++; + else if (tick == expireTime) animationTick--; + } + + public abstract void render(Screen screen); + + public boolean isExpired() { + return tick >= expireTime && animationTick <= 0; + } +} diff --git a/src/client/java/minicraft/screen/TutorialDisplayHandler.java b/src/client/java/minicraft/screen/TutorialDisplayHandler.java index 84b9b3235..4da6844ce 100644 --- a/src/client/java/minicraft/screen/TutorialDisplayHandler.java +++ b/src/client/java/minicraft/screen/TutorialDisplayHandler.java @@ -154,7 +154,7 @@ public static void turnOffTutorials() { currentOngoingElement = null; Settings.set("tutorials", false); Logging.TUTORIAL.debug("Tutorial completed."); - Game.notifications.add(Localization.getLocalized("minicraft.notification.tutorials_completed")); + Game.inGameNotifications.add(Localization.getLocalized("minicraft.notification.tutorials_completed")); } private static void turnOffGuides() { diff --git a/src/client/resources/assets/localization/en-us.json b/src/client/resources/assets/localization/en-us.json index 60c374467..51641bade 100644 --- a/src/client/resources/assets/localization/en-us.json +++ b/src/client/resources/assets/localization/en-us.json @@ -218,6 +218,7 @@ "minicraft.display.menus.inventory": "Inventory", "minicraft.display.options_display": "Options", "minicraft.display.options_display.change_key_bindings": "Change Key Bindings", + "minicraft.display.options_display.controller": "Controller Enabled", "minicraft.display.options_display.language": "Language", "minicraft.display.options_display.popup.hwa_warning.content": "Possible visual problems might occur if OpenGL Hardware Acceleration is enabled. Please confirm if you want to proceed.", "minicraft.display.options_display.popup.hwa_warning.title": "Warning", @@ -397,7 +398,6 @@ "minicraft.displays.world_select.popups.display.confirm": "%s to confirm", "minicraft.displays.world_select.popups.display.delete": "Are you sure you want to delete\n%s\"%s\"%s?\nThis can not be undone!", "minicraft.displays.world_select.select_world": "Select World", - "minicraft.notification.achievement_unlocked": "Achivement unlocked: %s", "minicraft.notification.air_wizard_defeated": "Air Wizard Defeated!", "minicraft.notification.boss_limit": "No more bosses can be spawned", "minicraft.notification.cannot_sleep": "Can't sleep! %sMin %s Sec left!", @@ -418,6 +418,11 @@ "minicraft.notification.world_saved": "World Saved!", "minicraft.notification.wrong_level_dungeon": "Can only be summoned on the dungeon level", "minicraft.notification.wrong_level_sky": "Can only be summoned on the sky level", + "minicraft.notification.controllers.configured_plugged": "The controller (\"%s\") is configured.", + "minicraft.notification.controllers.configured_unplugged": "The configured controller (\"%s\") is unplugged.", + "minicraft.notification.controllers.configured_undetected": "No controller is detected.", + "minicraft.notification.controllers.controller_plugged": "A controller (\"%s\") is plugged.", + "minicraft.notification.controllers.controller_unplugged": "A controller (\"%s\") is unplugged.", "minicraft.notifications.statue_tapped": "You hear echoed whispers...", "minicraft.notifications.statue_touched": "You hear the statue vibrating...", "minicraft.quest.farming": "Farming Farmer", @@ -491,6 +496,7 @@ "minicraft.skin.paul": "Paul", "minicraft.skin.paul_cape": "Paul with cape", "minicraft.text_particales.key_consumed": "-1 key", + "minicraft.toast.achievements.achievement_unlocked.title": "Achievement Unlocked!", "minicraft.tutorial.getting_rocks": "Getting stones and coals", "minicraft.tutorial.getting_rocks.description": "Getting at least 5 stone and 5 coal items by destroying rocks with the pickaxe crafted.", "minicraft.tutorial.getting_wood": "Getting more wood", diff --git a/src/client/resources/assets/textures/gui/app_status_bar.png b/src/client/resources/assets/textures/gui/app_status_bar.png new file mode 100644 index 000000000..4e60c6e2a Binary files /dev/null and b/src/client/resources/assets/textures/gui/app_status_bar.png differ diff --git a/src/client/resources/assets/textures/gui/toasts.png b/src/client/resources/assets/textures/gui/toasts.png new file mode 100644 index 000000000..8f735f60d Binary files /dev/null and b/src/client/resources/assets/textures/gui/toasts.png differ