diff --git a/Zennysoft.Game.Ma/addons/input_helper/InputHelper.cs b/Zennysoft.Game.Ma/addons/input_helper/InputHelper.cs new file mode 100644 index 00000000..a578125a --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/InputHelper.cs @@ -0,0 +1,287 @@ +using Godot; +using Godot.Collections; + +namespace NathanHoad +{ + public static class InputHelper + { + public delegate void DeviceChangedEventHandler(string device, int deviceIndex); + public delegate void KeyboardInputChangedEventHandler(string action, InputEvent input); + public delegate void JoypadInputChangedEventHandler(string action, InputEvent input); + public delegate void JoypadChangedEventHandler(int deviceIndex, bool isConnected); + + public static DeviceChangedEventHandler? DeviceChanged; + public static KeyboardInputChangedEventHandler? KeyboardInputChanged; + public static JoypadInputChangedEventHandler? JoypadInputChanged; + public static JoypadChangedEventHandler? JoypadChanged; + + + public const string DEVICE_KEYBOARD = "keyboard"; + public const string DEVICE_XBOX_CONTROLLER = "xbox"; + public const string DEVICE_SWITCH_CONTROLLER = "switch"; + public const string DEVICE_PLAYSTATION_CONTROLLER = "playstation"; + public const string DEVICE_STEAMDECK_CONTROLLER = "steamdeck"; + public const string DEVICE_GENERIC = "generic"; + + public const string SUB_DEVICE_XBOX_ONE_CONTROLLER = "xbox_one"; + public const string SUB_DEVICE_XBOX_SERIES_CONTROLLER = "xbox_series"; + public const string SUB_DEVICE_PLAYSTATION3_CONTROLLER = "playstation3"; + public const string SUB_DEVICE_PLAYSTATION4_CONTROLLER = "playstation4"; + public const string SUB_DEVICE_PLAYSTATION5_CONTROLLER = "playstation5"; + public const string SUB_DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER = "switch_left_joycon"; + public const string SUB_DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER = "switch_right_joycon"; + + + private static Node instance; + public static Node Instance + { + get + { + if (instance == null) + { + instance = (Node)Engine.GetSingleton("InputHelper"); + instance.Connect("device_changed", Callable.From((string device, int deviceIndex) => DeviceChanged?.Invoke(device, deviceIndex))); + instance.Connect("keyboard_input_changed", Callable.From((string action, InputEvent input) => KeyboardInputChanged?.Invoke(action, input))); + instance.Connect("joypad_input_changed", Callable.From((string action, InputEvent input) => JoypadInputChanged?.Invoke(action, input))); + instance.Connect("joypad_changed", Callable.From((int deviceIndex, bool isConnected) => JoypadChanged?.Invoke(deviceIndex, isConnected))); + } + return instance; + } + } + + + public static string Device + { + get => (string)Instance.Get("device"); + } + + + public static int DeviceIndex + { + get => (int)Instance.Get("device_index"); + } + + + public static string LastKnownJoypadDevice + { + get => (string)Instance.Get("last_known_joypad_device"); + } + + + public static string LastKnownJoypadIndex + { + get => (string)Instance.Get("last_known_joypad_index"); + } + + + public static float Deadzone + { + get => (float)Instance.Get("deadzone"); + set => Instance.Set("deadzone", value); + } + + + public static int MouseMotionThreshold + { + get => (int)Instance.Get("mouse_motion_threshold"); + set => Instance.Set("mouse_motion_threshold", value); + } + + + public static string GetSimplifiedDeviceName(string rawName) + { + return (string)Instance.Call("get_simplified_device_name", rawName); + } + + + public static string GetDeviceFromEvent(InputEvent @event) + { + return (string)Instance.Call("get_device_from_event", @event); + } + + + public static int GetDeviceIndexFromEvent(InputEvent @event) + { + return (int)Instance.Call("get_device_index_from_event", @event); + } + + + public static bool HasJoypad() + { + return (bool)Instance.Call("has_joypad"); + } + + + public static string GuessDeviceName() + { + return (string)Instance.Call("guess_device_name"); + } + + + public static void ResetAllActions() + { + Instance.Call("reset_all_actions"); + } + + + public static void SetKeyboardOrJoypadInputForAction(string action, InputEvent input, bool swapIfTaken = true) + { + Instance.Call("set_keyboard_or_joypad_input_for_action", action, input, swapIfTaken); + } + + + public static InputEvent GetKeyboardOrJoypadInputForAction(string action, InputEvent input, bool swapIfTaken = true) + { + return (InputEvent)Instance.Call("get_keyboard_or_joypad_input_for_action", action, input, swapIfTaken); + } + + + public static Array GetKeyboardOrJoypadInputsForAction(string action) + { + return (Array)Instance.Call("get_keyboard_or_joypad_inputs_for_action", action); + } + + + public static string GetLabelForInput(InputEvent input) + { + return (string)Instance.Call("get_label_for_input", input); + } + + + public static string SerializeInputsForAction(string action) + { + return (string)Instance.Call("serialize_inputs_for_action", action); + } + + + public static string SerializeInputsForActions(Array actions = null) + { + if (actions == null) + { + actions = new Array(); + } + return (string)Instance.Call("serialize_inputs_for_actions", actions); + } + + + public static void DeserializeInputsForAction(string action, string serializedInputs) + { + Instance.Call("desserialize_inputs_for_action", action, serializedInputs); + } + + public static void DeserializeInputsForActions(string serializedInputs) + { + Instance.Call("deserialize_inputs_for_actions", serializedInputs); + } + + + #region Keyboard/Mouse + + public static Array GetKeyboardInputsForAction(string action) + { + return (Array)Instance.Call("get_keyboard_inputs_for_action", action); + } + + + public static InputEvent GetKeyboardInputForAction(string action) + { + return (InputEvent)Instance.Call("get_keyboard_input_for_action", action); + } + + + public static void SetKeyboardInputForAction(string action, InputEvent input, bool swapIfTaken = true) + { + Instance.Call("set_keyboard_input_for_action", action, input, swapIfTaken); + } + + + public static void ReplaceKeyboardInputForAction(string action, InputEvent currentInput, InputEvent input, bool swapIfTaken = true) + { + Instance.Call("replace_keyboard_input_for_action", action, currentInput, input, swapIfTaken); + } + + + public static void ReplaceKeyboardInputAtIndex(string action, int index, InputEvent input, bool swapIfTaken = true) + { + Instance.Call("replace_keyboard_input_at_index", action, index, input, swapIfTaken); + } + + #endregion + + + #region Joypad + + public static Array GetJoypadInputsForAction(string action) + { + return (Array)Instance.Call("get_joypad_inputs_for_action", action); + } + + + public static InputEvent GetJoypadInputForAction(string action) + { + return (InputEvent)Instance.Call("get_joypad_input_for_action", action); + } + + + public static void SetJoypadInputForAction(string action, InputEvent input, bool swapIfTaken = true) + { + Instance.Call("set_joypad_input_for_action", action, input, swapIfTaken); + } + + + public static void ReplaceJoypadInputForAction(string action, InputEvent currentInput, InputEvent input, bool swapIfTaken = true) + { + Instance.Call("replace_joypad_input_for_action", action, currentInput, input, swapIfTaken); + } + + + public static void ReplaceJoypadInputAtIndex(string action, int index, InputEvent input, bool swapIfTaken = true) + { + Instance.Call("replace_joypad_input_at_index", action, index, input, swapIfTaken); + } + + + public static void RumbleSmall(int targetDevice = 0) + { + Instance.Call("rumble_small", targetDevice); + } + + + public static void RumbleMedium(int targetDevice = 0) + { + Instance.Call("rumble_medium", targetDevice); + } + + + public static void RumbleLarge(int targetDevice = 0) + { + Instance.Call("rumble_large", targetDevice); + } + + + public static void StartRumbleSmall(int targetDevice = 0) + { + Instance.Call("start_rumble_small", targetDevice); + } + + + public static void StartRumbleMedium(int targetDevice = 0) + { + Instance.Call("start_rumble_medium", targetDevice); + } + + + public static void StartRumbleLarge(int targetDevice = 0) + { + Instance.Call("start_rumble_large", targetDevice); + } + + + public static void StopRumble(int targetDevice = 0) + { + Instance.Call("stop_rumble", targetDevice); + } + + #endregion + } +} diff --git a/Zennysoft.Game.Ma/addons/input_helper/InputHelper.cs.uid b/Zennysoft.Game.Ma/addons/input_helper/InputHelper.cs.uid new file mode 100644 index 00000000..e80a4801 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/InputHelper.cs.uid @@ -0,0 +1 @@ +uid://cfs4dgwrfvb11 diff --git a/Zennysoft.Game.Ma/addons/input_helper/LICENSE b/Zennysoft.Game.Ma/addons/input_helper/LICENSE new file mode 100644 index 00000000..393b4deb --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-present Nathan Hoad + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Zennysoft.Game.Ma/addons/input_helper/assets/update.svg b/Zennysoft.Game.Ma/addons/input_helper/assets/update.svg new file mode 100644 index 00000000..3c86fc3a --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/assets/update.svg @@ -0,0 +1,230 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Zennysoft.Game.Ma/addons/input_helper/assets/update.svg.import b/Zennysoft.Game.Ma/addons/input_helper/assets/update.svg.import new file mode 100644 index 00000000..c4dde1cb --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/assets/update.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ddixs2ish5bi6" +path="res://.godot/imported/update.svg-3137f1f7d53c08c0ae65aabe138d898b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/input_helper/assets/update.svg" +dest_files=["res://.godot/imported/update.svg-3137f1f7d53c08c0ae65aabe138d898b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/Zennysoft.Game.Ma/addons/input_helper/components/download_update_panel.gd b/Zennysoft.Game.Ma/addons/input_helper/components/download_update_panel.gd new file mode 100644 index 00000000..96fa1614 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/components/download_update_panel.gd @@ -0,0 +1,83 @@ +@tool + +extends Control + + +signal failed() +signal updated(updated_to_version: String) + + +const TEMP_FILE_NAME = "user://temp.zip" + + +@onready var logo: TextureRect = %Logo +@onready var label: Label = $VBox/Label +@onready var http_request: HTTPRequest = $HTTPRequest +@onready var download_button: Button = %DownloadButton + +var next_version: String = "": + set(next_next_version): + next_version = next_next_version + label.text = "Version %s is available for download!" % next_version + get: + return next_version + + +func save_zip(bytes: PackedByteArray) -> void: + var file: FileAccess = FileAccess.open(TEMP_FILE_NAME, FileAccess.WRITE) + file.store_buffer(bytes) + file.flush() + + +### Signals + + +func _on_download_button_pressed() -> void: + # Safeguard the actual input helper repo from accidentally updating itself + if FileAccess.file_exists("res://examples/device_tester.gd"): + prints("You can't update the input helper from within itself.") + failed.emit() + return + + http_request.request("https://github.com/nathanhoad/godot_input_helper/archive/refs/tags/v%s.zip" % next_version) + download_button.disabled = true + download_button.text = "Downloading..." + + +func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void: + if result != HTTPRequest.RESULT_SUCCESS: + failed.emit() + return + + # Save the downloaded zip + save_zip(body) + + OS.move_to_trash(ProjectSettings.globalize_path("res://addons/input_helper")) + + var zip_reader: ZIPReader = ZIPReader.new() + zip_reader.open(TEMP_FILE_NAME) + var files: PackedStringArray = zip_reader.get_files() + + var base_path = files[1] + # Remove archive folder + files.remove_at(0) + # Remove assets folder + files.remove_at(0) + + for path in files: + var new_file_path: String = path.replace(base_path, "") + if path.ends_with("/"): + DirAccess.make_dir_recursive_absolute("res://addons/%s" % new_file_path) + else: + var file: FileAccess = FileAccess.open("res://addons/%s" % new_file_path, FileAccess.WRITE) + file.store_buffer(zip_reader.read_file(path)) + + zip_reader.close() + + DirAccess.remove_absolute(TEMP_FILE_NAME) + + updated.emit(next_version) + + +func _on_notes_button_pressed() -> void: + OS.shell_open("https://github.com/nathanhoad/godot_input_helper/releases/tag/v%s" % next_version) diff --git a/Zennysoft.Game.Ma/addons/input_helper/components/download_update_panel.gd.uid b/Zennysoft.Game.Ma/addons/input_helper/components/download_update_panel.gd.uid new file mode 100644 index 00000000..248665be --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/components/download_update_panel.gd.uid @@ -0,0 +1 @@ +uid://dcff0mowkn6km diff --git a/Zennysoft.Game.Ma/addons/input_helper/components/download_update_panel.tscn b/Zennysoft.Game.Ma/addons/input_helper/components/download_update_panel.tscn new file mode 100644 index 00000000..9d9bf3f0 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/components/download_update_panel.tscn @@ -0,0 +1,60 @@ +[gd_scene load_steps=3 format=3 uid="uid://b7mst0qu7vjk1"] + +[ext_resource type="Script" uid="uid://dcff0mowkn6km" path="res://addons/input_helper/components/download_update_panel.gd" id="1_4tm1k"] +[ext_resource type="Texture2D" uid="uid://ddixs2ish5bi6" path="res://addons/input_helper/assets/update.svg" id="2_j7shv"] + +[node name="DownloadUpdatePanel" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_4tm1k") + +[node name="HTTPRequest" type="HTTPRequest" parent="."] + +[node name="VBox" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -1.0 +offset_top = 9.0 +offset_right = -1.0 +offset_bottom = 9.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/separation = 10 + +[node name="Logo" type="TextureRect" parent="VBox"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(300, 80) +layout_mode = 2 +texture = ExtResource("2_j7shv") +stretch_mode = 5 + +[node name="Label" type="Label" parent="VBox"] +layout_mode = 2 +text = "v1.2.3 is available for download." +horizontal_alignment = 1 + +[node name="Center" type="CenterContainer" parent="VBox"] +layout_mode = 2 + +[node name="DownloadButton" type="Button" parent="VBox/Center"] +unique_name_in_owner = true +layout_mode = 2 +text = "Download and install update" + +[node name="Center2" type="CenterContainer" parent="VBox"] +layout_mode = 2 + +[node name="NotesButton" type="LinkButton" parent="VBox/Center2"] +layout_mode = 2 +text = "Read release notes..." + +[connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"] +[connection signal="pressed" from="VBox/Center/DownloadButton" to="." method="_on_download_button_pressed"] +[connection signal="pressed" from="VBox/Center2/NotesButton" to="." method="_on_notes_button_pressed"] diff --git a/Zennysoft.Game.Ma/addons/input_helper/input_helper.gd b/Zennysoft.Game.Ma/addons/input_helper/input_helper.gd new file mode 100644 index 00000000..06d70d30 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/input_helper.gd @@ -0,0 +1,601 @@ +extends Node + + +signal device_changed(device: String, device_index: int) +signal keyboard_input_changed(action: String, input: InputEvent) +signal joypad_input_changed(action: String, input: InputEvent) +signal joypad_changed(device_index: int, is_connected: bool) + + +const DEVICE_KEYBOARD = "keyboard" +const DEVICE_XBOX_CONTROLLER = "xbox" +const DEVICE_SWITCH_CONTROLLER = "switch" +const DEVICE_PLAYSTATION_CONTROLLER = "playstation" +const DEVICE_STEAMDECK_CONTROLLER = "steamdeck" +const DEVICE_GENERIC = "generic" + +const SUB_DEVICE_XBOX_ONE_CONTROLLER = "xbox_one" +const SUB_DEVICE_XBOX_SERIES_CONTROLLER = "xbox_series" + +const SUB_DEVICE_PLAYSTATION3_CONTROLLER = "playstation3" +const SUB_DEVICE_PLAYSTATION4_CONTROLLER = "playstation4" +const SUB_DEVICE_PLAYSTATION5_CONTROLLER = "playstation5" + +const SUB_DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER = "switch_left_joycon" +const SUB_DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER = "switch_right_joycon" + +const XBOX_BUTTON_LABELS = ["A", "B", "X", "Y", "Back", "Guide", "Start", "Left Stick", "Right Stick", "LB", "RB", "Up", "Down", "Left", "Right", "Share", "Paddle 1", "Paddle 2", "Paddle 3", "Paddle 4"] +const XBOX_ONE_BUTTON_LABELS = ["A", "B", "X", "Y", "View", "Guide", "Menu", "Left Stick", "Right Stick", "LB", "RB", "Up", "Down", "Left", "Right", "Share", "Paddle 1", "Paddle 2", "Paddle 3", "Paddle 4"] +const XBOX_SERIES_BUTTON_LABELS = ["A", "B", "X", "Y", "View", "Guide", "Menu", "Left Stick", "Right Stick", "LB", "RB", "Up", "Down", "Left", "Right", "Share", "Paddle 1", "Paddle 2", "Paddle 3", "Paddle 4"] +const STEAMDECK_BUTTON_LABELS = ["A", "B", "X", "Y", "View", "?", "Options", "Left Stick", "Right Stick", "L1", "R1", "Up", "Down", "Left", "Right", "", "", "", "", ""] +# Note: share and home buttons are not recognized +const SWITCH_BUTTON_LABELS = ["B", "A", "Y", "X", "Minus", "", "Plus", "Left Stick", "Right Stick", "LS", "RS", "Up", "Down", "Left", "Right", "Capture"] +# Mapping for left and right joypad connected together (extended gamepad) +# Left Stick is Axis 0 and 1 +# Right Stick is Axis 2 and 3 +# ZL and ZR are Axis 4 and 5 +const SWITCH_EXTENDED_GAMEPAD_BUTTON_LABELS = ["B", "A", "Y", "X", "Minus", "", "Plus", "Left Stick", "Right Stick", "L", "R", "Up", "Down", "Left", "Right", "Capture"] +const PLAYSTATION_3_4_BUTTON_LABELS = ["Cross", "Circle", "Square", "Triangle", "Share", "PS", "Options", "L3", "R3", "L1", "R1", "Up", "Down", "Left", "Right", "Microphone", "", "", "", "", "Touchpad"] +# Note: Microphone does not work on PC / touchpad is not recognized +const PLAYSTATION_5_BUTTON_LABELS = ["Cross", "Circle", "Square", "Triangle", "Create", "PS", "Options", "L3", "R3", "L1", "R1", "Up", "Down", "Left", "Right", "Microphone", "", "", "", "", "Touchpad"] + +const SERIAL_VERSION = 1 + +## The deadzone to ignore for joypad motion +var deadzone: float = 0.5 +## The mouse distance to ignore before movement is assumed +var mouse_motion_threshold: int = 100 +## The last known joypad device name (or "" if no joypad detected) +var last_known_joypad_device: String = get_simplified_device_name(Input.get_joy_name(0)) +## The last known joypad index +var last_known_joypad_index: int = 0 if Input.get_connected_joypads().size() > 0 else -1 + +## Used internally +var device_last_changed_at: int = 0 +var _last_known_granular_joypad_device: String = get_simplified_device_name(Input.get_joy_name(0), true) + +@onready var device: String = guess_device_name() +@onready var device_index: int = 0 if has_joypad() else -1 + + +func _ready() -> void: + process_mode = Node.PROCESS_MODE_ALWAYS + + if not Engine.has_singleton("InputHelper"): + Engine.register_singleton("InputHelper", self) + + Input.joy_connection_changed.connect(func(device_index, is_connected): joypad_changed.emit(device_index, is_connected)) + + +func _input(event: InputEvent) -> void: + var next_device: String = device + var next_device_index: int = device_index + + # Did we just press a key on the keyboard or move the mouse? + if event is InputEventKey \ + or event is InputEventMouseButton \ + or (event is InputEventMouseMotion and (event as InputEventMouseMotion).relative.length_squared() > mouse_motion_threshold): + next_device = DEVICE_KEYBOARD + next_device_index = -1 + + # Did we just use a joypad? + elif event is InputEventJoypadButton \ + or (event is InputEventJoypadMotion and abs(event.axis_value) > deadzone): + next_device = get_simplified_device_name(get_joy_name(event.device)) + last_known_joypad_device = next_device + next_device_index = event.device + last_known_joypad_index = next_device_index + + _last_known_granular_joypad_device = get_simplified_device_name(get_joy_name(event.device), true) + + # Debounce changes for 1 second because some joypads register twice in Windows for some reason + var not_changed_in_last_second = Engine.get_frames_drawn() - device_last_changed_at > Engine.get_frames_per_second() + if (next_device != device or next_device_index != device_index) and not_changed_in_last_second: + device_last_changed_at = Engine.get_frames_drawn() + + device = next_device + device_index = next_device_index + device_changed.emit(device, device_index) + + +## Get the name of a joypad +func get_joy_name(at_device_index: int) -> String: + var joy_name: String = Input.get_joy_name(at_device_index) + if joy_name == "" and Input.get_joy_info(at_device_index).size() > 0 and "xinput" in Input.get_joy_info(at_device_index).keys()[0]: + joy_name = "XInput" + return joy_name + + +## Get the device name for an event +func get_device_from_event(event: InputEvent) -> String: + if event is InputEventKey or event is InputEventMouseButton or event is InputEventMouseMotion: + return DEVICE_KEYBOARD + elif event is InputEventJoypadButton or event is InputEventJoypadMotion: + return get_simplified_device_name(get_joy_name(event.device)) + else: + return DEVICE_GENERIC + + +## Get the device name for an event +func get_device_index_from_event(event: InputEvent) -> int: + if event is InputEventJoypadButton or event is InputEventJoypadMotion: + return event.device + else: + return -1 + + +## Convert a Godot device identifier to a simplified string +func get_simplified_device_name(raw_name: String, force_granular_identifier: bool = false) -> String: + var use_granular_identifier: bool = force_granular_identifier or InputHelperSettings.get_setting(InputHelperSettings.USE_GRANULAR_DEVICE_IDENTIFIERS, false) + + var keywords: Dictionary = { + SUB_DEVICE_XBOX_ONE_CONTROLLER: ["Xbox One Controller"], + SUB_DEVICE_XBOX_SERIES_CONTROLLER: ["Xbox Series Controller", "Xbox Wireless Controller"], + DEVICE_XBOX_CONTROLLER: ["XInput", "XBox"], + SUB_DEVICE_PLAYSTATION3_CONTROLLER: ["PS3"], + SUB_DEVICE_PLAYSTATION4_CONTROLLER:["Nacon Revolution Unlimited Pro Controller", "PS4", "DUALSHOCK 4"], + SUB_DEVICE_PLAYSTATION5_CONTROLLER:["Sony DualSense", "PS5", "DualSense Wireless Controller"], + DEVICE_STEAMDECK_CONTROLLER: ["Steam"], + DEVICE_SWITCH_CONTROLLER: ["Switch", "Joy-Con (L/R)", "PowerA Core Controller"], + SUB_DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER: ["Joy-Con (L)"], + SUB_DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER: ["joy-Con (R)"], + } if use_granular_identifier else { + DEVICE_XBOX_CONTROLLER: ["XBox", "XInput"], + DEVICE_PLAYSTATION_CONTROLLER: ["Sony", "PS3", "PS5", "PS4", "DUALSHOCK 4", "DualSense", "Nacon Revolution Unlimited Pro Controller"], + DEVICE_STEAMDECK_CONTROLLER: ["Steam"], + DEVICE_SWITCH_CONTROLLER: ["Switch", "Joy-Con", "PowerA Core Controller"], + } + + for device_key in keywords: + for keyword in keywords[device_key]: + if keyword.to_lower() in raw_name.to_lower(): + return device_key + + return DEVICE_GENERIC + + +## Check if there is a connected joypad +func has_joypad() -> bool: + return Input.get_connected_joypads().size() > 0 + + +## Guess the initial input device +func guess_device_name() -> String: + if has_joypad(): + return get_simplified_device_name(get_joy_name(0)) + else: + return DEVICE_KEYBOARD + + +#region Mapping + + +func reset_all_actions() -> void: + InputMap.load_from_project_settings() + for action in InputMap.get_actions(): + var input: InputEvent = get_joypad_input_for_action(action) + if input != null: + joypad_input_changed.emit(action, input) + + input = get_keyboard_input_for_action(action) + if input != null: + keyboard_input_changed.emit(action, input) + + +## Set the key or button for an action +func set_keyboard_or_joypad_input_for_action(action: String, event: InputEvent, swap_if_taken: bool = true) -> void: + if event is InputEventKey or event is InputEventMouse: + set_keyboard_input_for_action(action, event, swap_if_taken) + elif event is InputEventJoypadButton: + set_joypad_input_for_action(action, event, swap_if_taken) + + +## Get the key or button for a given action depending on the current device +func get_keyboard_or_joypad_input_for_action(action: String) -> InputEvent: + if device == DEVICE_KEYBOARD: + return get_keyboard_input_for_action(action) + else: + return get_joypad_input_for_action(action) + + +## Get the key or button for a given action depending on the current device +func get_keyboard_or_joypad_inputs_for_action(action: String) -> Array[InputEvent]: + if device == DEVICE_KEYBOARD: + return get_keyboard_inputs_for_action(action) + else: + return get_joypad_inputs_for_action(action) + + +## Get a text label for a given input +func get_label_for_input(input: InputEvent) -> String: + if input == null: return "" + + if input is InputEventKey: + if input.physical_keycode > 0 : + var keycode: Key = DisplayServer.keyboard_get_keycode_from_physical(input.physical_keycode) if DisplayServer.keyboard_get_current_layout() > -1 else input.physical_keycode + return OS.get_keycode_string(keycode) + elif input.keycode > 0: + return OS.get_keycode_string(input.keycode) + else: + return input.as_text() + + elif input is InputEventMouseButton: + match input.button_index: + MOUSE_BUTTON_LEFT: + return "Mouse Left Button" + MOUSE_BUTTON_MIDDLE: + return "Mouse Middle Button" + MOUSE_BUTTON_RIGHT: + return "Mouse Right Button" + return "Mouse Button %d" % input.button_index + + elif input is InputEventJoypadButton: + var labels = [] + match _last_known_granular_joypad_device: + DEVICE_XBOX_CONTROLLER, DEVICE_GENERIC: + labels = XBOX_BUTTON_LABELS + SUB_DEVICE_XBOX_ONE_CONTROLLER: + labels = XBOX_ONE_BUTTON_LABELS + SUB_DEVICE_XBOX_SERIES_CONTROLLER: + labels = XBOX_SERIES_BUTTON_LABELS + SUB_DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER, SUB_DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER: + labels = SWITCH_BUTTON_LABELS + DEVICE_SWITCH_CONTROLLER: + labels = SWITCH_EXTENDED_GAMEPAD_BUTTON_LABELS + SUB_DEVICE_PLAYSTATION3_CONTROLLER, SUB_DEVICE_PLAYSTATION4_CONTROLLER: + labels = PLAYSTATION_3_4_BUTTON_LABELS + DEVICE_PLAYSTATION_CONTROLLER, SUB_DEVICE_PLAYSTATION5_CONTROLLER: + labels = PLAYSTATION_5_BUTTON_LABELS + DEVICE_STEAMDECK_CONTROLLER: + labels = STEAMDECK_BUTTON_LABELS + if input.button_index < labels.size(): + return "%s Button" % labels[input.button_index] + else: + return "Button %d" % input.button_index + + elif input is InputEventJoypadMotion: + var motion: InputEventJoypadMotion = input as InputEventJoypadMotion + match motion.axis: + JOY_AXIS_LEFT_X: + return "Left Stick %s" % ("Left" if motion.axis_value < 0 else "Right") + JOY_AXIS_LEFT_Y: + return "Left Stick %s" % ("Up" if motion.axis_value < 0 else "Down") + JOY_AXIS_RIGHT_X: + return "Right Stick %s" % ("Left" if motion.axis_value < 0 else "Right") + JOY_AXIS_RIGHT_Y: + return "Right Stick %s" % ("Up" if motion.axis_value < 0 else "Down") + JOY_AXIS_TRIGGER_LEFT: + return "Left Trigger" + JOY_AXIS_TRIGGER_RIGHT: + return "Right Trigger" + + return input.as_text() + + +## Serialize a single action's inputs. +func serialize_inputs_for_action(action: StringName) -> String: + var action_inputs: PackedStringArray = [] + var inputs: Array[InputEvent] = InputMap.action_get_events(action) + for input in inputs: + if input is InputEventKey: + var s: String = get_label_for_input(input) + var modifiers: Array[String] = [] + if input.alt_pressed: + modifiers.append("alt") + if input.shift_pressed: + modifiers.append("shift") + if input.ctrl_pressed: + modifiers.append("ctrl") + if input.meta_pressed: + modifiers.append("meta") + if not modifiers.is_empty(): + s += "|" + ",".join(modifiers) + action_inputs.append("key:%s" % s) + elif input is InputEventMouseButton: + action_inputs.append("mouse:%d" % input.button_index) + elif input is InputEventJoypadButton: + action_inputs.append("joypad:%d" % input.button_index) + elif input is InputEventJoypadMotion: + action_inputs.append("joypad:%d|%f" % [input.axis, input.axis_value]) + + return ";".join(action_inputs) + + +## Serialize a list of action inputs to string. If actions is empty then it will serialize +## all actions. +func serialize_inputs_for_actions(actions: PackedStringArray = []) -> String: + if actions == null or actions.is_empty(): + actions = InputMap.get_actions() + + var map: Dictionary = {} + for action in actions: + map[action] = serialize_inputs_for_action(action) + + return JSON.stringify({ + version = SERIAL_VERSION, + map = map + }) + + +## Deserialize a single action's inputs. +func deserialize_inputs_for_action(action: String, string: String) -> void: + InputMap.action_erase_events(action) + var action_inputs: PackedStringArray = string.split(";") + for action_input in action_inputs: + var bits: PackedStringArray = action_input.split(":") + + # Ignore any empty actions + if bits.size() < 2: continue + + var input_type: String = bits[0] + var input_details: String = bits[1] + + match input_type: + "key": + var keyboard_input = InputEventKey.new() + if "|" in input_details: + var detail_bits = input_details.split("|") + keyboard_input.keycode = OS.find_keycode_from_string(detail_bits[0]) + detail_bits = detail_bits[1].split(",") + if detail_bits.has("alt"): + keyboard_input.alt_pressed = true + if detail_bits.has("shift"): + keyboard_input.shift_pressed = true + if detail_bits.has("ctrl"): + keyboard_input.ctrl_pressed = true + if detail_bits.has("meta"): + keyboard_input.meta_pressed = true + else: + keyboard_input.keycode = OS.find_keycode_from_string(input_details) + InputMap.action_add_event(action, keyboard_input) + keyboard_input_changed.emit(action, keyboard_input) + + "mouse": + var mouse_input = InputEventMouseButton.new() + mouse_input.button_index = int(input_details) + InputMap.action_add_event(action, mouse_input) + keyboard_input_changed.emit(action, mouse_input) + + "joypad": + if "|" in str(input_details): + var joypad_motion_input = InputEventJoypadMotion.new() + var joypad_bits = input_details.split("|") + joypad_motion_input.axis = int(joypad_bits[0]) + joypad_motion_input.axis_value = float(joypad_bits[1]) + InputMap.action_add_event(action, joypad_motion_input) + joypad_input_changed.emit(action, joypad_motion_input) + else: + var joypad_input = InputEventJoypadButton.new() + joypad_input.button_index = int(input_details) + InputMap.action_add_event(action, joypad_input) + joypad_input_changed.emit(action, joypad_input) + + +## Deserialise a list of actions' inputs. +func deserialize_inputs_for_actions(string: String) -> void: + var data: Dictionary = JSON.parse_string(string) + + # Use legacy deserialization + if not data.has("version"): + _deprecated_deserialize_inputs_for_actions(string) + return + + # Version 1 + for action in data.map.keys(): + deserialize_inputs_for_action(action, data.map[action]) + + +# Load inputs from a serialized string. [deprecated] +func _deprecated_deserialize_inputs_for_actions(string: String) -> void: + var map: Dictionary = JSON.parse_string(string) + for action in map.keys(): + InputMap.action_erase_events(action) + + for key in map[action]["keyboard"]: + var keyboard_input = InputEventKey.new() + if "|" in key: + var bits = key.split("|") + keyboard_input.keycode = OS.find_keycode_from_string(bits[0]) + bits = bits[1].split(",") + if bits.has("alt"): + keyboard_input.alt_pressed = true + if bits.has("shift"): + keyboard_input.shift_pressed = true + if bits.has("ctrl"): + keyboard_input.ctrl_pressed = true + if bits.has("meta"): + keyboard_input.meta_pressed = true + else: + keyboard_input.keycode = OS.find_keycode_from_string(key) + InputMap.action_add_event(action, keyboard_input) + + for button_index in map[action]["mouse"]: + var mouse_input = InputEventMouseButton.new() + mouse_input.button_index = int(button_index) + InputMap.action_add_event(action, mouse_input) + + for button_index_or_motion in map[action]["joypad"]: + if "|" in str(button_index_or_motion): + var joypad_motion_input = InputEventJoypadMotion.new() + var bits = button_index_or_motion.split("|") + joypad_motion_input.axis = int(bits[0]) + joypad_motion_input.axis_value = float(bits[1]) + InputMap.action_add_event(action, joypad_motion_input) + else: + var joypad_input = InputEventJoypadButton.new() + joypad_input.button_index = int(button_index_or_motion) + InputMap.action_add_event(action, joypad_input) + + +#endregion + +#region Keyboard/mouse input + + +## Get all of the keys/mouse buttons used for an action. +func get_keyboard_inputs_for_action(action: String) -> Array[InputEvent]: + return InputMap.action_get_events(action).filter(func(event): + return event is InputEventKey or event is InputEventMouseButton + ) + + +## Get the first key for an action +func get_keyboard_input_for_action(action: String) -> InputEvent: + var inputs: Array[InputEvent] = get_keyboard_inputs_for_action(action) + return null if inputs.is_empty() else inputs[0] + + +## Set the key used for an action +func set_keyboard_input_for_action(action: String, input: InputEvent, swap_if_taken: bool = true) -> Error: + return _update_keyboard_input_for_action(action, input, swap_if_taken, null) + + +## Replace a specific key with another key +func replace_keyboard_input_for_action(action: String, current_input: InputEvent, input: InputEvent, swap_if_taken: bool = true) -> Error: + return _update_keyboard_input_for_action(action, input, swap_if_taken, current_input) + + +## Replace a specific key, given its index +func replace_keyboard_input_at_index(action: String, index: int, input: InputEvent, swap_if_taken: bool = true) -> Error: + var inputs: Array[InputEvent] = get_keyboard_inputs_for_action(action) + var replacing_input = InputEventKey.new() if (inputs.is_empty() or inputs.size() <= index) else inputs[index] + return _update_keyboard_input_for_action(action, input, swap_if_taken, replacing_input) + + +func _update_keyboard_input_for_action(action: String, input: InputEvent, swap_if_taken: bool, replacing_input: InputEvent = null) -> Error: + if not (input is InputEventKey or input is InputEventMouseButton): return ERR_INVALID_DATA + + var is_valid_keyboard_event = func(event): + return event is InputEventKey or event is InputEventMouseButton + + return _update_input_for_action(action, input, swap_if_taken, replacing_input, is_valid_keyboard_event, keyboard_input_changed) + + +#endregion + +#region Joypad input + + +## Get all buttons used for an action +func get_joypad_inputs_for_action(action: String) -> Array[InputEvent]: + return InputMap.action_get_events(action).filter(func(event): + return event is InputEventJoypadButton or event is InputEventJoypadMotion + ) + + +## Get the first button for an action +func get_joypad_input_for_action(action: String) -> InputEvent: + var buttons: Array[InputEvent] = get_joypad_inputs_for_action(action) + return null if buttons.is_empty() else buttons[0] + + +## Set the button for an action +func set_joypad_input_for_action(action: String, input: InputEvent, swap_if_taken: bool = true) -> Error: + return _update_joypad_input_for_action(action, input, swap_if_taken, null) + + +## Replace a specific button for an action +func replace_joypad_input_for_action(action: String, current_input: InputEvent, input: InputEventJoypadButton, swap_if_taken: bool = true) -> Error: + return _update_joypad_input_for_action(action, input, swap_if_taken, current_input) + + +## Replace a button, given its index +func replace_joypad_input_at_index(action: String, index: int, input: InputEvent, swap_if_taken: bool = true) -> Error: + var inputs: Array[InputEvent] = get_joypad_inputs_for_action(action) + var replacing_input + if inputs.is_empty() or inputs.size() <= index: + replacing_input = InputEventJoypadButton.new() + replacing_input.button_index = JOY_BUTTON_INVALID + else: + replacing_input = inputs[index] + return _update_joypad_input_for_action(action, input, swap_if_taken, replacing_input) + + +## Set the action used for a button +func _update_joypad_input_for_action(action: String, input: InputEvent, swap_if_taken: bool = true, replacing_input: InputEvent = null) -> Error: + var is_valid_keyboard_event = func(event): + return event is InputEventJoypadButton or event is InputEventJoypadMotion + + return _update_input_for_action(action, input, swap_if_taken, replacing_input, is_valid_keyboard_event, joypad_input_changed) + + +func _update_input_for_action(action: String, input: InputEvent, swap_if_taken: bool, replacing_input: InputEvent, check_is_valid: Callable, did_change_signal: Signal) -> Error: + # Find any action that is already mapped to this input + var clashing_action = "" + var clashing_event + if swap_if_taken: + for other_action in InputMap.get_actions(): + if other_action == action: continue + + for event in InputMap.action_get_events(other_action): + if event.is_match(input): + clashing_action = other_action + clashing_event = event + + # Find the key based event for the target action + var action_events: Array[InputEvent] = InputMap.action_get_events(action) + var is_replacing: bool = false + for i in range(0, action_events.size()): + var event: InputEvent = action_events[i] + if check_is_valid.call(event): + if replacing_input != null and not event.is_match(replacing_input): + continue + + # Remap the other event if there is a clashing one + if clashing_action: + _update_input_for_action(clashing_action, event, false, clashing_event, check_is_valid, did_change_signal) + + # Replace the event + action_events[i] = input + is_replacing = true + break + + # If we were trying to replace something but didn't find it then just add it to the end + if not is_replacing: + action_events.append(input) + + # Apply the changes + InputMap.action_erase_events(action) + for event in action_events: + if event != null: + InputMap.action_add_event(action, event) + + did_change_signal.emit(action, input) + + return OK + + +#endregion + +#region Rumbling + + +func rumble_small(target_device: int = 0) -> void: + Input.start_joy_vibration(target_device, 0.4, 0, 0.1) + + +func rumble_medium(target_device: int = 0) -> void: + Input.start_joy_vibration(target_device, 0, 0.7, 0.1) + + +func rumble_large(target_device: int = 0) -> void: + Input.start_joy_vibration(target_device, 0, 1, 0.1) + + +func start_rumble_small(target_device: int = 0) -> void: + Input.start_joy_vibration(target_device, 0.4, 0, 0) + + +func start_rumble_medium(target_device: int = 0) -> void: + Input.start_joy_vibration(target_device, 0, 0.7, 0) + + +func start_rumble_large(target_device: int = 0) -> void: + Input.start_joy_vibration(target_device, 0, 1, 0) + + +func stop_rumble(target_device: int = 0) -> void: + Input.stop_joy_vibration(target_device) + + +#endregion diff --git a/Zennysoft.Game.Ma/addons/input_helper/input_helper.gd.uid b/Zennysoft.Game.Ma/addons/input_helper/input_helper.gd.uid new file mode 100644 index 00000000..3286da83 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/input_helper.gd.uid @@ -0,0 +1 @@ +uid://cholww48njaeh diff --git a/Zennysoft.Game.Ma/addons/input_helper/plugin.cfg b/Zennysoft.Game.Ma/addons/input_helper/plugin.cfg new file mode 100644 index 00000000..3953a3ee --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Input Helper" +description="Detect which input device the player is using and manage input actions" +author="Nathan Hoad" +version="4.7.0" +script="plugin.gd" diff --git a/Zennysoft.Game.Ma/addons/input_helper/plugin.gd b/Zennysoft.Game.Ma/addons/input_helper/plugin.gd new file mode 100644 index 00000000..ea127b7e --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/plugin.gd @@ -0,0 +1,112 @@ +@tool +extends EditorPlugin + + +const REMOTE_RELEASES_URL = "https://api.github.com/repos/nathanhoad/godot_input_helper/releases" +const LOCAL_CONFIG_PATH = "res://addons/input_helper/plugin.cfg" + +const DownloadDialogScene = preload("res://addons/input_helper/views/download_dialog.tscn") + + +var http_request: HTTPRequest = HTTPRequest.new() +var next_version: String = "" + + +func _enter_tree(): + add_autoload_singleton("InputHelper", "res://addons/input_helper/input_helper.gd") + + # Configure settings + InputHelperSettings.prepare() + + # Check for updates on GitHub + get_editor_interface().get_base_control().add_child(http_request) + http_request.request_completed.connect(_on_http_request_request_completed) + http_request.request(REMOTE_RELEASES_URL) + + +func _exit_tree(): + remove_autoload_singleton("InputHelper") + + if next_version != "": + remove_tool_menu_item("Update Input Helper to v%s" % next_version) + + +# Get the current version +func get_version() -> String: + var config: ConfigFile = ConfigFile.new() + config.load(LOCAL_CONFIG_PATH) + return config.get_value("plugin", "version") + + +# Convert a version number to an actually comparable number +func version_to_number(version: String) -> int: + var bits = version.split(".") + return bits[0].to_int() * 1000000 + bits[1].to_int() * 1000 + bits[2].to_int() + + +### Signals + + +func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void: + http_request.queue_free() + + if result != HTTPRequest.RESULT_SUCCESS: return + + var current_version: String = get_version() + + # Work out the next version from the releases information on GitHub + var response = JSON.parse_string(body.get_string_from_utf8()) + if typeof(response) != TYPE_ARRAY: return + + # GitHub releases are in order of creation, not order of version + var versions = (response as Array).filter(func(release): + var version: String = release.tag_name.substr(1) + return version_to_number(version) > version_to_number(current_version) + ) + if versions.size() > 0: + next_version = versions[0].tag_name.substr(1) + add_tool_menu_item("Update Input Helper to v%s" % next_version, _update_input_helper) + + +func _update_input_helper() -> void: + var download_dialog := DownloadDialogScene.instantiate() + download_dialog.next_version = next_version + + var scale: float = get_editor_interface().get_editor_scale() + download_dialog.min_size = Vector2(300, 250) * scale + + download_dialog.update_finished.connect(_on_download_dialog_update_finished) + download_dialog.update_failed.connect(_on_download_dialog_update_failed) + + get_editor_interface().get_base_control().add_child(download_dialog) + download_dialog.show() + + +func _on_download_dialog_update_finished() -> void: + remove_tool_menu_item("Update Input Helper to v%s" % next_version) + + get_editor_interface().get_resource_filesystem().scan() + + print_rich("\n[b]Updated Input Helper to v%s[/b]\n" % next_version) + + var finished_dialog: AcceptDialog = AcceptDialog.new() + finished_dialog.dialog_text = "Your Input Helper is now up to date." + + var restart_addon = func(): + finished_dialog.queue_free() + get_editor_interface().call_deferred("set_plugin_enabled", "input_helper", true) + get_editor_interface().set_plugin_enabled("input_helper", false) + + finished_dialog.canceled.connect(restart_addon) + finished_dialog.confirmed.connect(restart_addon) + get_editor_interface().get_base_control().add_child(finished_dialog) + finished_dialog.popup_centered() + + +func _on_download_dialog_update_failed() -> void: + var failed_dialog: AcceptDialog = AcceptDialog.new() + failed_dialog.dialog_text = "There was a problem downloading the update." + failed_dialog.canceled.connect(func(): failed_dialog.queue_free()) + failed_dialog.confirmed.connect(func(): failed_dialog.queue_free()) + get_editor_interface().get_base_control().add_child(failed_dialog) + failed_dialog.popup_centered() diff --git a/Zennysoft.Game.Ma/addons/input_helper/plugin.gd.uid b/Zennysoft.Game.Ma/addons/input_helper/plugin.gd.uid new file mode 100644 index 00000000..a13a6434 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/plugin.gd.uid @@ -0,0 +1 @@ +uid://cul6evy00vr55 diff --git a/Zennysoft.Game.Ma/addons/input_helper/settings.gd b/Zennysoft.Game.Ma/addons/input_helper/settings.gd new file mode 100644 index 00000000..d4f48176 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/settings.gd @@ -0,0 +1,42 @@ +class_name InputHelperSettings extends Node + +const USE_GRANULAR_DEVICE_IDENTIFIERS = "devices/use_granular_device_identifiers" + +const SETTINGS_CONFIGURATION = { + USE_GRANULAR_DEVICE_IDENTIFIERS: { + value = false, + type = TYPE_BOOL, + is_advanced = true + }, +} + + +static func prepare() -> void: + for key: String in SETTINGS_CONFIGURATION: + var setting_config: Dictionary = SETTINGS_CONFIGURATION[key] + var setting_name: String = "input_helper/%s" % key + if not ProjectSettings.has_setting(setting_name): + ProjectSettings.set_setting(setting_name, setting_config.value) + ProjectSettings.set_initial_value(setting_name, setting_config.value) + ProjectSettings.add_property_info({ + "name" = setting_name, + "type" = setting_config.type, + "hint" = setting_config.get("hint", PROPERTY_HINT_NONE), + "hint_string" = setting_config.get("hint_string", "") + }) + ProjectSettings.set_as_basic(setting_name, not setting_config.has("is_advanced")) + ProjectSettings.set_as_internal(setting_name, setting_config.has("is_hidden")) + + +static func set_setting(key: String, value) -> void: + if get_setting(key, value) != value: + ProjectSettings.set_setting("input_helper/%s" % key, value) + ProjectSettings.set_initial_value("input_helper/%s" % key, SETTINGS_CONFIGURATION[key].value) + ProjectSettings.save() + + +static func get_setting(key: String, default): + if ProjectSettings.has_setting("input_helper/%s" % key): + return ProjectSettings.get_setting("input_helper/%s" % key) + else: + return default diff --git a/Zennysoft.Game.Ma/addons/input_helper/settings.gd.uid b/Zennysoft.Game.Ma/addons/input_helper/settings.gd.uid new file mode 100644 index 00000000..5880d1b9 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/settings.gd.uid @@ -0,0 +1 @@ +uid://yqplm6a6focp diff --git a/Zennysoft.Game.Ma/addons/input_helper/views/download_dialog.gd b/Zennysoft.Game.Ma/addons/input_helper/views/download_dialog.gd new file mode 100644 index 00000000..13503ae4 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/views/download_dialog.gd @@ -0,0 +1,30 @@ +@tool + +extends AcceptDialog + + +signal update_finished() +signal update_failed() + + +@onready var download_update_panel := $DownloadUpdatePanel + + +var next_version: String + + +func _ready() -> void: + download_update_panel.next_version = next_version + + +### Signals + + +func _on_download_update_panel_updated(updated_to_version) -> void: + update_finished.emit() + queue_free() + + +func _on_download_update_panel_failed() -> void: + update_failed.emit() + queue_free() diff --git a/Zennysoft.Game.Ma/addons/input_helper/views/download_dialog.gd.uid b/Zennysoft.Game.Ma/addons/input_helper/views/download_dialog.gd.uid new file mode 100644 index 00000000..ac63efac --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/views/download_dialog.gd.uid @@ -0,0 +1 @@ +uid://1t3qhgrro2es diff --git a/Zennysoft.Game.Ma/addons/input_helper/views/download_dialog.tscn b/Zennysoft.Game.Ma/addons/input_helper/views/download_dialog.tscn new file mode 100644 index 00000000..451a451d --- /dev/null +++ b/Zennysoft.Game.Ma/addons/input_helper/views/download_dialog.tscn @@ -0,0 +1,14 @@ +[gd_scene load_steps=3 format=3 uid="uid://bownbkcmm43gn"] + +[ext_resource type="PackedScene" uid="uid://b7mst0qu7vjk1" path="res://addons/input_helper/components/download_update_panel.tscn" id="1_37q37"] +[ext_resource type="Script" uid="uid://1t3qhgrro2es" path="res://addons/input_helper/views/download_dialog.gd" id="1_ltktf"] + +[node name="DownloadDialog" type="AcceptDialog"] +initial_position = 2 +ok_button_text = "Close" +script = ExtResource("1_ltktf") + +[node name="DownloadUpdatePanel" parent="." instance=ExtResource("1_37q37")] + +[connection signal="failed" from="DownloadUpdatePanel" to="." method="_on_download_update_panel_failed"] +[connection signal="updated" from="DownloadUpdatePanel" to="." method="_on_download_update_panel_updated"] diff --git a/Zennysoft.Game.Ma/project.godot b/Zennysoft.Game.Ma/project.godot index 5b27fe12..e9463fe2 100644 --- a/Zennysoft.Game.Ma/project.godot +++ b/Zennysoft.Game.Ma/project.godot @@ -23,6 +23,7 @@ DialogueManager="*res://addons/dialogue_manager/dialogue_manager.gd" DialogueController="*res://src/game/DialogueController.cs" AudioManager="*res://src/audio/AudioManager.cs" BgmPlayer="*res://src/audio/BGMPlayer.cs" +InputHelper="*res://addons/input_helper/input_helper.gd" [dialogue_manager] @@ -43,7 +44,7 @@ project/assembly_name="Ma" [editor_plugins] -enabled=PackedStringArray("res://addons/dialogue_manager/plugin.cfg") +enabled=PackedStringArray("res://addons/dialogue_manager/plugin.cfg", "res://addons/input_helper/plugin.cfg") [file_customization] @@ -72,9 +73,7 @@ texture={ ui_accept={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) -, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":false,"script":null) -, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":3,"pressure":0.0,"pressed":false,"script":null) -, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":true,"script":null) ] } ui_select={ @@ -83,34 +82,40 @@ ui_select={ } ui_cancel={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194305,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) -, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194309,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) -, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":false,"script":null) +"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":true,"script":null) , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null) ] } +ui_focus_next={ +"deadzone": 0.5, +"events": [] +} +ui_focus_prev={ +"deadzone": 0.5, +"events": [] +} ui_left={ "deadzone": 0.5, -"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null) -, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null) ] } ui_right={ "deadzone": 0.5, -"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":14,"pressure":0.0,"pressed":false,"script":null) -, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":14,"pressure":0.0,"pressed":false,"script":null) ] } ui_up={ "deadzone": 0.5, -"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":11,"pressure":0.0,"pressed":false,"script":null) -, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":11,"pressure":0.0,"pressed":false,"script":null) ] } ui_down={ "deadzone": 0.5, -"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null) -, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194322,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null) ] } MoveUp={ diff --git a/Zennysoft.Game.Ma/src/app/App.cs b/Zennysoft.Game.Ma/src/app/App.cs index ae7fb22d..4ea4a74d 100644 --- a/Zennysoft.Game.Ma/src/app/App.cs +++ b/Zennysoft.Game.Ma/src/app/App.cs @@ -4,8 +4,8 @@ using Chickensoft.GodotNodeInterfaces; using Chickensoft.Introspection; using Godot; using Godot.Collections; +using NathanHoad; using SimpleInjector.Lifestyles; -using System.IO; using System.IO.Abstractions; using System.Linq; using System.Threading.Tasks; @@ -47,11 +47,11 @@ public partial class App : Node, IApp private bool _loadingGame = false; private bool _loadingEnemyViewer = false; private string _optionsSavePath = string.Empty; + private string _controllerSavePath = string.Empty; private ISaveFileManager _saveFileManager; public void Initialize() { - MainMenu.Hide(); _container = new SimpleInjector.Container(); _container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle(); _container.RegisterSingleton(); @@ -61,6 +61,7 @@ public partial class App : Node, IApp _saveFileManager = _container.GetInstance(); _optionsSavePath = $"{OS.GetUserDataDir()}/options.json"; + _controllerSavePath = $"{OS.GetUserDataDir()}/controls.json"; Task.Run(() => _saveFileManager.ReadFromFile(_optionsSavePath).ContinueWith((data) => { @@ -68,6 +69,12 @@ public partial class App : Node, IApp OptionsMenu.CallDeferred("Load", data.Result); })); + Task.Run(() => _saveFileManager.ReadFromFile(_controllerSavePath).ContinueWith((data) => + { + if (data.IsCompletedSuccessfully) + OptionsMenu.Controller.CallDeferred(nameof(OptionsMenu.Controller.LoadControllerInput), data.Result); + })); + MainMenu.StartGame += OnStartGame; MainMenu.EnemyViewer += OnEnemyViewer; MainMenu.Options += OnOptions; @@ -91,8 +98,10 @@ public partial class App : Node, IApp { var saveFileManager = _container.GetInstance(); await saveFileManager.WriteToFile(OptionsMenu.OptionsData, _optionsSavePath); - MainMenu.Show(); + var controllerOutput = InputHelper.SerializeInputsForActions(); + await saveFileManager.WriteToFile(controllerOutput, _controllerSavePath); OptionsMenu.Hide(); + MainMenu.OptionsButton.GrabFocus(); } private void OnGameLoaded(string sceneName) @@ -141,7 +150,6 @@ public partial class App : Node, IApp }); AppLogic.Start(); - MainMenu.Show(); } public override void _Process(double delta) @@ -170,7 +178,7 @@ public partial class App : Node, IApp private async void OnOptions() { OptionsMenu.Show(); - MainMenu.Hide(); + OptionsMenu.MasterVolumeSlider.GrabFocus(); } public void OnQuit() => AppLogic.Input(new AppLogic.Input.QuitGame()); diff --git a/Zennysoft.Game.Ma/src/menu/MainMenu.tscn b/Zennysoft.Game.Ma/src/menu/MainMenu.tscn index 021b0a5c..aba6810f 100644 --- a/Zennysoft.Game.Ma/src/menu/MainMenu.tscn +++ b/Zennysoft.Game.Ma/src/menu/MainMenu.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=2 format=3 uid="uid://rfvnddfqufho"] +[gd_scene load_steps=3 format=3 uid="uid://rfvnddfqufho"] [ext_resource type="Script" uid="uid://14b7o2c6cgry" path="res://src/menu/MainMenu.cs" id="1_y6722"] +[ext_resource type="Shortcut" uid="uid://dumkrjur22k2a" path="res://src/ui/ButtonShortcut.tres" id="2_7fwjx"] [node name="MainMenu" type="Control"] layout_mode = 3 @@ -27,7 +28,6 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -mouse_filter = 0 theme_override_constants/margin_left = 100 theme_override_constants/margin_top = 100 theme_override_constants/margin_right = 100 @@ -37,41 +37,56 @@ theme_override_constants/margin_bottom = 100 layout_mode = 2 size_flags_horizontal = 0 size_flags_vertical = 0 -mouse_filter = 0 [node name="StartGameButton" type="Button" parent="MarginContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 +focus_neighbor_bottom = NodePath("../EnemyViewerButton") +focus_next = NodePath("../EnemyViewerButton") theme_override_colors/font_focus_color = Color(0.976471, 0.827451, 0, 1) +shortcut = ExtResource("2_7fwjx") text = "Start Game" [node name="EnemyViewerButton" type="Button" parent="MarginContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 focus_neighbor_bottom = NodePath("../GalleryButton") +focus_next = NodePath("../GalleryButton") +focus_previous = NodePath("../StartGameButton") theme_override_colors/font_focus_color = Color(0.976471, 0.827451, 0, 1) +shortcut = ExtResource("2_7fwjx") text = "Enemy Viewer" [node name="GalleryButton" type="Button" parent="MarginContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 focus_neighbor_top = NodePath("../EnemyViewerButton") -focus_neighbor_bottom = NodePath("../QuitButton") +focus_neighbor_bottom = NodePath("../OptionsButton") +focus_next = NodePath("../OptionsButton") +focus_previous = NodePath("../EnemyViewerButton") theme_override_colors/font_focus_color = Color(0.976471, 0.827451, 0, 1) +shortcut = ExtResource("2_7fwjx") text = "Gallery" [node name="OptionsButton" type="Button" parent="MarginContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 -focus_neighbor_top = NodePath("../EnemyViewerButton") +focus_neighbor_top = NodePath("../GalleryButton") focus_neighbor_bottom = NodePath("../QuitButton") +focus_next = NodePath("../QuitButton") +focus_previous = NodePath("../GalleryButton") theme_override_colors/font_focus_color = Color(0.976471, 0.827451, 0, 1) +shortcut = ExtResource("2_7fwjx") text = "Options" [node name="QuitButton" type="Button" parent="MarginContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 -focus_neighbor_top = NodePath("../GalleryButton") +focus_neighbor_top = NodePath("../OptionsButton") +focus_neighbor_bottom = NodePath(".") +focus_next = NodePath(".") +focus_previous = NodePath("../OptionsButton") theme_override_colors/font_focus_color = Color(0.976471, 0.827451, 0, 1) +shortcut = ExtResource("2_7fwjx") text = "Quit " diff --git a/Zennysoft.Game.Ma/src/options/InputMapButton.cs b/Zennysoft.Game.Ma/src/options/InputMapButton.cs new file mode 100644 index 00000000..0590d3f4 --- /dev/null +++ b/Zennysoft.Game.Ma/src/options/InputMapButton.cs @@ -0,0 +1,19 @@ +using Godot; + +namespace Zennysoft.Game.Ma; + +public abstract partial class InputMapButton : Button +{ + [Signal] public delegate void RemapEventHandler(InputMapButton buttonBeingRemapped); + + public string Action { get; set; } + + public InputEvent InputEvent { get; set; } + + public InputMapButton() + { + Pressed += RemapButton_Pressed; + } + + private void RemapButton_Pressed() => EmitSignal(SignalName.Remap, this); +} diff --git a/Zennysoft.Game.Ma/src/options/InputMapButton.cs.uid b/Zennysoft.Game.Ma/src/options/InputMapButton.cs.uid new file mode 100644 index 00000000..72829c1e --- /dev/null +++ b/Zennysoft.Game.Ma/src/options/InputMapButton.cs.uid @@ -0,0 +1 @@ +uid://bhfrdlnv7xyf6 diff --git a/Zennysoft.Game.Ma/src/options/InputMapper.cs b/Zennysoft.Game.Ma/src/options/InputMapper.cs new file mode 100644 index 00000000..88d52f88 --- /dev/null +++ b/Zennysoft.Game.Ma/src/options/InputMapper.cs @@ -0,0 +1,191 @@ +using Chickensoft.AutoInject; +using Chickensoft.Introspection; +using Godot; +using NathanHoad; +using SimpleInjector.Lifestyles; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; +using Zennysoft.Game.Abstractions; +using Zennysoft.Game.Implementation; + +namespace Zennysoft.Game.Ma; + +[Meta(typeof(IAutoNode))] +public partial class InputMapper : PanelContainer +{ + public override void _Notification(int what) => this.Notify(what); + + [Node] public VBoxContainer ActionList { get; set; } + + [Node] public KeyboardRemapButton MoveForwardKeyboard { get; set; } + [Node] public JoypadRemapButton MoveForwardController { get; set; } + [Node] public KeyboardRemapButton MoveLeftKeyboard { get; set; } + [Node] public JoypadRemapButton MoveLeftController { get; set; } + [Node] public KeyboardRemapButton MoveRightKeyboard { get; set; } + [Node] public JoypadRemapButton MoveRightController { get; set; } + [Node] public KeyboardRemapButton MoveBackwardKeyboard { get; set; } + [Node] public JoypadRemapButton MoveBackwardController { get; set; } + [Node] public KeyboardRemapButton StrafeLeftKeyboard { get; set; } + [Node] public JoypadRemapButton StrafeLeftController { get; set; } + [Node] public KeyboardRemapButton StrafeRightKeyboard { get; set; } + [Node] public JoypadRemapButton StrafeRightController { get; set; } + [Node] public KeyboardRemapButton AttackKeyboard { get; set; } + [Node] public JoypadRemapButton AttackController { get; set; } + [Node] public KeyboardRemapButton InteractKeyboard { get; set; } + [Node] public JoypadRemapButton InteractController { get; set; } + [Node] public KeyboardRemapButton InventoryKeyboard { get; set; } + [Node] public JoypadRemapButton InventoryController { get; set; } + [Node] public KeyboardRemapButton SortKeyboard { get; set; } + [Node] public JoypadRemapButton SortController { get; set; } + + private Button _remappingButton = null; + private InputEvent _remappingAction = null; + + private List _actionJoyMap = []; + private List _actionKeyMap = []; + + private SimpleInjector.Container _container; + private ISaveFileManager _saveFileManager; + + [Signal] public delegate void SaveControllerInputEventHandler(); + + public void OnReady() + { + _container = new SimpleInjector.Container(); + _container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle(); + _container.RegisterSingleton(); + _container.RegisterSingleton(); + _saveFileManager = _container.GetInstance(); + + MoveForwardController.Action = GameInputs.MoveUp; + MoveForwardKeyboard.Action = GameInputs.MoveUp; + MoveLeftController.Action = GameInputs.MoveLeft; + MoveLeftKeyboard.Action = GameInputs.MoveLeft; + MoveRightController.Action = GameInputs.MoveRight; + MoveRightKeyboard.Action = GameInputs.MoveRight; + MoveBackwardController.Action = GameInputs.MoveDown; + MoveBackwardKeyboard.Action = GameInputs.MoveDown; + StrafeLeftController.Action = GameInputs.StrafeLeft; + StrafeLeftKeyboard.Action = GameInputs.StrafeLeft; + StrafeRightController.Action = GameInputs.StrafeRight; + StrafeRightKeyboard.Action = GameInputs.StrafeRight; + AttackController.Action = GameInputs.Attack; + AttackKeyboard.Action = GameInputs.Attack; + InteractController.Action = GameInputs.Interact; + InteractKeyboard.Action = GameInputs.Interact; + InventoryController.Action = GameInputs.Inventory; + InventoryKeyboard.Action = GameInputs.Inventory; + SortController.Action = GameInputs.InventorySort; + SortKeyboard.Action = GameInputs.InventorySort; + + _actionJoyMap.Add(MoveForwardController); + _actionJoyMap.Add(MoveLeftController); + _actionJoyMap.Add(MoveRightController); + _actionJoyMap.Add(MoveBackwardController); + _actionJoyMap.Add(StrafeLeftController); + _actionJoyMap.Add(StrafeRightController); + _actionJoyMap.Add(AttackController); + _actionJoyMap.Add(InteractController); + _actionJoyMap.Add(InventoryController); + _actionJoyMap.Add(SortController); + + _actionKeyMap.Add(MoveForwardKeyboard); + _actionKeyMap.Add(MoveLeftKeyboard); + _actionKeyMap.Add(MoveRightKeyboard); + _actionKeyMap.Add(MoveBackwardKeyboard); + _actionKeyMap.Add(StrafeLeftKeyboard); + _actionKeyMap.Add(StrafeRightKeyboard); + _actionKeyMap.Add(AttackKeyboard); + _actionKeyMap.Add(InteractKeyboard); + _actionKeyMap.Add(InventoryKeyboard); + _actionKeyMap.Add(SortKeyboard); + + MoveForwardKeyboard.Remap += OnRemap; + MoveForwardController.Remap += OnRemap; + MoveLeftKeyboard.Remap += OnRemap; + MoveLeftController.Remap += OnRemap; + MoveRightKeyboard.Remap += OnRemap; + MoveRightController.Remap += OnRemap; + MoveBackwardKeyboard.Remap += OnRemap; + MoveBackwardController.Remap += OnRemap; + StrafeLeftKeyboard.Remap += OnRemap; + StrafeLeftController.Remap += OnRemap; + StrafeRightKeyboard.Remap += OnRemap; + StrafeRightController.Remap += OnRemap; + AttackKeyboard.Remap += OnRemap; + AttackController.Remap += OnRemap; + InteractKeyboard.Remap += OnRemap; + InteractController.Remap += OnRemap; + InventoryKeyboard.Remap += OnRemap; + InventoryController.Remap += OnRemap; + SortKeyboard.Remap += OnRemap; + SortController.Remap += OnRemap; + + InputHelper.JoypadInputChanged += (string action, InputEvent input) => + { + var buttonChanged = _actionJoyMap.SingleOrDefault(x => x.Action == action); + if (buttonChanged != null) + { + buttonChanged.SetProcessInput(false); + buttonChanged.Text = InputHelper.GetLabelForInput(input); + } + var allButtons = _actionKeyMap.Concat(_actionJoyMap); + foreach (var button in allButtons) + button.Disabled = false; + }; + + InputHelper.KeyboardInputChanged += (string action, InputEvent input) => + { + var buttonChanged = _actionKeyMap.SingleOrDefault(x => x.Action == action); + if (buttonChanged != null) + { + buttonChanged.SetProcessInput(false); + buttonChanged.Text = InputHelper.GetLabelForInput(input); + } + var allButtons = _actionKeyMap.Concat(_actionJoyMap); + foreach (var button in allButtons) + button.Disabled = false; + }; + } + + public void LoadControllerInput(string jsonData) + { + InputHelper.DeserializeInputsForActions(jsonData); + InitializeButtonText(); + } + + public void InitializeButtonText() + { + MoveForwardKeyboard.Text = InputHelper.GetLabelForInput(InputHelper.GetKeyboardInputForAction(GameInputs.MoveUp)); + MoveForwardController.Text = InputHelper.GetLabelForInput(InputHelper.GetJoypadInputForAction(GameInputs.MoveUp)); + MoveLeftKeyboard.Text = InputHelper.GetLabelForInput(InputHelper.GetKeyboardInputForAction(GameInputs.MoveLeft)); + MoveLeftController.Text = InputHelper.GetLabelForInput(InputHelper.GetJoypadInputForAction(GameInputs.MoveLeft)); + MoveRightKeyboard.Text = InputHelper.GetLabelForInput(InputHelper.GetKeyboardInputForAction(GameInputs.MoveRight)); + MoveRightController.Text = InputHelper.GetLabelForInput(InputHelper.GetJoypadInputForAction(GameInputs.MoveRight)); + MoveBackwardKeyboard.Text = InputHelper.GetLabelForInput(InputHelper.GetKeyboardInputForAction(GameInputs.MoveDown)); + MoveBackwardController.Text = InputHelper.GetLabelForInput(InputHelper.GetJoypadInputForAction(GameInputs.MoveDown)); + StrafeLeftKeyboard.Text = InputHelper.GetLabelForInput(InputHelper.GetKeyboardInputForAction(GameInputs.StrafeLeft)); + StrafeLeftController.Text = InputHelper.GetLabelForInput(InputHelper.GetJoypadInputForAction(GameInputs.StrafeLeft)); + StrafeRightKeyboard.Text = InputHelper.GetLabelForInput(InputHelper.GetKeyboardInputForAction(GameInputs.StrafeRight)); + StrafeRightController.Text = InputHelper.GetLabelForInput(InputHelper.GetJoypadInputForAction(GameInputs.StrafeRight)); + AttackKeyboard.Text = InputHelper.GetLabelForInput(InputHelper.GetKeyboardInputForAction(GameInputs.Attack)); + AttackController.Text = InputHelper.GetLabelForInput(InputHelper.GetJoypadInputForAction(GameInputs.Attack)); + InteractKeyboard.Text = InputHelper.GetLabelForInput(InputHelper.GetKeyboardInputForAction(GameInputs.Interact)); + InteractController.Text = InputHelper.GetLabelForInput(InputHelper.GetJoypadInputForAction(GameInputs.Interact)); + InventoryKeyboard.Text = InputHelper.GetLabelForInput(InputHelper.GetKeyboardInputForAction(GameInputs.Inventory)); + InventoryController.Text = InputHelper.GetLabelForInput(InputHelper.GetJoypadInputForAction(GameInputs.Inventory)); + SortKeyboard.Text = InputHelper.GetLabelForInput(InputHelper.GetKeyboardInputForAction(GameInputs.InventorySort)); + SortController.Text = InputHelper.GetLabelForInput(InputHelper.GetJoypadInputForAction(GameInputs.InventorySort)); + } + + private void OnRemap(InputMapButton inputButton) + { + inputButton.Text = "..."; + inputButton.SetProcessInput(true); + var allButtons = _actionKeyMap.Concat(_actionJoyMap); + foreach (var button in allButtons) + button.Disabled = true; + ReleaseFocus(); + } +} diff --git a/Zennysoft.Game.Ma/src/options/InputMapper.cs.uid b/Zennysoft.Game.Ma/src/options/InputMapper.cs.uid new file mode 100644 index 00000000..2c0fb92d --- /dev/null +++ b/Zennysoft.Game.Ma/src/options/InputMapper.cs.uid @@ -0,0 +1 @@ +uid://c6lw5yp8p0wb5 diff --git a/Zennysoft.Game.Ma/src/options/InputMapper.tscn b/Zennysoft.Game.Ma/src/options/InputMapper.tscn new file mode 100644 index 00000000..f1d02dc2 --- /dev/null +++ b/Zennysoft.Game.Ma/src/options/InputMapper.tscn @@ -0,0 +1,361 @@ +[gd_scene load_steps=4 format=3 uid="uid://dk5esf6mm6kte"] + +[ext_resource type="Script" uid="uid://c6lw5yp8p0wb5" path="res://src/options/InputMapper.cs" id="1_rwvs3"] +[ext_resource type="Script" uid="uid://b70br20xue678" path="res://src/options/KeyboardRemapButton.cs" id="2_fmxfy"] +[ext_resource type="Script" uid="uid://bo7vk56h1lr07" path="res://src/options/JoypadRemapButton.cs" id="3_yis0i"] + +[node name="InputMapper" type="PanelContainer"] +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -451.0 +offset_top = -451.0 +offset_right = 449.0 +offset_bottom = 449.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_rwvs3") + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 2 +mouse_filter = 0 +theme_override_constants/margin_left = 25 +theme_override_constants/margin_top = 25 +theme_override_constants/margin_right = 25 +theme_override_constants/margin_bottom = 25 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 3 + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="ActionList" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/separation = 10 + +[node name="Header" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Header"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Actions" +horizontal_alignment = 2 + +[node name="Control" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Header"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Label2" type="Label" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Header"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Keyboard" +horizontal_alignment = 1 + +[node name="Control2" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Header"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Label3" type="Label" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Header"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Controller" +horizontal_alignment = 1 + +[node name="Move Forward" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Forward"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Move Forward" +horizontal_alignment = 2 + +[node name="Control" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Forward"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MoveForwardKeyboard" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Forward"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 35) +layout_mode = 2 +script = ExtResource("2_fmxfy") + +[node name="Control2" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Forward"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MoveForwardController" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Forward"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +script = ExtResource("3_yis0i") + +[node name="Move Left" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Left"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Move Left" +horizontal_alignment = 2 + +[node name="Control" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Left"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MoveLeftKeyboard" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Left"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 35) +layout_mode = 2 +script = ExtResource("2_fmxfy") + +[node name="Control2" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Left"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MoveLeftController" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Left"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +script = ExtResource("3_yis0i") + +[node name="Move Right" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Right"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Move Right" +horizontal_alignment = 2 + +[node name="Control" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Right"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MoveRightKeyboard" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Right"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 35) +layout_mode = 2 +script = ExtResource("2_fmxfy") + +[node name="Control2" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Right"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MoveRightController" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Right"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +script = ExtResource("3_yis0i") + +[node name="Move Backward" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Backward"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Move Backward" +horizontal_alignment = 2 + +[node name="Control" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Backward"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MoveBackwardKeyboard" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Backward"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +script = ExtResource("2_fmxfy") + +[node name="Control2" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Backward"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MoveBackwardController" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Move Backward"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +script = ExtResource("3_yis0i") + +[node name="Strafe Left" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Strafe Left"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Strafe Left" +horizontal_alignment = 2 + +[node name="Control" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Strafe Left"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="StrafeLeftKeyboard" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Strafe Left"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +script = ExtResource("2_fmxfy") + +[node name="Control2" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Strafe Left"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="StrafeLeftController" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Strafe Left"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +script = ExtResource("3_yis0i") + +[node name="Strafe Right" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Strafe Right"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Strafe Right" +horizontal_alignment = 2 + +[node name="Control" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Strafe Right"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="StrafeRightKeyboard" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Strafe Right"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +script = ExtResource("2_fmxfy") + +[node name="Control2" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Strafe Right"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="StrafeRightController" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Strafe Right"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +script = ExtResource("3_yis0i") + +[node name="Attack" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Attack"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Attack" +horizontal_alignment = 2 + +[node name="Control" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Attack"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="AttackKeyboard" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Attack"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 35) +layout_mode = 2 +script = ExtResource("2_fmxfy") + +[node name="Control2" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Attack"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="AttackController" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Attack"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +script = ExtResource("3_yis0i") + +[node name="Interact" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Interact"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Interact" +horizontal_alignment = 2 + +[node name="Control" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Interact"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="InteractKeyboard" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Interact"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 35) +layout_mode = 2 +script = ExtResource("2_fmxfy") + +[node name="Control2" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Interact"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="InteractController" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Interact"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +script = ExtResource("3_yis0i") + +[node name="Open Inventory" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Open Inventory"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Open Inventory" +horizontal_alignment = 2 + +[node name="Control" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Open Inventory"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="InventoryKeyboard" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Open Inventory"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 35) +layout_mode = 2 +script = ExtResource("2_fmxfy") + +[node name="Control2" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Open Inventory"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="InventoryController" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Open Inventory"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +script = ExtResource("3_yis0i") + +[node name="Sort Inventory" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Sort Inventory"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Sort Inventory" +horizontal_alignment = 2 + +[node name="Control" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Sort Inventory"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="SortKeyboard" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Sort Inventory"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 35) +layout_mode = 2 +script = ExtResource("2_fmxfy") + +[node name="Control2" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Sort Inventory"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="SortController" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ActionList/Sort Inventory"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +script = ExtResource("3_yis0i") diff --git a/Zennysoft.Game.Ma/src/options/InputToString.cs.uid b/Zennysoft.Game.Ma/src/options/InputToString.cs.uid new file mode 100644 index 00000000..726dcf37 --- /dev/null +++ b/Zennysoft.Game.Ma/src/options/InputToString.cs.uid @@ -0,0 +1 @@ +uid://bwd066xv65pbi diff --git a/Zennysoft.Game.Ma/src/options/JoypadRemapButton.cs b/Zennysoft.Game.Ma/src/options/JoypadRemapButton.cs new file mode 100644 index 00000000..fb90adb4 --- /dev/null +++ b/Zennysoft.Game.Ma/src/options/JoypadRemapButton.cs @@ -0,0 +1,41 @@ +using Godot; +using NathanHoad; + +namespace Zennysoft.Game.Ma; + +public partial class JoypadRemapButton : InputMapButton +{ + public override void _Ready() + { + SetProcessInput(false); + } + + public override void _Input(InputEvent @event) + { + if (@event.IsActionType() && InputHelper.GetDeviceFromEvent(@event) != "keyboard") + { + if (InputHelper.GetJoypadInputForAction(GameInputs.Pause).IsMatch(@event)) + { + InputHelper.SetJoypadInputForAction(Action, InputEvent); + } + else + { + InputHelper.ReplaceJoypadInputForAction(Action, InputEvent, @event, true); + if (Action == GameInputs.MoveUp) + InputHelper.ReplaceJoypadInputForAction(GameInputs.UiUp, InputEvent, @event, true); + else if (Action == GameInputs.MoveLeft) + InputHelper.ReplaceJoypadInputForAction(GameInputs.UiLeft, InputEvent, @event, true); + else if (Action == GameInputs.MoveRight) + InputHelper.ReplaceJoypadInputForAction(GameInputs.UiRight, InputEvent, @event, true); + else if (Action == GameInputs.MoveDown) + InputHelper.ReplaceJoypadInputForAction(GameInputs.UiDown, InputEvent, @event, true); + else if (Action == GameInputs.Attack) + InputHelper.ReplaceJoypadInputForAction(GameInputs.UiAccept, InputEvent, @event, true); + else if (Action == GameInputs.Interact) + InputHelper.ReplaceJoypadInputForAction(GameInputs.UiCancel, InputEvent, @event, true); + InputEvent = @event; + } + } + AcceptEvent(); + } +} \ No newline at end of file diff --git a/Zennysoft.Game.Ma/src/options/JoypadRemapButton.cs.uid b/Zennysoft.Game.Ma/src/options/JoypadRemapButton.cs.uid new file mode 100644 index 00000000..e65146eb --- /dev/null +++ b/Zennysoft.Game.Ma/src/options/JoypadRemapButton.cs.uid @@ -0,0 +1 @@ +uid://bo7vk56h1lr07 diff --git a/Zennysoft.Game.Ma/src/options/KeyboardRemapButton.cs b/Zennysoft.Game.Ma/src/options/KeyboardRemapButton.cs new file mode 100644 index 00000000..5da3297d --- /dev/null +++ b/Zennysoft.Game.Ma/src/options/KeyboardRemapButton.cs @@ -0,0 +1,42 @@ +using Godot; +using NathanHoad; + +namespace Zennysoft.Game.Ma; + +public partial class KeyboardRemapButton : InputMapButton +{ + public override void _Ready() + { + SetProcessInput(false); + } + + public override void _Input(InputEvent @event) + { + AcceptEvent(); + if (@event.IsActionType() && InputHelper.GetDeviceFromEvent(@event) == "keyboard") + { + if (InputHelper.GetKeyboardInputForAction(GameInputs.Pause).IsMatch(@event)) + { + InputHelper.SetKeyboardInputForAction(Action, InputEvent); + } + else + { + InputHelper.ReplaceKeyboardInputForAction(Action, InputEvent, @event, true); + if (Action == GameInputs.MoveUp) + InputHelper.ReplaceKeyboardInputForAction(GameInputs.UiUp, InputEvent, @event, true); + else if (Action == GameInputs.MoveLeft) + InputHelper.ReplaceKeyboardInputForAction(GameInputs.UiLeft, InputEvent, @event, true); + else if (Action == GameInputs.MoveRight) + InputHelper.ReplaceKeyboardInputForAction(GameInputs.UiRight, InputEvent, @event, true); + else if (Action == GameInputs.MoveDown) + InputHelper.ReplaceKeyboardInputForAction(GameInputs.UiDown, InputEvent, @event, true); + else if (Action == GameInputs.Attack) + InputHelper.ReplaceKeyboardInputForAction(GameInputs.UiAccept, InputEvent, @event, true); + else if (Action == GameInputs.Interact) + InputHelper.ReplaceKeyboardInputForAction(GameInputs.UiCancel, InputEvent, @event, true); + InputEvent = @event; + } + } + } +} + diff --git a/Zennysoft.Game.Ma/src/options/KeyboardRemapButton.cs.uid b/Zennysoft.Game.Ma/src/options/KeyboardRemapButton.cs.uid new file mode 100644 index 00000000..e6c4e9c6 --- /dev/null +++ b/Zennysoft.Game.Ma/src/options/KeyboardRemapButton.cs.uid @@ -0,0 +1 @@ +uid://b70br20xue678 diff --git a/Zennysoft.Game.Ma/src/options/OptionsData.cs b/Zennysoft.Game.Ma/src/options/OptionsData.cs new file mode 100644 index 00000000..41d5f2fe --- /dev/null +++ b/Zennysoft.Game.Ma/src/options/OptionsData.cs @@ -0,0 +1,21 @@ +using Chickensoft.Introspection; +using Chickensoft.Serialization; +using Godot; + +namespace Zennysoft.Game.Ma; + +[Meta, Id("options_data")] +public partial class OptionsData : Node +{ + [Save("MasterVolume")] + public required double MasterVolumeLevel { get; set; } + + [Save("MusicVolume")] + public required double MusicVolumeLevel { get; set; } + + [Save("SFXVolume")] + public required double SFXVolumeLevel { get; set; } + + [Save("ScreenResolution")] + public required int ScreenResolution { get; set; } +} \ No newline at end of file diff --git a/Zennysoft.Game.Ma/src/options/OptionsData.cs.uid b/Zennysoft.Game.Ma/src/options/OptionsData.cs.uid new file mode 100644 index 00000000..676d24d4 --- /dev/null +++ b/Zennysoft.Game.Ma/src/options/OptionsData.cs.uid @@ -0,0 +1 @@ +uid://jrl2hyudv6jn diff --git a/Zennysoft.Game.Ma/src/options/OptionsMenu.cs b/Zennysoft.Game.Ma/src/options/OptionsMenu.cs index fd8a675f..ea121b08 100644 --- a/Zennysoft.Game.Ma/src/options/OptionsMenu.cs +++ b/Zennysoft.Game.Ma/src/options/OptionsMenu.cs @@ -1,7 +1,8 @@ using Chickensoft.AutoInject; using Chickensoft.Introspection; -using Chickensoft.Serialization; using Godot; +using NathanHoad; +using System.Linq; namespace Zennysoft.Game.Ma; @@ -15,7 +16,12 @@ public partial class OptionsMenu : Control [Node] public HSlider MasterVolumeSlider { get; set; } [Node] public HSlider MusicVolumeSlider { get; set; } [Node] public HSlider SFXVolumeSlider { get; set; } - [Node] public Button SaveAndExitButton { get; set; } + + [Node] public InputMapper Controller { get; set; } + [Node] public TabContainer TabContainer { get; set; } + [Node] public Label PressToGoBackLabel { get; set; } + + [Node] public CanvasLayer CanvasLayer { get; set; } public OptionsData OptionsData; @@ -48,11 +54,65 @@ public partial class OptionsMenu : Control ResolutionOptions.ItemSelected += ResolutionOptions_ItemSelected; - SaveAndExitButton.ButtonUp += SaveAndExitButton_ButtonUp; + TabContainer.TabChanged += TabContainer_TabChanged; _masterBusIndex = AudioServer.GetBusIndex("Master"); _musicBusIndex = AudioServer.GetBusIndex("MUSIC"); _sfxBusIndex = AudioServer.GetBusIndex("SFX"); + + VisibilityChanged += OptionsMenu_VisibilityChanged; + + InputHelper.JoypadInputChanged += (string action, InputEvent input) => + { + if (GameInputs.Interact == action) + { + var interactInputs = InputHelper.GetLabelForInput(InputHelper.GetJoypadInputForAction(GameInputs.Interact)); + PressToGoBackLabel.Text = $"Press {interactInputs} to save and exit."; + } + }; + + InputHelper.KeyboardInputChanged += (string action, InputEvent input) => + { + if (GameInputs.Interact == action) + { + var interactInputs = InputHelper.GetLabelForInput(InputHelper.GetKeyboardInputForAction(GameInputs.Interact)); + PressToGoBackLabel.Text = $"Press {interactInputs} to save and exit."; + } + }; + } + + private void TabContainer_TabChanged(long tab) + { + if (tab == 0) + MasterVolumeSlider.GrabFocus(); + if (tab == 1) + Controller.MoveForwardKeyboard.GrabFocus(); + } + + private void OptionsMenu_VisibilityChanged() => CanvasLayer.Visible = !CanvasLayer.Visible; + + public override void _Input(InputEvent @event) + { + var interactInputs = InputHelper.GetKeyboardOrJoypadInputsForAction(GameInputs.Interact); + if (interactInputs.Any(x => x.IsMatch(@event))) + { + AcceptEvent(); + SaveAndExitMenu(); + } + + var leftTab = InputHelper.GetKeyboardOrJoypadInputsForAction(GameInputs.StrafeLeft); + if (leftTab.Any(x => x.IsMatch(@event))) + { + AcceptEvent(); + TabContainer.CurrentTab = Mathf.Max(0, TabContainer.CurrentTab - 1); + } + + var rightTab = InputHelper.GetKeyboardOrJoypadInputsForAction(GameInputs.StrafeRight); + if (rightTab.Any(x => x.IsMatch(@event))) + { + AcceptEvent(); + TabContainer.CurrentTab = Mathf.Min(TabContainer.GetTabCount() - 1, TabContainer.CurrentTab + 1); + } } private void ResolutionOptions_ItemSelected(long index) @@ -71,7 +131,7 @@ public partial class OptionsMenu : Control DisplayServer.WindowSetMode(_windowModes[optionsData.ScreenResolution]); } - private void SaveAndExitButton_ButtonUp() => EmitSignal(SignalName.OptionsMenuExited); + private void SaveAndExitMenu() => EmitSignal(SignalName.OptionsMenuExited); private void MasterVolumeSlider_Changed(double valueChanged) { @@ -90,20 +150,4 @@ public partial class OptionsMenu : Control OptionsData.SFXVolumeLevel = valueChanged; AudioServer.SetBusVolumeDb(_sfxBusIndex, Mathf.LinearToDb((float)valueChanged)); } -} - -[Meta, Id("options_data")] -public partial class OptionsData : Node -{ - [Save("MasterVolume")] - public required double MasterVolumeLevel { get; set; } - - [Save("MusicVolume")] - public required double MusicVolumeLevel { get; set; } - - [Save("SFXVolume")] - public required double SFXVolumeLevel { get; set; } - - [Save("ScreenResolution")] - public required int ScreenResolution { get; set; } } \ No newline at end of file diff --git a/Zennysoft.Game.Ma/src/options/OptionsMenu.tscn b/Zennysoft.Game.Ma/src/options/OptionsMenu.tscn index 124aa2bb..1403a031 100644 --- a/Zennysoft.Game.Ma/src/options/OptionsMenu.tscn +++ b/Zennysoft.Game.Ma/src/options/OptionsMenu.tscn @@ -1,6 +1,8 @@ -[gd_scene load_steps=5 format=3 uid="uid://drkl3btdy6uxj"] +[gd_scene load_steps=7 format=3 uid="uid://drkl3btdy6uxj"] [ext_resource type="Script" uid="uid://cjxmdvhixcj6e" path="res://src/options/OptionsMenu.cs" id="1_jli36"] +[ext_resource type="Shortcut" uid="uid://dumkrjur22k2a" path="res://src/ui/ButtonShortcut.tres" id="2_1egkf"] +[ext_resource type="PackedScene" uid="uid://dk5esf6mm6kte" path="res://src/options/InputMapper.tscn" id="2_utd4g"] [sub_resource type="StyleBoxLine" id="StyleBoxLine_jli36"] @@ -18,43 +20,50 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_jli36") -[node name="ColorRect" type="ColorRect" parent="."] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -color = Color(0.137255, 0.121569, 0.12549, 1) +[node name="CanvasLayer" type="CanvasLayer" parent="."] +unique_name_in_owner = true +visible = false -[node name="MarginContainer" type="MarginContainer" parent="."] -layout_mode = 1 +[node name="CenterContainer" type="AspectRatioContainer" parent="CanvasLayer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 mouse_filter = 0 + +[node name="VBoxContainer" type="VBoxContainer" parent="CanvasLayer/CenterContainer"] +layout_mode = 2 + +[node name="TabContainer" type="TabContainer" parent="CanvasLayer/CenterContainer/VBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(1280, 960) +layout_mode = 2 +current_tab = 0 + +[node name="Audio" type="MarginContainer" parent="CanvasLayer/CenterContainer/VBoxContainer/TabContainer"] +layout_mode = 2 theme_override_constants/margin_left = 100 theme_override_constants/margin_top = 100 theme_override_constants/margin_right = 100 theme_override_constants/margin_bottom = 100 +metadata/_tab_index = 0 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="CanvasLayer/CenterContainer/VBoxContainer/TabContainer/Audio"] layout_mode = 2 size_flags_horizontal = 0 mouse_filter = 0 -[node name="MasterVolume" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +[node name="MasterVolume" type="HBoxContainer" parent="CanvasLayer/CenterContainer/VBoxContainer/TabContainer/Audio/VBoxContainer"] layout_mode = 2 -[node name="VolumeLabel" type="Label" parent="MarginContainer/VBoxContainer/MasterVolume"] +[node name="VolumeLabel" type="Label" parent="CanvasLayer/CenterContainer/VBoxContainer/TabContainer/Audio/VBoxContainer/MasterVolume"] custom_minimum_size = Vector2(125, 0) layout_mode = 2 text = "Master Volume" horizontal_alignment = 2 -[node name="MasterVolumeSlider" type="HSlider" parent="MarginContainer/VBoxContainer/MasterVolume"] +[node name="MasterVolumeSlider" type="HSlider" parent="CanvasLayer/CenterContainer/VBoxContainer/TabContainer/Audio/VBoxContainer/MasterVolume"] unique_name_in_owner = true custom_minimum_size = Vector2(300, 0) layout_mode = 2 @@ -66,16 +75,16 @@ max_value = 1.0 step = 0.001 value = 1.0 -[node name="MusicVolume" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +[node name="MusicVolume" type="HBoxContainer" parent="CanvasLayer/CenterContainer/VBoxContainer/TabContainer/Audio/VBoxContainer"] layout_mode = 2 -[node name="VolumeLabel" type="Label" parent="MarginContainer/VBoxContainer/MusicVolume"] +[node name="VolumeLabel" type="Label" parent="CanvasLayer/CenterContainer/VBoxContainer/TabContainer/Audio/VBoxContainer/MusicVolume"] custom_minimum_size = Vector2(125, 0) layout_mode = 2 text = "Music Volume" horizontal_alignment = 2 -[node name="MusicVolumeSlider" type="HSlider" parent="MarginContainer/VBoxContainer/MusicVolume"] +[node name="MusicVolumeSlider" type="HSlider" parent="CanvasLayer/CenterContainer/VBoxContainer/TabContainer/Audio/VBoxContainer/MusicVolume"] unique_name_in_owner = true custom_minimum_size = Vector2(300, 0) layout_mode = 2 @@ -87,16 +96,16 @@ max_value = 1.0 step = 0.001 value = 1.0 -[node name="SFXVolume" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +[node name="SFXVolume" type="HBoxContainer" parent="CanvasLayer/CenterContainer/VBoxContainer/TabContainer/Audio/VBoxContainer"] layout_mode = 2 -[node name="VolumeLabel" type="Label" parent="MarginContainer/VBoxContainer/SFXVolume"] +[node name="VolumeLabel" type="Label" parent="CanvasLayer/CenterContainer/VBoxContainer/TabContainer/Audio/VBoxContainer/SFXVolume"] custom_minimum_size = Vector2(125, 0) layout_mode = 2 text = "SFX Volume" horizontal_alignment = 2 -[node name="SFXVolumeSlider" type="HSlider" parent="MarginContainer/VBoxContainer/SFXVolume"] +[node name="SFXVolumeSlider" type="HSlider" parent="CanvasLayer/CenterContainer/VBoxContainer/TabContainer/Audio/VBoxContainer/SFXVolume"] unique_name_in_owner = true custom_minimum_size = Vector2(300, 0) layout_mode = 2 @@ -108,27 +117,33 @@ max_value = 1.0 step = 0.001 value = 1.0 -[node name="Resolution" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +[node name="Resolution" type="HBoxContainer" parent="CanvasLayer/CenterContainer/VBoxContainer/TabContainer/Audio/VBoxContainer"] layout_mode = 2 -[node name="ResolutionLabel" type="Label" parent="MarginContainer/VBoxContainer/Resolution"] +[node name="ResolutionLabel" type="Label" parent="CanvasLayer/CenterContainer/VBoxContainer/TabContainer/Audio/VBoxContainer/Resolution"] custom_minimum_size = Vector2(125, 0) layout_mode = 2 text = "Resolution: " horizontal_alignment = 2 -[node name="ResolutionOptions" type="OptionButton" parent="MarginContainer/VBoxContainer/Resolution"] +[node name="ResolutionOptions" type="OptionButton" parent="CanvasLayer/CenterContainer/VBoxContainer/TabContainer/Audio/VBoxContainer/Resolution"] unique_name_in_owner = true custom_minimum_size = Vector2(200, 0) layout_mode = 2 +shortcut = ExtResource("2_1egkf") flat = true -[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="CanvasLayer/CenterContainer/VBoxContainer/TabContainer/Audio/VBoxContainer"] layout_mode = 2 size_flags_vertical = 10 -[node name="SaveAndExitButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"] +[node name="Controller" parent="CanvasLayer/CenterContainer/VBoxContainer/TabContainer" instance=ExtResource("2_utd4g")] unique_name_in_owner = true -custom_minimum_size = Vector2(150, 75) +visible = false layout_mode = 2 -text = "Save and Exit" +metadata/_tab_index = 1 + +[node name="PressToGoBackLabel" type="Label" parent="CanvasLayer/CenterContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Press " diff --git a/Zennysoft.Game.Ma/src/ui/ButtonShortcut.tres b/Zennysoft.Game.Ma/src/ui/ButtonShortcut.tres new file mode 100644 index 00000000..99bc5811 --- /dev/null +++ b/Zennysoft.Game.Ma/src/ui/ButtonShortcut.tres @@ -0,0 +1,3 @@ +[gd_resource type="Shortcut" format=3 uid="uid://dumkrjur22k2a"] + +[resource] diff --git a/Zennysoft.Game.Ma/src/ui/dialogue/DialogueBalloon.cs b/Zennysoft.Game.Ma/src/ui/dialogue/DialogueBalloon.cs index 4935adb6..c69c06ce 100644 --- a/Zennysoft.Game.Ma/src/ui/dialogue/DialogueBalloon.cs +++ b/Zennysoft.Game.Ma/src/ui/dialogue/DialogueBalloon.cs @@ -5,8 +5,8 @@ using Godot.Collections; namespace Zennysoft.Game.Ma; public partial class DialogueBalloon : CanvasLayer { - [Export] public string NextAction = "ui_accept"; - [Export] public string SkipAction = "ui_cancel"; + [Export] public string NextAction = GameInputs.Interact; + [Export] public string SkipAction = GameInputs.Attack; Control balloon; diff --git a/Zennysoft.Game.Ma/src/ui/in_game_ui/InGameUI.cs b/Zennysoft.Game.Ma/src/ui/in_game_ui/InGameUI.cs index 1a3c629b..2289e7cf 100644 --- a/Zennysoft.Game.Ma/src/ui/in_game_ui/InGameUI.cs +++ b/Zennysoft.Game.Ma/src/ui/in_game_ui/InGameUI.cs @@ -60,7 +60,7 @@ public partial class InGameUI : Control, IInGameUI public override void _UnhandledInput(InputEvent @event) { - if (@event.IsActionPressed(GameInputs.Inventory)) + if (@event.IsActionPressed(GameInputs.Inventory) && !InventoryMenu.Visible) { GD.Print("Inventory button pressed"); InGameUILogic.Input(new InGameUILogic.Input.ShowInventory()); diff --git a/Zennysoft.Game.Ma/src/ui/inventory_menu/InventoryMenu.cs b/Zennysoft.Game.Ma/src/ui/inventory_menu/InventoryMenu.cs index acb5f272..c3d3c113 100644 --- a/Zennysoft.Game.Ma/src/ui/inventory_menu/InventoryMenu.cs +++ b/Zennysoft.Game.Ma/src/ui/inventory_menu/InventoryMenu.cs @@ -124,7 +124,7 @@ public partial class InventoryMenu : Control, IInventoryMenu #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed public override void _Input(InputEvent @event) { - if (Visible && @event.IsActionPressed(GameInputs.UiCancel)) + if (Visible && @event.IsActionPressed(GameInputs.Inventory)) { if (UseButton.HasFocus() || DropButton.HasFocus() || ThrowButton.HasFocus()) { @@ -134,6 +134,7 @@ public partial class InventoryMenu : Control, IInventoryMenu } else { + AcceptEvent(); Autoload.AudioManager.Play(SoundEffect.Cancel); _gameRepo.CloseInventory(); } @@ -142,17 +143,17 @@ public partial class InventoryMenu : Control, IInventoryMenu if (ItemSlots.Length == 0 || UseButton.HasFocus() || DropButton.HasFocus() || ThrowButton.HasFocus()) return; - if (@event.IsActionPressed(GameInputs.UiRight) && _currentPageNumber == InventoryPageNumber.FirstPage) + if (@event.IsActionPressed(GameInputs.MoveRight) && _currentPageNumber == InventoryPageNumber.FirstPage) { var inventory = Player.Inventory; if (inventory.Items.Count > _itemsPerPage) ChangeInventoryPage(InventoryPageNumber.SecondPage); } - if (@event.IsActionPressed(GameInputs.UiLeft) && _currentPageNumber == InventoryPageNumber.SecondPage) + if (@event.IsActionPressed(GameInputs.MoveLeft) && _currentPageNumber == InventoryPageNumber.SecondPage) ChangeInventoryPage(InventoryPageNumber.FirstPage); - if (@event.IsActionPressed(GameInputs.UiDown)) + if (@event.IsActionPressed(GameInputs.MoveDown)) { var oldIndex = _currentIndex; var newIndex = new[] { _currentIndex + 1, _itemsPerPage - 1, ItemSlots.Length - 1 }.Min(); @@ -165,7 +166,7 @@ public partial class InventoryMenu : Control, IInventoryMenu _currentIndex = newIndex; } - if (@event.IsActionPressed(GameInputs.UiUp)) + if (@event.IsActionPressed(GameInputs.MoveUp)) { var oldIndex = _currentIndex; var newIndex = new[] { _currentIndex - 1, 0 }.Max(); @@ -179,7 +180,7 @@ public partial class InventoryMenu : Control, IInventoryMenu _currentIndex = newIndex; } - if (@event.IsActionPressed(GameInputs.UiAccept)) + if (@event.IsActionPressed(GameInputs.Attack)) { DisplayUserActionPrompt(); Autoload.AudioManager.Play(SoundEffect.Select); diff --git a/Zennysoft.Game.Ma/src/ui/inventory_menu/InventoryMenu.tscn b/Zennysoft.Game.Ma/src/ui/inventory_menu/InventoryMenu.tscn index 895c3c17..2bbffb82 100644 --- a/Zennysoft.Game.Ma/src/ui/inventory_menu/InventoryMenu.tscn +++ b/Zennysoft.Game.Ma/src/ui/inventory_menu/InventoryMenu.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=29 format=3 uid="uid://dlj8qdg1c5048"] +[gd_scene load_steps=30 format=3 uid="uid://dlj8qdg1c5048"] [ext_resource type="Script" uid="uid://cmtet15hi5oiy" path="res://src/ui/inventory_menu/InventoryMenu.cs" id="1_l64wl"] [ext_resource type="Shader" uid="uid://cnphwvmr05hp1" path="res://src/ui/inventory_menu/InventoryMenu.gdshader" id="2_0fvsh"] @@ -8,6 +8,7 @@ [ext_resource type="LabelSettings" uid="uid://ca1q6yu8blwxf" path="res://src/ui/label_settings/InventoryMainTextBold.tres" id="6_tmdno"] [ext_resource type="LabelSettings" uid="uid://cuuo43x72xcsc" path="res://src/ui/label_settings/MainTextBold.tres" id="7_vyrxm"] [ext_resource type="Theme" uid="uid://daxuhpmyxwxck" path="res://src/ui/inventory_menu/InventoryDialogueSelectionStyle.tres" id="8_khyvo"] +[ext_resource type="Shortcut" uid="uid://dumkrjur22k2a" path="res://src/ui/ButtonShortcut.tres" id="9_b6rkr"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_i55tv"] shader = ExtResource("2_0fvsh") @@ -381,6 +382,7 @@ theme_override_styles/disabled = SubResource("StyleBoxEmpty_fu7o2") theme_override_styles/pressed = SubResource("StyleBoxEmpty_nkvce") theme_override_styles/normal = SubResource("StyleBoxEmpty_545ij") button_mask = 0 +shortcut = ExtResource("9_b6rkr") text = "Use" alignment = 0 @@ -405,6 +407,7 @@ theme_override_styles/disabled = SubResource("StyleBoxEmpty_ascpt") theme_override_styles/pressed = SubResource("StyleBoxEmpty_abpb1") theme_override_styles/normal = SubResource("StyleBoxEmpty_545ij") button_mask = 0 +shortcut = ExtResource("9_b6rkr") text = "Throw" alignment = 0 @@ -429,6 +432,7 @@ theme_override_styles/disabled = SubResource("StyleBoxEmpty_uerb4") theme_override_styles/pressed = SubResource("StyleBoxEmpty_lvcf8") theme_override_styles/normal = SubResource("StyleBoxEmpty_ct6ql") button_mask = 0 +shortcut = ExtResource("9_b6rkr") text = "Drop" alignment = 0