diff --git a/run_gut_unit_tests.sh b/run_gut_unit_tests.sh new file mode 100644 index 000000000..20705ee52 --- /dev/null +++ b/run_gut_unit_tests.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# This script runs GUT unit tests locally on Windows 10 using Godot 4.4/4.5. +# Assumptions: +# - Godot executable is in your PATH or specify the full path below. +# - Project is in the current directory. +# - Tests are located in res://test/ and use GUT syntax. +# - Run this script from the project root using Git Bash, WSL, or similar. +# - A .gutconfig.json file should exist in the project root (create one if missing; see Step 2). + +# Path to Godot executable (update if not in PATH; on Windows, it's godot.exe) +GODOT="godot" # Or full path, e.g., "C:/Path/To/Godot/godot.exe" + +# Project directory (current working directory) +PROJECT_DIR="$(pwd)" + +# Install GUT if not already in addons/ +echo "Ensuring GUT is installed in addons/..." +if [ ! -d "$PROJECT_DIR/addons/gut" ]; then + mkdir -p "$PROJECT_DIR/addons" + wget https://github.com/bitwes/Gut/archive/refs/tags/v9.5.0.zip + unzip v9.5.0.zip -d "$PROJECT_DIR/addons" + mv "$PROJECT_DIR/addons/Gut-9.5.0/addons/gut" "$PROJECT_DIR/addons/gut" + rm -rf "$PROJECT_DIR/addons/Gut-9.5.0" v9.5.0.zip +fi + +# Optional: Import resources (uncomment if needed; runs Godot headless to import) +echo "Importing Resources..." +"$GODOT" --headless --path "$PROJECT_DIR" --import --quit +if [ $? -ne 0 ]; then echo "Resource import failed."; exit 1; fi + +# Run GUT tests +echo "Running GUT Unit Tests..." +"$GODOT" --headless --verbose --path "$PROJECT_DIR" -s addons/gut/gut_cmdln.gd -gconfig=res://.gutconfig.json -gdir=res://test -ginclude_subdirs=true -gexit +if [ $? -ne 0 ]; then echo "Unit tests failed."; exit 1; fi + +# Optional: Handle reports (GUT can generate JUnit XML or other reports based on config) +echo "Tests completed! Check reports in gut-reports/ or configured directory if generated." diff --git a/scenes/key_mapping_menu.tscn b/scenes/key_mapping_menu.tscn index 3ab1b19b5..cc15b10d6 100644 --- a/scenes/key_mapping_menu.tscn +++ b/scenes/key_mapping_menu.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=13 format=3 uid="uid://dxep4xm1sl8im"] +[gd_scene load_steps=15 format=3 uid="uid://dxep4xm1sl8im"] [ext_resource type="Script" uid="uid://cxlfe85bvd0mx" path="res://scripts/key_mapping.gd" id="1_5b0ek"] [ext_resource type="FontFile" uid="uid://borwvgqdgawbj" path="res://files/fonts/EMPIREST.TTF" id="1_rbr5m"] @@ -11,6 +11,10 @@ corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_5b0ek"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_312xi"] + [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tmees"] bg_color = Color(0.2627451, 0.2627451, 0.2627451, 0.5882353) corner_radius_top_left = 40 @@ -67,161 +71,331 @@ anchor_left = 0.5 anchor_top = 0.5 anchor_right = 0.5 anchor_bottom = 0.5 -offset_left = -240.0 -offset_top = -290.0 -offset_right = 240.0 -offset_bottom = 290.0 +offset_left = -390.0 +offset_top = -340.0 +offset_right = 390.0 +offset_bottom = 340.0 grow_horizontal = 2 grow_vertical = 2 size_flags_horizontal = 4 size_flags_vertical = 4 theme_override_styles/panel = SubResource("StyleBoxFlat_312xi") -[node name="OptionsVBoxContainer" type="VBoxContainer" parent="Panel"] +[node name="Options" type="VBoxContainer" parent="Panel"] layout_mode = 1 anchors_preset = 8 anchor_left = 0.5 anchor_top = 0.5 anchor_right = 0.5 anchor_bottom = 0.5 -offset_left = -205.5 -offset_top = -265.0 -offset_right = 205.5 -offset_bottom = 265.0 +offset_left = -362.5 +offset_top = -310.0 +offset_right = 362.5 +offset_bottom = 310.0 grow_horizontal = 2 grow_vertical = 2 -theme_override_constants/separation = 20 +theme_override_constants/separation = 40 -[node name="MainLabel" type="Label" parent="Panel/OptionsVBoxContainer"] +[node name="MainLabel" type="Label" parent="Panel/Options"] layout_mode = 2 size_flags_horizontal = 4 -size_flags_vertical = 6 theme_override_colors/font_color = Color(0.9607843, 0.9607843, 0.050980393, 1) theme_override_fonts/font = ExtResource("1_rbr5m") theme_override_font_sizes/font_size = 55 -text = "{OPTIONS}" +text = "{KEY=MAPPING=OPTIONS}" -[node name="Label" type="Label" parent="Panel/OptionsVBoxContainer"] +[node name="DeviceTypeContainer" type="HBoxContainer" parent="Panel/Options"] layout_mode = 2 -size_flags_horizontal = 4 -size_flags_vertical = 6 +alignment = 1 + +[node name="DeviceTypeLabel" type="Label" parent="Panel/Options/DeviceTypeContainer"] +layout_mode = 2 +size_flags_horizontal = 2 theme_override_colors/font_color = Color(0.9607843, 0.9607843, 0.050980393, 1) theme_override_fonts/font = ExtResource("1_rbr5m") -theme_override_font_sizes/font_size = 28 -text = "{KEY=MAPPING}" -horizontal_alignment = 1 +theme_override_font_sizes/font_size = 30 +text = "{DEVICE=TYPE}" vertical_alignment = 1 -[node name="KeyMapVBoxContainer" type="VBoxContainer" parent="Panel/OptionsVBoxContainer"] +[node name="Keyboard" type="CheckButton" parent="Panel/Options/DeviceTypeContainer"] +layout_mode = 2 +size_flags_horizontal = 6 +tooltip_text = "Keyboard" +theme_override_colors/font_color = Color(0.60784316, 0.60784316, 0.60784316, 1) +theme_override_colors/font_focus_color = Color(0.60784316, 0.60784316, 0.60784316, 0.92156863) +theme_override_colors/font_pressed_color = Color(0, 0.8784314, 0.007843138, 1) +theme_override_colors/font_hover_color = Color(0.60784316, 0.60784316, 0.60784316, 0.92156863) +theme_override_colors/font_hover_pressed_color = Color(0, 0.8784314, 0.007843138, 0.9019608) +theme_override_colors/font_disabled_color = Color(0.60784316, 0.60784316, 0.60784316, 1) +theme_override_fonts/font = ExtResource("1_rbr5m") +theme_override_font_sizes/font_size = 20 +theme_override_styles/focus = SubResource("StyleBoxEmpty_5b0ek") +button_pressed = true +text = "{KEYBOARD}" + +[node name="Gamepad" type="CheckButton" parent="Panel/Options/DeviceTypeContainer"] +layout_mode = 2 +size_flags_horizontal = 8 +tooltip_text = "Gamepad" +theme_override_colors/font_color = Color(0.60784316, 0.60784316, 0.60784316, 1) +theme_override_colors/font_focus_color = Color(0.60784316, 0.60784316, 0.60784316, 0.92156863) +theme_override_colors/font_pressed_color = Color(0, 0.8784314, 0.007843138, 1) +theme_override_colors/font_hover_color = Color(0.60784316, 0.60784316, 0.60784316, 0.92156863) +theme_override_colors/font_hover_pressed_color = Color(0, 0.8784314, 0.007843138, 0.9019608) +theme_override_colors/font_disabled_color = Color(0.60784316, 0.60784316, 0.60784316, 1) +theme_override_fonts/font = ExtResource("1_rbr5m") +theme_override_font_sizes/font_size = 20 +theme_override_styles/focus = SubResource("StyleBoxEmpty_312xi") +text = "{GAMEPAD}" + +[node name="KeyMapContainer" type="HBoxContainer" parent="Panel/Options"] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/separation = 90 + +[node name="PlayerKeyMap" type="VBoxContainer" parent="Panel/Options/KeyMapContainer"] layout_mode = 2 theme_override_constants/separation = 20 -[node name="KeyMappingSpeedUp" type="HBoxContainer" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer"] +[node name="Label" type="Label" parent="Panel/Options/KeyMapContainer/PlayerKeyMap"] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 6 +theme_override_colors/font_color = Color(0.9607843, 0.9607843, 0.050980393, 1) +theme_override_fonts/font = ExtResource("1_rbr5m") +theme_override_font_sizes/font_size = 30 +text = "{PLAYER=CONTROLS}" +vertical_alignment = 1 + +[node name="KeyMappingSpeedUp" type="HBoxContainer" parent="Panel/Options/KeyMapContainer/PlayerKeyMap"] layout_mode = 2 -tooltip_text = "Key mapping for forward button" +tooltip_text = "Key mapping for \"speed up\" button" +theme_override_constants/separation = 8 -[node name="Label" type="Label" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer/KeyMappingSpeedUp"] +[node name="Label" type="Label" parent="Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingSpeedUp"] layout_mode = 2 theme_override_fonts/font = ExtResource("1_rbr5m") theme_override_font_sizes/font_size = 18 text = "{SPEED=UP:}" -[node name="SpeedUpInputRemapButton" type="Button" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer/KeyMappingSpeedUp"] +[node name="SpeedUpInputRemap" type="Button" parent="Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingSpeedUp"] layout_direction = 3 layout_mode = 2 theme_override_font_sizes/font_size = 18 script = ExtResource("2_5b0ek") action = "speed_up" -[node name="KeyMappingSpeedDown" type="HBoxContainer" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer"] +[node name="KeyMappingSpeedDown" type="HBoxContainer" parent="Panel/Options/KeyMapContainer/PlayerKeyMap"] layout_mode = 2 -tooltip_text = "Key mapping for backward button" +tooltip_text = "Key mapping for \"speed down\" button" +theme_override_constants/separation = 8 -[node name="Label" type="Label" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer/KeyMappingSpeedDown"] +[node name="Label" type="Label" parent="Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingSpeedDown"] layout_mode = 2 theme_override_fonts/font = ExtResource("1_rbr5m") theme_override_font_sizes/font_size = 18 text = "{SPEED=DOWN:}" -[node name="SpeedDownInputRemapButton" type="Button" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer/KeyMappingSpeedDown"] +[node name="SpeedDownInputRemap" type="Button" parent="Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingSpeedDown"] layout_mode = 2 theme_override_font_sizes/font_size = 18 script = ExtResource("2_5b0ek") action = "speed_down" -[node name="KeyMappingLeft" type="HBoxContainer" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer"] +[node name="KeyMappingLeft" type="HBoxContainer" parent="Panel/Options/KeyMapContainer/PlayerKeyMap"] layout_mode = 2 tooltip_text = "Key mapping for move left button" +theme_override_constants/separation = 8 -[node name="Label" type="Label" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer/KeyMappingLeft"] +[node name="Label" type="Label" parent="Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingLeft"] layout_mode = 2 theme_override_fonts/font = ExtResource("1_rbr5m") theme_override_font_sizes/font_size = 18 text = "{MOVE=LEFT:}" -[node name="LeftInputRemapButton" type="Button" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer/KeyMappingLeft"] +[node name="LeftInputRemap" type="Button" parent="Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingLeft"] layout_mode = 2 theme_override_font_sizes/font_size = 18 script = ExtResource("2_5b0ek") action = "move_left" -[node name="KeyMappingRight" type="HBoxContainer" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer"] +[node name="KeyMappingRight" type="HBoxContainer" parent="Panel/Options/KeyMapContainer/PlayerKeyMap"] layout_mode = 2 tooltip_text = "Key mapping for move right button" +theme_override_constants/separation = 8 -[node name="Label" type="Label" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer/KeyMappingRight"] +[node name="Label" type="Label" parent="Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingRight"] layout_mode = 2 theme_override_fonts/font = ExtResource("1_rbr5m") theme_override_font_sizes/font_size = 18 text = "{MOVE=RIGHT:}" -[node name="RightInputRemapButton" type="Button" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer/KeyMappingRight"] +[node name="RightInputRemap" type="Button" parent="Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingRight"] layout_mode = 2 theme_override_font_sizes/font_size = 18 script = ExtResource("2_5b0ek") action = "move_right" -[node name="KeyMappingFire" type="HBoxContainer" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer"] +[node name="KeyMappingFire" type="HBoxContainer" parent="Panel/Options/KeyMapContainer/PlayerKeyMap"] layout_mode = 2 tooltip_text = "Key mapping for fire button" +theme_override_constants/separation = 8 -[node name="Label" type="Label" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer/KeyMappingFire"] +[node name="Label" type="Label" parent="Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingFire"] layout_mode = 2 theme_override_fonts/font = ExtResource("1_rbr5m") theme_override_font_sizes/font_size = 18 text = "{FIRE:}" -[node name="FireInputRemapButton" type="Button" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer/KeyMappingFire"] +[node name="FireInputRemap" type="Button" parent="Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingFire"] layout_mode = 2 theme_override_font_sizes/font_size = 18 script = ExtResource("2_5b0ek") action = "fire" -[node name="KeyMappingNextWeapon" type="HBoxContainer" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer"] +[node name="KeyMappingNextWeapon" type="HBoxContainer" parent="Panel/Options/KeyMapContainer/PlayerKeyMap"] layout_mode = 2 tooltip_text = "Key mapping for next weapon button" +theme_override_constants/separation = 8 -[node name="Label" type="Label" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer/KeyMappingNextWeapon"] +[node name="Label" type="Label" parent="Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingNextWeapon"] layout_mode = 2 theme_override_fonts/font = ExtResource("1_rbr5m") theme_override_font_sizes/font_size = 18 text = "{NEXT=WEAPON:}" -[node name="NextWeaponInputRemapButton" type="Button" parent="Panel/OptionsVBoxContainer/KeyMapVBoxContainer/KeyMappingNextWeapon"] +[node name="NextWeaponInputRemap" type="Button" parent="Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingNextWeapon"] layout_mode = 2 theme_override_font_sizes/font_size = 18 script = ExtResource("2_5b0ek") action = "next_weapon" -[node name="BtnContainer" type="HBoxContainer" parent="Panel/OptionsVBoxContainer"] +[node name="MenuKeyMap" type="VBoxContainer" parent="Panel/Options/KeyMapContainer"] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="Label" type="Label" parent="Panel/Options/KeyMapContainer/MenuKeyMap"] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 6 +theme_override_colors/font_color = Color(0.9607843, 0.9607843, 0.050980393, 1) +theme_override_fonts/font = ExtResource("1_rbr5m") +theme_override_font_sizes/font_size = 30 +text = "{MENU=CONTROLS}" +vertical_alignment = 1 + +[node name="KeyMappingPause" type="HBoxContainer" parent="Panel/Options/KeyMapContainer/MenuKeyMap"] +layout_mode = 2 +tooltip_text = "Key mapping for pause button" +theme_override_constants/separation = 8 + +[node name="Label" type="Label" parent="Panel/Options/KeyMapContainer/MenuKeyMap/KeyMappingPause"] +layout_mode = 2 +theme_override_fonts/font = ExtResource("1_rbr5m") +theme_override_font_sizes/font_size = 18 +text = "{PAUSE:}" + +[node name="PauseInputRemap" type="Button" parent="Panel/Options/KeyMapContainer/MenuKeyMap/KeyMappingPause"] +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +script = ExtResource("2_5b0ek") +action = "pause" + +[node name="KeyMappingAccept" type="HBoxContainer" parent="Panel/Options/KeyMapContainer/MenuKeyMap"] +layout_mode = 2 +tooltip_text = "Key mapping for accept button" +theme_override_constants/separation = 8 + +[node name="Label" type="Label" parent="Panel/Options/KeyMapContainer/MenuKeyMap/KeyMappingAccept"] +layout_mode = 2 +theme_override_fonts/font = ExtResource("1_rbr5m") +theme_override_font_sizes/font_size = 18 +text = "{ACCEPT:}" + +[node name="AcceptInputRemap" type="Button" parent="Panel/Options/KeyMapContainer/MenuKeyMap/KeyMappingAccept"] +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +script = ExtResource("2_5b0ek") +action = "ui_accept" + +[node name="KeyMappingMenuUp" type="HBoxContainer" parent="Panel/Options/KeyMapContainer/MenuKeyMap"] +layout_mode = 2 +tooltip_text = "Key mapping for menu navigation \"move up\" button" +theme_override_constants/separation = 8 + +[node name="Label" type="Label" parent="Panel/Options/KeyMapContainer/MenuKeyMap/KeyMappingMenuUp"] +layout_mode = 2 +theme_override_fonts/font = ExtResource("1_rbr5m") +theme_override_font_sizes/font_size = 18 +text = "{MENU=UP:}" + +[node name="MenuUpInputRemap" type="Button" parent="Panel/Options/KeyMapContainer/MenuKeyMap/KeyMappingMenuUp"] +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +script = ExtResource("2_5b0ek") +action = "ui_up" + +[node name="KeyMappingMenuDown" type="HBoxContainer" parent="Panel/Options/KeyMapContainer/MenuKeyMap"] +layout_mode = 2 +tooltip_text = "Key mapping for menu navigation \"move down\" button" +theme_override_constants/separation = 8 + +[node name="Label" type="Label" parent="Panel/Options/KeyMapContainer/MenuKeyMap/KeyMappingMenuDown"] +layout_mode = 2 +theme_override_fonts/font = ExtResource("1_rbr5m") +theme_override_font_sizes/font_size = 18 +text = "{MENU=DOWN:}" + +[node name="MenuDownInputRemap" type="Button" parent="Panel/Options/KeyMapContainer/MenuKeyMap/KeyMappingMenuDown"] +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +script = ExtResource("2_5b0ek") +action = "ui_down" + +[node name="KeyMappingMenuLeft" type="HBoxContainer" parent="Panel/Options/KeyMapContainer/MenuKeyMap"] +layout_mode = 2 +tooltip_text = "Key mapping for menu navigation \"move left\" button" +theme_override_constants/separation = 8 + +[node name="Label" type="Label" parent="Panel/Options/KeyMapContainer/MenuKeyMap/KeyMappingMenuLeft"] +layout_mode = 2 +theme_override_fonts/font = ExtResource("1_rbr5m") +theme_override_font_sizes/font_size = 18 +text = "{MENU=LEFT:}" + +[node name="MenuLeftInputRemap" type="Button" parent="Panel/Options/KeyMapContainer/MenuKeyMap/KeyMappingMenuLeft"] +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +script = ExtResource("2_5b0ek") +action = "ui_left" + +[node name="KeyMappingMenuRight" type="HBoxContainer" parent="Panel/Options/KeyMapContainer/MenuKeyMap"] +layout_mode = 2 +tooltip_text = "Key mapping for menu navigation \"move right\" button" +theme_override_constants/separation = 8 + +[node name="Label" type="Label" parent="Panel/Options/KeyMapContainer/MenuKeyMap/KeyMappingMenuRight"] +layout_mode = 2 +theme_override_fonts/font = ExtResource("1_rbr5m") +theme_override_font_sizes/font_size = 18 +text = "{MENU=RIGHT:}" + +[node name="MenuRightInputRemap" type="Button" parent="Panel/Options/KeyMapContainer/MenuKeyMap/KeyMappingMenuRight"] +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +script = ExtResource("2_5b0ek") +action = "ui_right" + +[node name="BtnContainer" type="HBoxContainer" parent="Panel/Options"] layout_mode = 2 size_flags_vertical = 3 theme_override_constants/separation = 20 -[node name="ControlsBackButton" type="Button" parent="Panel/OptionsVBoxContainer/BtnContainer"] +[node name="ControlsBackButton" type="Button" parent="Panel/Options/BtnContainer"] layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 +tooltip_text = "Back button" theme_override_colors/font_color = Color(0.99215686, 0.21568628, 0.2784314, 1) theme_override_colors/font_focus_color = Color(0, 0, 0, 1) theme_override_colors/font_hover_color = Color(0.9607843, 0.9607843, 0.050980393, 1) @@ -233,10 +407,11 @@ theme_override_styles/hover = SubResource("StyleBoxFlat_kgm0p") theme_override_styles/focus = SubResource("StyleBoxEmpty_7mufp") text = "{BACK}" -[node name="ControlResetButton" type="Button" parent="Panel/OptionsVBoxContainer/BtnContainer"] +[node name="ControlResetButton" type="Button" parent="Panel/Options/BtnContainer"] layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 +tooltip_text = "Reset button" theme_override_colors/font_color = Color(0.99215686, 0.21568628, 0.2784314, 1) theme_override_colors/font_focus_color = Color(1, 0.19607843, 0.2784314, 0.98039216) theme_override_colors/font_pressed_color = Color(0.9607843, 0.9607843, 0.050980393, 0.9019608) @@ -248,3 +423,6 @@ theme_override_styles/pressed = SubResource("StyleBoxFlat_7mufp") theme_override_styles/hover = SubResource("StyleBoxFlat_0yqio") theme_override_styles/focus = SubResource("StyleBoxEmpty_rhvir") text = "{RESET}" + +[connection signal="toggled" from="Panel/Options/DeviceTypeContainer/Keyboard" to="." method="_on_keyboard_toggled"] +[connection signal="toggled" from="Panel/Options/DeviceTypeContainer/Gamepad" to="." method="_on_gamepad_toggled"] diff --git a/scripts/input_remap_button.gd b/scripts/input_remap_button.gd index 3a4f7a923..7889fb502 100644 --- a/scripts/input_remap_button.gd +++ b/scripts/input_remap_button.gd @@ -15,6 +15,12 @@ class_name InputRemapButton extends Button +# Enum for clean Inspector dropdown + match +enum DeviceType { + KEYBOARD, + GAMEPAD, +} + const KEY_LABELS: Dictionary = { Key.KEY_W: "W", Key.KEY_S: "S", @@ -27,7 +33,8 @@ const KEY_LABELS: Dictionary = { Key.KEY_RIGHT: "Right", Key.KEY_UP: "Up", Key.KEY_DOWN: "Down", - Key.KEY_ESCAPE: "Esc" + Key.KEY_ESCAPE: "Esc", + Key.KEY_ENTER: "Enter", # Add more as needed for arrow keys, etc. } @@ -64,8 +71,7 @@ const JOY_AXIS_BASE_LABELS: Dictionary = { JOY_AXIS_RIGHT_X: "Right Stick X", JOY_AXIS_RIGHT_Y: "Right Stick Y", JOY_AXIS_TRIGGER_LEFT: "Left Trigger", - JOY_AXIS_TRIGGER_RIGHT: "Right Trigger" - # Add more axes if your game uses them + JOY_AXIS_TRIGGER_RIGHT: "Right Trigger", } # Custom labels for common joypad axes/directions (for nice display) @@ -84,8 +90,8 @@ const AXIS_DEADZONE_THRESHOLD: float = 0.5 # Value to normalize axis direction to (e.g., +1.0 or -1.0) const AXIS_NORMALIZED_VALUE: float = 1.0 +@export var current_device: DeviceType = DeviceType.KEYBOARD @export var action: String = "" -@export var action_event_index: int = 0 var listening: bool = false @@ -93,6 +99,7 @@ var listening: bool = false # Ready: Setup toggle, text, connect pressed. # :rtype: void func _ready() -> void: + add_to_group("remap_buttons") toggle_mode = true update_button_text() if not pressed.is_connected(_on_pressed): @@ -113,38 +120,64 @@ func _on_pressed() -> void: # :param event: Input event. # :type event: InputEvent # :rtype: void +# In input_remap_button.gd (full updated _input function) func _input(event: InputEvent) -> void: if not listening: return + # Device-specific filtering: Skip if event doesn't match current device + if current_device == DeviceType.KEYBOARD and not event is InputEventKey: + return + + if ( + current_device == DeviceType.GAMEPAD + and not (event is InputEventJoypadButton or event is InputEventJoypadMotion) + ): + return + # Handle keyboard key press if event is InputEventKey and event.pressed: - erase_old_event() - var new_event := InputEventKey.new() + erase_old_event() # Remove the old matching event for this device + var new_event := InputEventKey.new() # Consistent name: new_event new_event.physical_keycode = event.physical_keycode - InputMap.action_add_event(action, new_event) - finish_remap() + InputMap.action_add_event(action, new_event) # Add the new event + # Log the remap right here, using get_event_label on the new event + Globals.log_message( + "User remapped action '" + action + "' to '" + get_event_label(new_event) + "'", + Globals.LogLevel.DEBUG + ) + finish_remap() # Wrap up: update text, stop listening, save, etc. return - # Handle joypad button press + # Handle gamepad (joypad) button press if event is InputEventJoypadButton and event.pressed: - erase_old_event() - var new_event := InputEventJoypadButton.new() + erase_old_event() # Remove the old matching event for this device + var new_event := InputEventJoypadButton.new() # Consistent name: new_event new_event.button_index = event.button_index new_event.device = -1 # All devices - InputMap.action_add_event(action, new_event) - finish_remap() + InputMap.action_add_event(action, new_event) # Add the new event + # Log the remap right here, using get_event_label on the new event + Globals.log_message( + "User remapped action '" + action + "' to '" + get_event_label(new_event) + "'", + Globals.LogLevel.DEBUG + ) + finish_remap() # Wrap up return - # Handle joypad axis motion (if moved past deadzone) + # Handle gamepad (joypad) axis motion (if moved past deadzone) if event is InputEventJoypadMotion and abs(event.axis_value) > AXIS_DEADZONE_THRESHOLD: - erase_old_event() - var new_event := InputEventJoypadMotion.new() + erase_old_event() # Remove the old matching event for this device + var new_event := InputEventJoypadMotion.new() # Consistent name: new_event new_event.axis = event.axis new_event.axis_value = get_normalized_axis_direction(event.axis_value) # Normalize to +1 or -1 new_event.device = -1 # All devices - InputMap.action_add_event(action, new_event) - finish_remap() + InputMap.action_add_event(action, new_event) # Add the new event + # Log the remap right here, using get_event_label on the new event + Globals.log_message( + "User remapped action '" + action + "' to '" + get_event_label(new_event) + "'", + Globals.LogLevel.DEBUG + ) + finish_remap() # Wrap up return @@ -161,9 +194,25 @@ func get_normalized_axis_direction(axis_value: float) -> float: # Erase old event at index. # :rtype: void func erase_old_event() -> void: - var events := InputMap.action_get_events(action) - if events.size() > action_event_index: - InputMap.action_erase_event(action, events[action_event_index]) + var old_ev: InputEvent = get_matching_event() + if old_ev: + InputMap.action_erase_event(action, old_ev) + + +## Gets matching event for current device. +## :rtype: InputEvent|null +func get_matching_event() -> InputEvent: + var events: Array[InputEvent] = InputMap.action_get_events(action) + for ev: InputEvent in events: + if current_device == DeviceType.KEYBOARD and ev is InputEventKey: + return ev + + if ( + current_device == DeviceType.GAMEPAD + and (ev is InputEventJoypadButton or ev is InputEventJoypadMotion) + ): + return ev + return null # In input_remap_button.gd, inside finish_remap() func (before Settings.save_input_mappings()) @@ -173,26 +222,15 @@ func finish_remap() -> void: update_button_text() button_pressed = false listening = false - # Log remap at DEBUG if index valid (uses get_event_label for new binding) - var events: Array[InputEvent] = InputMap.action_get_events(action) - if events.size() > action_event_index: - var new_label: String = get_event_label(events[action_event_index]) - Globals.log_message( - "User remapped action '" + action + "' to '" + new_label + "'", Globals.LogLevel.DEBUG - ) - Settings.save_input_mappings() - get_viewport().set_input_as_handled() + Settings.save_input_mappings() # Save the changes + get_viewport().set_input_as_handled() # Prevent further input propagation # Updated to display key, joypad button, or axis label # :rtype: void func update_button_text() -> void: - var events := InputMap.action_get_events(action) - if events.size() > action_event_index: - var event := events[action_event_index] - text = get_event_label(event) - else: - text = "Unbound" + var ev: InputEvent = get_matching_event() + text = get_event_label(ev) if ev else "Unbound" # Get display label for any event type (uses custom dicts only) diff --git a/scripts/key_mapping.gd b/scripts/key_mapping.gd index fcdeac608..a0d7e2f54 100644 --- a/scripts/key_mapping.gd +++ b/scripts/key_mapping.gd @@ -9,10 +9,12 @@ var js_bridge_wrapper: JavaScriptBridgeWrapper = JavaScriptBridgeWrapper.new() var _controls_back_button_pressed_cb: Variant var _intentional_exit: bool = false -@onready -var controls_back_button: Button = $Panel/OptionsVBoxContainer/BtnContainer/ControlsBackButton -@onready -var controls_reset_button: Button = $Panel/OptionsVBoxContainer/BtnContainer/ControlResetButton +@onready var controls_back_button: Button = $Panel/Options/BtnContainer/ControlsBackButton +@onready var controls_reset_button: Button = $Panel/Options/BtnContainer/ControlResetButton +# NEW: Onreadys for device switcher +@onready var keyboard: CheckButton = $Panel/Options/DeviceTypeContainer/Keyboard +@onready var gamepad: CheckButton = $Panel/Options/DeviceTypeContainer/Gamepad +@onready var device_group: ButtonGroup = ButtonGroup.new() # Mutual exclusivity func _ready() -> void: @@ -28,6 +30,16 @@ func _ready() -> void: if not controls_back_button.pressed.is_connected(_on_controls_back_button_pressed): controls_back_button.pressed.connect(_on_controls_back_button_pressed) + # NEW: Reset button connect (add if not there—resets to defaults) + if not controls_reset_button.pressed.is_connected(_on_reset_pressed): + controls_reset_button.pressed.connect(_on_reset_pressed) + + # NEW: Assign ButtonGroup for exclusivity + keyboard.button_group = device_group + gamepad.button_group = device_group + keyboard.button_pressed = true # Default: Keyboard + update_all_remap_buttons() + process_mode = Node.PROCESS_MODE_ALWAYS Globals.log_message("Controls menu loaded.", Globals.LogLevel.DEBUG) @@ -50,6 +62,45 @@ func _ready() -> void: js_window.controlsBackPressed = _controls_back_button_pressed_cb +## Updates all remap buttons. +## :rtype: void +func update_all_remap_buttons() -> void: + var current: InputRemapButton.DeviceType = ( + InputRemapButton.DeviceType.KEYBOARD + if keyboard.button_pressed + else InputRemapButton.DeviceType.GAMEPAD + ) + var buttons: Array[Node] = get_tree().get_nodes_in_group("remap_buttons") + for btn: InputRemapButton in buttons: + btn.current_device = current + btn.update_button_text() + + +# NEW: Reset button handler—resets InputMap to defaults and updates buttons +func _on_reset_pressed() -> void: + # Function resets only the selected device type (keyboard or gamepad) + var device_type: String = "keyboard" if keyboard.button_pressed else "gamepad" + Settings.reset_to_defaults(device_type) + update_all_remap_buttons() + Globals.log_message("Resetting " + device_type + " controls.", Globals.LogLevel.DEBUG) + + +func _on_keyboard_toggled(toggled_on: bool) -> void: + if toggled_on: + Globals.log_message( + "_on_keyboard_toggled control pressed: " + str(toggled_on), Globals.LogLevel.DEBUG + ) + update_all_remap_buttons() + + +func _on_gamepad_toggled(toggled_on: bool) -> void: + if toggled_on: + Globals.log_message( + "_on_gamepad_toggled control pressed: " + str(toggled_on), Globals.LogLevel.DEBUG + ) + update_all_remap_buttons() + + func _on_controls_back_button_pressed() -> void: ## Handles Back button press. ## diff --git a/scripts/main_menu.gd b/scripts/main_menu.gd index 0b2f2e850..42432475e 100644 --- a/scripts/main_menu.gd +++ b/scripts/main_menu.gd @@ -89,18 +89,14 @@ func _ready() -> void: ) -func _input(event: InputEvent) -> void: +func _input(_event: InputEvent) -> void: ## Handles input events for the main menu. ## ## Logs mouse clicks and unlocks audio on web platforms upon user gesture. ## - ## :param event: The input event to process. - ## :type event: InputEvent + ## :param _event: The input event to process. + ## :type _event: InputEvent ## :rtype: void - if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: - var pos: Vector2 = event.position # Explicitly type as Vector2 - Globals.log_message("Clicked at: (%s, %s)" % [pos.x, pos.y], Globals.LogLevel.DEBUG) - # New: Unlock audio on first qualifying gesture (click or key press) if OS.get_name() == "Web" and not background_music.playing: background_music.play() diff --git a/scripts/pause_menu.gd b/scripts/pause_menu.gd index c136ba1a5..c5d7778c9 100644 --- a/scripts/pause_menu.gd +++ b/scripts/pause_menu.gd @@ -51,7 +51,7 @@ func _unhandled_input(event: InputEvent) -> void: ## :rtype: void if not visible and Globals.options_open: return - if event.is_action_pressed("ui_cancel"): + if event.is_action_pressed("pause"): toggle_pause() diff --git a/scripts/settings.gd b/scripts/settings.gd index dd08c133f..9ab65572c 100644 --- a/scripts/settings.gd +++ b/scripts/settings.gd @@ -6,10 +6,23 @@ extends Node const CONFIG_PATH: String = "user://settings.cfg" + const ACTIONS: Array[String] = [ - "speed_up", "speed_down", "move_left", "move_right", "fire", "next_weapon", "pause" + "speed_up", + "speed_down", + "move_left", + "move_right", + "fire", + "next_weapon", + "pause", + "ui_up", + "ui_down", + "ui_left", + "ui_right", + "ui_accept", ] -const DEFAULT_KEYS: Dictionary = { +# New: Default keyboard mappings. +const DEFAULT_KEYBOARD: Dictionary = { "speed_up": KEY_W, "speed_down": KEY_X, "move_left": KEY_A, @@ -17,11 +30,33 @@ const DEFAULT_KEYS: Dictionary = { "fire": KEY_SPACE, "next_weapon": KEY_Q, "pause": KEY_ESCAPE, + "ui_up": KEY_UP, + "ui_down": KEY_DOWN, + "ui_left": KEY_LEFT, + "ui_right": KEY_RIGHT, + "ui_accept": KEY_ENTER, +} +# New: Default gamepad mappings (assumes Xbox layout; adjust if needed). +const DEFAULT_GAMEPAD: Dictionary = { + "speed_up": {"type": "axis", "axis": JOY_AXIS_TRIGGER_RIGHT, "value": 1.0}, # Throttle up. + "speed_down": {"type": "axis", "axis": JOY_AXIS_TRIGGER_LEFT, "value": 1.0}, # Throttle down. + "move_left": {"type": "axis", "axis": JOY_AXIS_LEFT_X, "value": -1.0}, + "move_right": {"type": "axis", "axis": JOY_AXIS_LEFT_X, "value": 1.0}, + "fire": {"type": "button", "button": JOY_BUTTON_A}, + "next_weapon": {"type": "button", "button": JOY_BUTTON_Y}, + "pause": {"type": "button", "button": JOY_BUTTON_START}, + "ui_accept": {"type": "button", "button": JOY_BUTTON_A}, + "ui_up": {"type": "button", "button": JOY_BUTTON_DPAD_UP}, + "ui_down": {"type": "button", "button": JOY_BUTTON_DPAD_DOWN}, + "ui_left": {"type": "button", "button": JOY_BUTTON_DPAD_LEFT}, + "ui_right": {"type": "button", "button": JOY_BUTTON_DPAD_RIGHT}, } var _needs_migration: bool = false # Flag for old-format upgrade +## Initializes the settings by loading input mappings. +## If migration is needed, saves the updated mappings. func _ready() -> void: load_input_mappings() if _needs_migration: @@ -48,8 +83,8 @@ func serialize_event(ev: InputEvent) -> String: ## Loads input mappings from config, overriding project defaults only if saved. -## Handles old int keycode format for backward compat. -## Skips if no saved data (preserves project key+joypad bindings). +## Handles various formats for backward compatibility and adds defaults if necessary. +## Proceeds even if no file to add defaults where events missing. ## :param path: Config file path (default: CONFIG_PATH). ## :type path: String ## :param actions: Actions to load (default: ACTIONS). @@ -58,56 +93,84 @@ func serialize_event(ev: InputEvent) -> String: func load_input_mappings(path: String = CONFIG_PATH, actions: Array[String] = ACTIONS) -> void: var config: ConfigFile = ConfigFile.new() var err: int = config.load(path) - if err == ERR_FILE_NOT_FOUND: # NEW: Specific check for file missing + if err != OK and err != ERR_FILE_NOT_FOUND: # Handle errors except missing file Globals.log_message( - "No settings file found at " + path + "—using project defaults.", Globals.LogLevel.INFO + "Error loading settings file at " + path + ": " + str(err), Globals.LogLevel.ERROR ) return - - if err != OK: # NEW: Handle other errors (e.g., parse error, permissions) + if err == ERR_FILE_NOT_FOUND: Globals.log_message( - "Error loading settings file at " + path + ": " + str(err), Globals.LogLevel.ERROR + "No settings file found at " + path + "—adding defaults where missing.", + Globals.LogLevel.INFO ) - return for action: String in actions: var has_saved: bool = config.has_section_key("input", action) if has_saved: - InputMap.action_erase_events(action) # Only erase if we're overriding with saved data var value: Variant = config.get_value("input", action) - var serials: Array[String] = [] - if value is int: # Old format: single keycode int - _needs_migration = true - serials = ["key:" + str(value)] - elif value is Array: # New format - for item: String in value: - if typeof(item) == TYPE_STRING: - serials.append(item) + var serialized_events: Array[String] = [] + + if value is Array: + var temp: Array = value + for item: Variant in temp: + if item is String: + serialized_events.append(item) else: Globals.log_message( - "Non-string item in input array for " + action + ": skipping item.", + "Non-string item in array for action '" + action + "': skipped", Globals.LogLevel.WARNING ) + elif value is int: + serialized_events = ["key:" + str(value)] # Old int keycode—migrate to key format + _needs_migration = true + elif value is String: + serialized_events = [value] # Old string—could be "key:87" or plain "87" + _needs_migration = true else: Globals.log_message( - "Invalid saved value for " + action + ": skipping.", Globals.LogLevel.WARNING + "Unexpected type for action '" + action + "': " + str(typeof(value)), + Globals.LogLevel.WARNING ) - continue - for s: String in serials: - _deserialize_and_add(action, s) - # After loading (or skipping), check if empty and add default key if needed + # Fallback: Treat as empty to avoid errors + + InputMap.action_erase_events(action) + for serialized: String in serialized_events: + _deserialize_and_add(action, serialized) + + for action: String in actions: var events: Array[InputEvent] = InputMap.action_get_events(action) - if events.is_empty() and DEFAULT_KEYS.has(action): - var keycode: int = DEFAULT_KEYS[action] - if keycode != -1: - var nev: InputEventKey = InputEventKey.new() - nev.physical_keycode = keycode + var has_key_event: bool = false + var has_joy_event: bool = false + for ev: InputEvent in events: + if ev is InputEventKey: + has_key_event = true + elif ev is InputEventJoypadButton or ev is InputEventJoypadMotion: + has_joy_event = true + if not has_key_event and DEFAULT_KEYBOARD.has(action): + var nev: InputEventKey = InputEventKey.new() + nev.physical_keycode = DEFAULT_KEYBOARD[action] + InputMap.action_add_event(action, nev) + Globals.log_message( + "Added default keyboard event for " + action, Globals.LogLevel.DEBUG + ) + if not has_joy_event and DEFAULT_GAMEPAD.has(action): + var def: Dictionary = DEFAULT_GAMEPAD[action] + if def["type"] == "button": + var nev: InputEventJoypadButton = InputEventJoypadButton.new() + nev.button_index = def["button"] + nev.device = -1 + InputMap.action_add_event(action, nev) + elif def["type"] == "axis": + var nev: InputEventJoypadMotion = InputEventJoypadMotion.new() + nev.axis = def["axis"] + nev.axis_value = def["value"] + nev.device = -1 InputMap.action_add_event(action, nev) - Globals.log_message("Added default key for " + action, Globals.LogLevel.DEBUG) + Globals.log_message("Added default gamepad event for " + action, Globals.LogLevel.DEBUG) -## Deserializes string to event and adds to action. -## Handles device for joy events (-1 if omitted or empty). +## Deserializes a string to an InputEvent and adds it to the specified action. +## Handles various serialized formats and logs warnings for invalid data. ## Skips and warns on malformed serialized strings, including invalid int/float values, ## with robust error handling. ## :param action: Target action. @@ -181,6 +244,12 @@ func _deserialize_and_add(action: String, serialized: String) -> void: Globals.LogLevel.WARNING ) return + elif serialized.is_valid_int(): + var kc: int = int(serialized) + var nev: InputEventKey = InputEventKey.new() + nev.physical_keycode = kc + InputMap.action_add_event(action, nev) + _needs_migration = true # Ensure save if we hit this old case else: Globals.log_message("Unknown serialized prefix: " + serialized, Globals.LogLevel.WARNING) return @@ -215,3 +284,40 @@ func save_input_mappings(path: String = CONFIG_PATH, actions: Array[String] = AC Globals.log_message("Failed to save input mappings: " + str(err), Globals.LogLevel.ERROR) else: Globals.log_message("Input mappings saved.", Globals.LogLevel.DEBUG) + + +## Resets input mappings to defaults for the specified device type. +## :param device_type: "keyboard" or "gamepad" +## :type device_type: String +## :rtype: void +func reset_to_defaults(device_type: String) -> void: + if device_type not in ["keyboard", "gamepad"]: + return + for action: String in ACTIONS: + var events: Array[InputEvent] = InputMap.action_get_events(action).duplicate() + for ev: InputEvent in events: + if device_type == "keyboard" and ev is InputEventKey: + InputMap.action_erase_event(action, ev) + elif ( + device_type == "gamepad" + and (ev is InputEventJoypadButton or ev is InputEventJoypadMotion) + ): + InputMap.action_erase_event(action, ev) + if device_type == "keyboard" and DEFAULT_KEYBOARD.has(action): + var nev: InputEventKey = InputEventKey.new() + nev.physical_keycode = DEFAULT_KEYBOARD[action] + InputMap.action_add_event(action, nev) + elif device_type == "gamepad" and DEFAULT_GAMEPAD.has(action): + var def: Dictionary = DEFAULT_GAMEPAD[action] + if def["type"] == "button": + var nev: InputEventJoypadButton = InputEventJoypadButton.new() + nev.button_index = def["button"] # FIX: Use "button" instead of "index" + nev.device = -1 + InputMap.action_add_event(action, nev) + elif def["type"] == "axis": + var nev: InputEventJoypadMotion = InputEventJoypadMotion.new() + nev.axis = def["axis"] + nev.axis_value = def["value"] + nev.device = -1 + InputMap.action_add_event(action, nev) + save_input_mappings() diff --git a/test/gut/test_audio_reset_button.gd b/test/gut/test_audio_reset_button.gd index 9574b1b7f..169fce856 100644 --- a/test/gut/test_audio_reset_button.gd +++ b/test/gut/test_audio_reset_button.gd @@ -50,8 +50,18 @@ func before_each() -> void: ## Per-test cleanup: Remove test config if exists ## :rtype: void func after_each() -> void: + if is_instance_valid(audio_instance): + if is_instance_valid(audio_instance.master_warning_dialog): + audio_instance.master_warning_dialog.hide() + if is_instance_valid(audio_instance.sfx_warning_dialog): + audio_instance.sfx_warning_dialog.hide() + if audio_instance.get_parent() == self: + remove_child(audio_instance) # If not already + audio_instance.queue_free() + audio_instance = null if FileAccess.file_exists(test_config_path): DirAccess.remove_absolute(test_config_path) + await get_tree().process_frame # Wait for free to process ## TC-Reset-01 | All audio buses muted; All volumes set to 0.5; UI reflects this. | Click the Reset button. | All muted flags false; All volumes 1.0; apply_all_volumes/save_volumes called; UI updated: all mute buttons pressed, all sliders 1.0 and editable; _update_other_controls_ui called; Log message. diff --git a/test/gut/test_combined_multi_manager_loads.gd b/test/gut/test_combined_multi_manager_loads.gd index 8fd2f2052..b9dc96a55 100644 --- a/test/gut/test_combined_multi_manager_loads.gd +++ b/test/gut/test_combined_multi_manager_loads.gd @@ -39,7 +39,7 @@ func before_each() -> void: # Reset InputMap to defaults for action: String in Settings.ACTIONS: InputMap.action_erase_events(action) - var default_key: int = Settings.DEFAULT_KEYS[action] + var default_key: int = Settings.DEFAULT_KEYBOARD[action] var ev: InputEventKey = InputEventKey.new() ev.physical_keycode = default_key InputMap.action_add_event(action, ev) @@ -66,11 +66,15 @@ func test_tc_sl_11() -> void: # Verify Audio assert_almost_eq(AudioManager.master_volume, 0.5, 0.01) assert_true(AudioManager.master_muted) - # Verify Inputs (e.g., speed_up is KEY_W=87) + # Verify Inputs (e.g., speed_up is KEY_W=87, plus added default gamepad) var events: Array = InputMap.action_get_events("speed_up") - assert_eq(events.size(), 1) + assert_eq(events.size(), 2) assert_true(events[0] is InputEventKey) assert_eq(events[0].physical_keycode, 87) + assert_true(events[1] is InputEventJoypadMotion) + assert_eq(events[1].axis, JOY_AXIS_TRIGGER_RIGHT) + assert_eq(events[1].axis_value, 1.0) + assert_eq(events[1].device, -1) # Verify Globals assert_eq(Globals.current_log_level, Globals.LogLevel.WARNING) assert_eq(Globals.difficulty, 1.5) @@ -119,13 +123,17 @@ func test_tc_sl_13() -> void: Settings._needs_migration = false # Verify migrated in InputMap var events: Array = InputMap.action_get_events("speed_up") - assert_eq(events.size(), 1) + assert_eq(events.size(), 2) assert_true(events[0] is InputEventKey) assert_eq(events[0].physical_keycode, 87) + assert_true(events[1] is InputEventJoypadMotion) + assert_eq(events[1].axis, JOY_AXIS_TRIGGER_RIGHT) + assert_eq(events[1].axis_value, 1.0) + assert_eq(events[1].device, -1) # Config upgraded config = ConfigFile.new() config.load(test_config_path) - assert_eq(config.get_value("input", "speed_up"), ["key:87"]) # Upgraded to array string + assert_eq(config.get_value("input", "speed_up"), ["key:87", "joyaxis:5:1.0:-1"]) # Upgraded to array string with added gamepad default # Now save audio changes AudioManager.master_volume = 0.5 AudioManager.save_volumes(test_config_path) @@ -133,7 +141,7 @@ func test_tc_sl_13() -> void: config = ConfigFile.new() config.load(test_config_path) assert_almost_eq(config.get_value("audio", "master_volume"), 0.5, 0.01) - assert_eq(config.get_value("input", "speed_up"), ["key:87"]) + assert_eq(config.get_value("input", "speed_up"), ["key:87", "joyaxis:5:1.0:-1"]) ## TC-SL-14 | Multiple saves: Config with all; Change audio, save; Change settings, save; Change inputs, save. | Sequence of saves from different managers | Each save loads existing, updates own section, saves; All sections preserved across calls; No data loss. @@ -161,7 +169,8 @@ func test_tc_sl_14() -> void: assert_almost_eq(config.get_value("audio", "master_volume"), 0.5, 0.01) assert_eq(config.get_value("Settings", "difficulty"), 2.0) assert_eq(config.get_value("input", "speed_up"), ["key:87"]) - # Change inputs and save (e.g., add another event) + # Change inputs and save (e.g., replace event to simulate remap) + InputMap.action_erase_events("speed_up") var ev: InputEventKey = InputEventKey.new() ev.physical_keycode = KEY_UP InputMap.action_add_event("speed_up", ev) @@ -171,9 +180,8 @@ func test_tc_sl_14() -> void: assert_almost_eq(config.get_value("audio", "master_volume"), 0.5, 0.01) assert_eq(config.get_value("Settings", "difficulty"), 2.0) var serials: Array = config.get_value("input", "speed_up") - assert_eq(serials.size(), 2) - assert_eq(serials[0], "key:87") - assert_eq(serials[1], "key:4194320") # KEY_UP=4194320 + assert_eq(serials.size(), 1) + assert_eq(serials[0], "key:4194320") ## TC-SL-15 | Concurrent-like: Simulate rapid saves from audio then globals. | Call AudioManager.save_volumes(); Immediately Globals._save_settings() | No race (Godot single-threaded), but verify final config has both updates; No overwrites. diff --git a/test/gut/test_error_edge_cases.gd b/test/gut/test_error_edge_cases.gd index 926812de7..0a787e362 100644 --- a/test/gut/test_error_edge_cases.gd +++ b/test/gut/test_error_edge_cases.gd @@ -104,14 +104,14 @@ func test_tc_sl_25() -> void: # Verify upgraded config = ConfigFile.new() config.load(test_config_path) - assert_eq(config.get_value("input", "speed_up"), ["key:87"]) + assert_eq(config.get_value("input", "speed_up"), ["key:87", "joyaxis:5:1.0:-1"]) # Save audio after AudioManager.master_volume = 0.5 AudioManager.save_volumes(test_config_path) # Verify preserves upgraded inputs config = ConfigFile.new() config.load(test_config_path) - assert_eq(config.get_value("input", "speed_up"), ["key:87"]) + assert_eq(config.get_value("input", "speed_up"), ["key:87", "joyaxis:5:1.0:-1"]) assert_eq(config.get_value("audio", "master_volume"), 0.5) # No re-migration assert_false(Settings._needs_migration) diff --git a/test/gut/test_master_volume_control_and_music.gd b/test/gut/test_master_volume_control_and_music.gd index 0c816bf67..72c11cee7 100644 --- a/test/gut/test_master_volume_control_and_music.gd +++ b/test/gut/test_master_volume_control_and_music.gd @@ -10,6 +10,7 @@ var audio_scene: PackedScene = load("res://scenes/audio_settings.tscn") var audio_instance: Control var test_config_path: String = "user://test_music.cfg" + ## Per-test setup: Instantiate audio scene, reset state ## :rtype: void func before_each() -> void: @@ -42,11 +43,22 @@ func before_each() -> void: AudioServer.add_bus() AudioServer.set_bus_name(AudioServer.get_bus_count() - 1, "SFX_Weapon") -## Per-test cleanup: Remove test config if exists + +## Per-test cleanup: Free audio_instance safely. ## :rtype: void func after_each() -> void: + if is_instance_valid(audio_instance): + if is_instance_valid(audio_instance.master_warning_dialog): + audio_instance.master_warning_dialog.hide() + if is_instance_valid(audio_instance.sfx_warning_dialog): + audio_instance.sfx_warning_dialog.hide() + remove_child(audio_instance) + audio_instance.queue_free() + audio_instance = null if FileAccess.file_exists(test_config_path): DirAccess.remove_absolute(test_config_path) + await get_tree().process_frame + ## TC-Music-01 | Master unmuted, Music unmuted, Slider editable | Click and drag Music slider to new value | Slider value changes; AudioServer bus volume updates; AudioManager.music_volume updates; save_debounce_timer starts; After debounce, AudioManager.save_volumes() called; Log messages for volume change. ## :rtype: void @@ -65,6 +77,7 @@ func test_tc_music_01() -> void: print("Config exists after debounce: ", FileAccess.file_exists(test_config_path)) # Debug assert_true(FileAccess.file_exists(test_config_path), "save_volumes should create config") + ## TC-Music-02 | Master unmuted, Music unmuted, Slider editable | Toggle Music mute button (to muted) | mute_music.button_pressed = false; AudioManager.music_muted = true; music_slider.editable = false; AudioServer.set_bus_mute("Music", true); AudioManager.save_volumes() called; Log "Music mute button toggled to: false". ## :rtype: void func test_tc_music_02() -> void: @@ -80,6 +93,7 @@ func test_tc_music_02() -> void: print("Config exists: ", FileAccess.file_exists(test_config_path)) # Debug assert_true(FileAccess.file_exists(test_config_path), "save_volumes should create config") + ## TC-Music-03 | Master unmuted, Music muted, Slider disabled | Toggle Music mute button (to unmuted) | mute_music.button_pressed = true; AudioManager.music_muted = false; music_slider.editable = true; AudioServer.set_bus_mute("Music", false); AudioManager.save_volumes() called; Log "Music mute button toggled to: true". ## :rtype: void func test_tc_music_03() -> void: @@ -99,6 +113,7 @@ func test_tc_music_03() -> void: print("Config exists: ", FileAccess.file_exists(test_config_path)) # Debug assert_true(FileAccess.file_exists(test_config_path), "save_volumes should create config") + ## TC-Music-04 | Master unmuted, Music muted, Slider disabled | Click Music slider | _on_music_mute_toggled(true) called; mute_music.button_pressed = true; AudioManager.music_muted = false; music_slider.editable = true; Event not consumed (allows future drag); No warning dialog; Log "Music mute button toggled to: true". ## :rtype: void func test_tc_music_04() -> void: @@ -121,6 +136,7 @@ func test_tc_music_04() -> void: print("Config exists: ", FileAccess.file_exists(test_config_path)) # Debug assert_true(FileAccess.file_exists(test_config_path), "save_volumes should create config") + ## TC-Music-05 | Master unmuted, Music unmuted, Slider editable | Click Music mute button (already unmuted) | mute_music.button_pressed = false; AudioManager.music_muted = true; music_slider.editable = false; AudioServer.set_bus_mute("Music", true); AudioManager.save_volumes() called; Log "Music mute button toggled to: false". (NOTE: Test plan expected no change, but this is incorrect—clicking should toggle to muted. Updated to match actual behavior.) ## :rtype: void func test_tc_music_05() -> void: @@ -145,6 +161,7 @@ func test_tc_music_05() -> void: print("Config exists: ", FileAccess.file_exists(test_config_path)) # Debug assert_true(FileAccess.file_exists(test_config_path), "save_volumes should create config") # Save from toggle + ## TC-Music-06 | Master muted, Music unmuted, Slider disabled (due to master) | Click Music slider | master_warning_dialog.popup_centered(); Event consumed; No unmute; Slider remains disabled; No save/apply. ## :rtype: void func test_tc_music_06() -> void: @@ -167,6 +184,7 @@ func test_tc_music_06() -> void: print("Config exists: ", FileAccess.file_exists(test_config_path)) # Debug assert_false(FileAccess.file_exists(test_config_path), "No save") + ## TC-Music-07 | Master muted, Music unmuted, Mute button disabled | Click Music mute button | master_warning_dialog.popup_centered(); Event consumed; No toggle; Mute button remains disabled; No save/apply. ## :rtype: void func test_tc_music_07() -> void: @@ -195,6 +213,7 @@ func test_tc_music_07() -> void: print("Config exists: ", FileAccess.file_exists(test_config_path)) # Debug assert_false(FileAccess.file_exists(test_config_path), "No save") + ## TC-Music-08 | Master muted, Music muted, Slider disabled | Click Music slider | master_warning_dialog.popup_centered(); Event consumed; No unmute; Slider remains disabled. ## :rtype: void func test_tc_music_08() -> void: @@ -216,6 +235,7 @@ func test_tc_music_08() -> void: print("Slider editable: ", audio_instance.music_slider.editable) # Debug assert_false(audio_instance.music_slider.editable) # Disabled + ## TC-Music-09 | Master muted, Music muted, Mute button disabled | Click Music mute button | master_warning_dialog.popup_centered(); Event consumed; No toggle. ## :rtype: void func test_tc_music_09() -> void: @@ -241,6 +261,7 @@ func test_tc_music_09() -> void: print("Button pressed: ", audio_instance.mute_music.button_pressed) # Debug assert_false(audio_instance.mute_music.button_pressed) # No change (muted) + ## TC-Music-10 | Master unmuted (after muted), Music muted | Master toggle to unmuted | _update_other_controls_ui() called; mute_music.disabled = false; music_slider.editable = false (still muted); No auto-unmute of Music. ## :rtype: void func test_tc_music_10() -> void: @@ -258,6 +279,7 @@ func test_tc_music_10() -> void: print("Music muted: ", AudioManager.music_muted) # Debug assert_true(AudioManager.music_muted) # No auto-unmute + ## TC-Music-11 | Initial load from config: Music muted | Scene _ready() | mute_music.button_pressed = false; music_slider.editable = false; AudioServer muted; Log messages from load_volumes(). ## :rtype: void func test_tc_music_11() -> void: @@ -275,6 +297,7 @@ func test_tc_music_11() -> void: print("Bus mute: ", AudioServer.is_bus_mute(AudioServer.get_bus_index("Music"))) # Debug assert_true(AudioServer.is_bus_mute(AudioServer.get_bus_index("Music"))) + ## TC-Music-12 | Initial load from config: Music unmuted | Scene _ready() | mute_music.button_pressed = true; music_slider.editable = true; AudioServer unmuted. ## :rtype: void func test_tc_music_12() -> void: @@ -293,6 +316,7 @@ func test_tc_music_12() -> void: print("Bus mute: ", AudioServer.is_bus_mute(AudioServer.get_bus_index("Music"))) # Debug assert_false(AudioServer.is_bus_mute(AudioServer.get_bus_index("Music"))) + ## TC-Music-13 | Master unmuted, Music unmuted | Non-left click on slider (e.g., right-click) | No action; Event not handled/consumed. ## :rtype: void func test_tc_music_13() -> void: @@ -303,6 +327,7 @@ func test_tc_music_13() -> void: print("Music muted: ", AudioManager.music_muted) # Debug assert_false(AudioManager.music_muted) # No action, still unmuted + ## TC-Music-14 | Master unmuted, Music muted | Mouse motion (drag attempt) on slider | No value change (editable=false); No unmute (requires press). ## :rtype: void func test_tc_music_14() -> void: @@ -319,6 +344,7 @@ func test_tc_music_14() -> void: print("Music muted: ", AudioManager.music_muted) # Debug assert_true(AudioManager.music_muted) # No unmute + ## TC-Music-15 | Unexpected exit (queue_free without back press) | Simulate tree_exited | Previous menu (e.g., options) visible = true; hidden_menus.pop_back(); If web, backPressed restored; Overlays hidden. ## :rtype: void func test_tc_music_15() -> void: diff --git a/test/gut/test_preserve_other_sections.gd b/test/gut/test_preserve_other_sections.gd index 5dacd23db..9b0669e69 100644 --- a/test/gut/test_preserve_other_sections.gd +++ b/test/gut/test_preserve_other_sections.gd @@ -40,20 +40,22 @@ func before_each() -> void: # Reset Globals settings Globals.current_log_level = Globals.LogLevel.INFO Globals.difficulty = 1.0 + - -## Per-test cleanup: Restore config backup, free instance. +## Per-test cleanup: Free audio_instance safely. ## :rtype: void func after_each() -> void: if is_instance_valid(audio_instance): + if is_instance_valid(audio_instance.master_warning_dialog): + audio_instance.master_warning_dialog.hide() + if is_instance_valid(audio_instance.sfx_warning_dialog): + audio_instance.sfx_warning_dialog.hide() + remove_child(audio_instance) audio_instance.queue_free() - await get_tree().process_frame - if FileAccess.file_exists(backup_path): - DirAccess.copy_absolute(backup_path, test_config_path) - DirAccess.remove_absolute(backup_path) - else: - if FileAccess.file_exists(test_config_path): - DirAccess.remove_absolute(test_config_path) + audio_instance = null + if FileAccess.file_exists(test_config_path): + DirAccess.remove_absolute(test_config_path) + await get_tree().process_frame ## TC-SL-06 | Config exists with "input" section (e.g., speed_up=["key:87"]); "Settings" (log_level=1, difficulty=1.5); No "audio". | Call AudioManager.save_volumes() with changes (e.g., sfx_volume=0.6) | "audio" section added with changes; "input" and "Settings" unchanged; Log "Saved volumes to config." diff --git a/test/gut/test_reset_scenarios.gd b/test/gut/test_reset_scenarios.gd index f33fc7705..7880462c3 100644 --- a/test/gut/test_reset_scenarios.gd +++ b/test/gut/test_reset_scenarios.gd @@ -40,12 +40,21 @@ func before_each() -> void: Globals.difficulty = 1.0 -## Per-test cleanup: Free instance if exists. +## Per-test cleanup: Free audio_instance safely. ## :rtype: void func after_each() -> void: + var audio_instance: Control = self.audio_instance # Typed ref if is_instance_valid(audio_instance): + if is_instance_valid(audio_instance.master_warning_dialog): + audio_instance.master_warning_dialog.hide() + if is_instance_valid(audio_instance.sfx_warning_dialog): + audio_instance.sfx_warning_dialog.hide() + remove_child(audio_instance) audio_instance.queue_free() - await get_tree().process_frame + self.audio_instance = null + if FileAccess.file_exists(test_config_path): + DirAccess.remove_absolute(test_config_path) + await get_tree().process_frame ## TC-SL-16 | Config with all; Non-audio changed (e.g., difficulty=2.0). | Call AudioManager.reset_volumes() | Only "audio" reset to defaults and saved; "Settings" difficulty unchanged; "input" preserved; Log "Audio volumes reset to defaults."; apply_all_volumes() called. diff --git a/test/gut/test_sfx_rotor_volume_control.gd b/test/gut/test_sfx_rotor_volume_control.gd index 6bc35e30a..a4399efee 100644 --- a/test/gut/test_sfx_rotor_volume_control.gd +++ b/test/gut/test_sfx_rotor_volume_control.gd @@ -44,11 +44,21 @@ func before_each() -> void: AudioServer.set_bus_name(AudioServer.get_bus_count() - 1, "SFX_Weapon") -## Per-test cleanup: Remove test config if exists +## Per-test cleanup: Free audio_instance safely. ## :rtype: void func after_each() -> void: + var audio_instance: Control = self.audio_instance # Typed ref + if is_instance_valid(audio_instance): + if is_instance_valid(audio_instance.master_warning_dialog): + audio_instance.master_warning_dialog.hide() + if is_instance_valid(audio_instance.sfx_warning_dialog): + audio_instance.sfx_warning_dialog.hide() + remove_child(audio_instance) + audio_instance.queue_free() + self.audio_instance = null if FileAccess.file_exists(test_config_path): DirAccess.remove_absolute(test_config_path) + await get_tree().process_frame ## TC-Rotor-01 | Master unmuted, SFX unmuted, Rotor unmuted, Slider editable | Click and drag Rotor slider to new value | Slider value changes; AudioServer bus volume updates; AudioManager.rotors_volume updates; save_debounce_timer starts; After debounce, AudioManager.save_volumes() called; Log messages for volume change. diff --git a/test/gut/test_sfx_volume_control.gd b/test/gut/test_sfx_volume_control.gd index c3a330a2e..9bf542c26 100644 --- a/test/gut/test_sfx_volume_control.gd +++ b/test/gut/test_sfx_volume_control.gd @@ -44,11 +44,21 @@ func before_each() -> void: AudioServer.set_bus_name(AudioServer.get_bus_count() - 1, "SFX_Weapon") -## Per-test cleanup: Remove test config if exists +## Per-test cleanup: Free audio_instance safely. ## :rtype: void func after_each() -> void: + var audio_instance: Control = self.audio_instance # Typed ref + if is_instance_valid(audio_instance): + if is_instance_valid(audio_instance.master_warning_dialog): + audio_instance.master_warning_dialog.hide() + if is_instance_valid(audio_instance.sfx_warning_dialog): + audio_instance.sfx_warning_dialog.hide() + remove_child(audio_instance) + audio_instance.queue_free() + self.audio_instance = null if FileAccess.file_exists(test_config_path): DirAccess.remove_absolute(test_config_path) + await get_tree().process_frame ## TC-SFX-01 | Master unmuted, SFX unmuted, Slider editable, Children unmuted | Click and drag SFX slider to new value | Slider value changes; AudioServer bus volume updates; AudioManager.sfx_volume updates; save_debounce_timer starts; After debounce, AudioManager.save_volumes() called; Log messages for volume change. @@ -312,3 +322,4 @@ func test_tc_sfx_15() -> void: await get_tree().process_frame assert_true(prev_menu.visible) assert_true(Globals.hidden_menus.is_empty()) + prev_menu.queue_free() diff --git a/test/gut/test_sfx_weapon_volume_control.gd b/test/gut/test_sfx_weapon_volume_control.gd index cc1dee370..eaf95a25d 100644 --- a/test/gut/test_sfx_weapon_volume_control.gd +++ b/test/gut/test_sfx_weapon_volume_control.gd @@ -44,11 +44,21 @@ func before_each() -> void: AudioServer.set_bus_name(AudioServer.get_bus_count() - 1, "SFX_Weapon") -## Per-test cleanup: Remove test config if exists +## Per-test cleanup: Free audio_instance safely. ## :rtype: void func after_each() -> void: + var audio_instance: Control = self.audio_instance # Typed ref + if is_instance_valid(audio_instance): + if is_instance_valid(audio_instance.master_warning_dialog): + audio_instance.master_warning_dialog.hide() + if is_instance_valid(audio_instance.sfx_warning_dialog): + audio_instance.sfx_warning_dialog.hide() + remove_child(audio_instance) + audio_instance.queue_free() + self.audio_instance = null if FileAccess.file_exists(test_config_path): DirAccess.remove_absolute(test_config_path) + await get_tree().process_frame ## TC-Weapon-01 | Master unmuted, SFX unmuted, Weapon unmuted, Slider editable | Click and drag Weapon slider to new value | Slider value changes; AudioServer bus volume updates; AudioManager.weapon_volume updates; save_debounce_timer starts; After debounce, AudioManager.save_volumes() called; Log messages for volume change. diff --git a/test/test_input_remap_button.gd b/test/test_input_remap_button.gd index 068f606d9..add8e562f 100644 --- a/test/test_input_remap_button.gd +++ b/test/test_input_remap_button.gd @@ -1,26 +1,24 @@ -# test_input_remap_button.gd (updated for new features: keyboard, joypad button, motion; tests display & remap) -# Unit tests for InputRemapButton class using GdUnit4. -# Covers label display for various input types, unbound state, remapping simulations, and fallback labels. -# Assumes GdUnit4 is installed via AssetLib. -# Run via GdUnit Inspector or command line. -# :classname: test_input_remap_button +## test_input_remap_button.gd +## +## Unit tests for InputRemapButton class using GdUnit4. +## +## Covers label display for various input types, unbound state, remapping simulations, and fallback labels. +## Assumes GdUnit4 is installed via AssetLib. +## Run via GdUnit Inspector or command line. extends GdUnitTestSuite ## Global setup if needed (e.g., mock Globals/Settings if logging/save called) -## :param none ## :rtype: void func before() -> void: pass ## Global cleanup -## :param none ## :rtype: void func after() -> void: pass ## Test keyboard label display (original, updated for Godot 4.x) -## :param none ## :rtype: void func test_keyboard_label_display() -> void: # Clean up if action exists @@ -45,7 +43,6 @@ func test_keyboard_label_display() -> void: InputMap.erase_action("test_action") ## Test joypad button label display -## :param none ## :rtype: void func test_joypad_button_label_display() -> void: if InputMap.has_action("test_action"): @@ -59,6 +56,7 @@ func test_joypad_button_label_display() -> void: var button: InputRemapButton = auto_free(InputRemapButton.new()) button.action = "test_action" + button.current_device = InputRemapButton.DeviceType.GAMEPAD button._ready() assert_str(button.text).is_equal("A") @@ -66,7 +64,6 @@ func test_joypad_button_label_display() -> void: InputMap.erase_action("test_action") ## Test joypad motion (axis) label display -## :param none ## :rtype: void func test_joypad_motion_label_display() -> void: if InputMap.has_action("test_action"): @@ -81,6 +78,7 @@ func test_joypad_motion_label_display() -> void: var button: InputRemapButton = auto_free(InputRemapButton.new()) button.action = "test_action" + button.current_device = InputRemapButton.DeviceType.GAMEPAD button._ready() assert_str(button.text).is_equal("Left Stick Left") @@ -88,7 +86,6 @@ func test_joypad_motion_label_display() -> void: InputMap.erase_action("test_action") ## Test unbound display -## :param none ## :rtype: void func test_unbound_label_display() -> void: if InputMap.has_action("test_action"): @@ -105,7 +102,6 @@ func test_unbound_label_display() -> void: InputMap.erase_action("test_action") ## Test remapping keyboard (simulate input) -## :param none ## :rtype: void func test_remap_keyboard() -> void: if InputMap.has_action("test_action"): @@ -134,7 +130,7 @@ func test_remap_keyboard() -> void: assert_bool(button.listening).is_false() # Verify event added - var events: Array = InputMap.action_get_events("test_action") + var events: Array[InputEvent] = InputMap.action_get_events("test_action") assert_int(events.size()).is_equal(1) assert_that(events[0] is InputEventKey).is_true() assert_int(events[0].physical_keycode).is_equal(KEY_D) @@ -142,7 +138,6 @@ func test_remap_keyboard() -> void: InputMap.erase_action("test_action") ## Test remapping joypad button -## :param none ## :rtype: void func test_remap_joypad_button() -> void: if InputMap.has_action("test_action"): @@ -152,6 +147,7 @@ func test_remap_joypad_button() -> void: var button: InputRemapButton = auto_free(InputRemapButton.new()) button.action = "test_action" + button.current_device = InputRemapButton.DeviceType.GAMEPAD add_child(button) # Add to scene tree for viewport access button._ready() @@ -167,7 +163,7 @@ func test_remap_joypad_button() -> void: assert_str(button.text).is_equal("B") assert_bool(button.listening).is_false() - var events: Array = InputMap.action_get_events("test_action") + var events: Array[InputEvent] = InputMap.action_get_events("test_action") assert_int(events.size()).is_equal(1) assert_that(events[0] is InputEventJoypadButton).is_true() assert_int(events[0].button_index).is_equal(JOY_BUTTON_B) @@ -176,7 +172,6 @@ func test_remap_joypad_button() -> void: InputMap.erase_action("test_action") ## Test remapping joypad motion (axis) -## :param none ## :rtype: void func test_remap_joypad_motion() -> void: if InputMap.has_action("test_action"): @@ -186,6 +181,7 @@ func test_remap_joypad_motion() -> void: var button: InputRemapButton = auto_free(InputRemapButton.new()) button.action = "test_action" + button.current_device = InputRemapButton.DeviceType.GAMEPAD add_child(button) # Add to scene tree for viewport access button._ready() @@ -201,7 +197,7 @@ func test_remap_joypad_motion() -> void: assert_str(button.text).is_equal("Left Stick Down") assert_bool(button.listening).is_false() - var events: Array = InputMap.action_get_events("test_action") + var events: Array[InputEvent] = InputMap.action_get_events("test_action") assert_int(events.size()).is_equal(1) assert_that(events[0] is InputEventJoypadMotion).is_true() assert_int(events[0].axis).is_equal(JOY_AXIS_LEFT_Y) @@ -211,7 +207,6 @@ func test_remap_joypad_motion() -> void: InputMap.erase_action("test_action") ## Test fallback labels (unknown button/axis) -## :param none ## :rtype: void func test_fallback_labels() -> void: if InputMap.has_action("test_action"): @@ -226,6 +221,7 @@ func test_fallback_labels() -> void: var button: InputRemapButton = auto_free(InputRemapButton.new()) button.action = "test_action" + button.current_device = InputRemapButton.DeviceType.GAMEPAD button._ready() assert_str(button.text).is_equal("Button 999") @@ -240,7 +236,7 @@ func test_fallback_labels() -> void: InputMap.erase_action("test_action") -## Test finish_remap with invalid index (no error, skips log, adds event but displays Unbound). +## Test remap keyboard adds event ## :rtype: void func test_finish_remap_invalid_index() -> void: if InputMap.has_action("test_action"): @@ -250,7 +246,6 @@ func test_finish_remap_invalid_index() -> void: var button: InputRemapButton = auto_free(InputRemapButton.new()) button.action = "test_action" - button.action_event_index = 99 # Invalid high index add_child(button) button._ready() @@ -262,11 +257,11 @@ func test_finish_remap_invalid_index() -> void: sim_event.pressed = true button._input(sim_event) # Calls finish_remap, should not error - assert_str(button.text).is_equal("Unbound") # Displays Unbound due to invalid index + assert_str(button.text).is_equal("A") assert_bool(button.listening).is_false() # Verify event added despite invalid index - var events: Array = InputMap.action_get_events("test_action") + var events: Array[InputEvent] = InputMap.action_get_events("test_action") assert_int(events.size()).is_equal(1) assert_that(events[0] is InputEventKey).is_true() assert_int(events[0].physical_keycode).is_equal(KEY_A) @@ -274,7 +269,6 @@ func test_finish_remap_invalid_index() -> void: InputMap.erase_action("test_action") ## Test keyboard fallback label for key not in custom dict -## :param none ## :rtype: void func test_keyboard_fallback_label() -> void: if InputMap.has_action("test_action"): diff --git a/test/test_settings.gd b/test/test_settings.gd index 5394b77a5..78de3cf85 100644 --- a/test/test_settings.gd +++ b/test/test_settings.gd @@ -1,6 +1,6 @@ -# test_settings.gd (updated for new features: multi-event, joypad button/motion, backward compat, defaults) -# Uses GdUnit4 (assume installed; install via AssetLib if not). -# Run via GdUnit Inspector or command line. +## test_settings.gd (updated for new features: multi-event, joypad button/motion, backward compat, defaults) +## Uses GdUnit4 (assume installed; install via AssetLib if not). +## Run via GdUnit Inspector or command line. extends GdUnitTestSuite @warning_ignore("unused_parameter") @@ -159,41 +159,39 @@ func test_backward_compat_old_format() -> void: assert_that(events[0] is InputEventKey).is_true() assert_int(events[0].physical_keycode).is_equal(KEY_Q) -# Test default key fallback if no saved data for action +# Test default fallback when no file func test_default_key_fallback() -> void: - var test_path: String = "user://test_settings.cfg" + var test_path: String = "user://default_test.cfg" var test_actions: Array[String] = ["speed_up"] - # Backup original - var original_events: Array[InputEvent] = InputMap.action_get_events("speed_up") - - # Create empty config (no key for action) - var config: ConfigFile = ConfigFile.new() - config.save(test_path) - - assert_bool(FileAccess.file_exists(test_path)).is_true() - - # Erase and load (should add default KEY_W since no saved) + # No file—load should add defaults where missing (but speed_up empty initially) InputMap.action_erase_events("speed_up") Settings.load_input_mappings(test_path, test_actions) - # Verify default added + # Verify defaults added (key and joy for speed_up) var events: Array[InputEvent] = InputMap.action_get_events("speed_up") - assert_int(events.size()).is_equal(1) - assert_that(events[0] is InputEventKey).is_true() - assert_int(events[0].physical_keycode).is_equal(KEY_W) + assert_int(events.size()).is_equal(2) - # Restore original - InputMap.action_erase_events("speed_up") - for ev: InputEvent in original_events: - InputMap.action_add_event("speed_up", ev) + var key_found: bool = false + var joy_found: bool = false + for ev: InputEvent in events: + if ev is InputEventKey: + assert_int(ev.physical_keycode).is_equal(KEY_W) + key_found = true + elif ev is InputEventJoypadMotion: + assert_int(ev.axis).is_equal(JOY_AXIS_TRIGGER_RIGHT) + assert_float(ev.axis_value).is_equal(1.0) + assert_int(ev.device).is_equal(-1) + joy_found = true + assert_bool(key_found).is_true() + assert_bool(joy_found).is_true() -# Test unbound action save/load (empty array) +# Test unbound persistence (empty array saved/loaded) func test_unbound_action_persistence() -> void: - var test_path: String = "user://test_settings.cfg" + var test_path: String = "user://unbound_test.cfg" var test_actions: Array[String] = ["test_action"] - # Unbind + # Set unbound (erase events) InputMap.action_erase_events("test_action") # Save (should save empty array) @@ -201,16 +199,19 @@ func test_unbound_action_persistence() -> void: assert_bool(FileAccess.file_exists(test_path)).is_true() - # Load (should load empty, no default for test_action) + # Add temp event, then load (should erase to unbound) + var temp_event: InputEventKey = InputEventKey.new() + temp_event.physical_keycode = KEY_B + InputMap.action_add_event("test_action", temp_event) Settings.load_input_mappings(test_path, test_actions) # Verify empty var events: Array[InputEvent] = InputMap.action_get_events("test_action") assert_int(events.size()).is_equal(0) -# Test multi-action persistence (updated for array) +# Test multi-action persistence func test_multi_action_persistence() -> void: - var test_path: String = "user://multi_test.cfg" + var test_path: String = "user://multi_action.cfg" var test_actions: Array[String] = ["test_action1", "test_action2"] # Set action1 with key, action2 unbound