From bacc66d26c4f64e3cdba167f3f3aa6f614096403 Mon Sep 17 00:00:00 2001 From: Vladimir nett00n Budylnikov Date: Thu, 25 Sep 2025 11:11:49 +0400 Subject: [PATCH] add gamepad support to menus --- CLAUDE.md | 2 + docs/CLAUDE.md | 2 + docs/MAP.md | 16 +- docs/UI_COMPONENTS.md | 281 +++++++++++++++++++++ examples/LanguageSelectorExample.gd | 25 ++ examples/LanguageSelectorExample.tscn | 58 +++++ examples/ValueStepperExample.gd | 77 ++++++ examples/ValueStepperExample.tscn | 113 +++++++++ scenes/game/gameplays/tile.gd | 1 - scenes/ui/MainMenu.gd | 58 +++++ scenes/ui/SettingsMenu.gd | 176 +++++++++---- scenes/ui/SettingsMenu.tscn | 21 +- scenes/ui/components/LanguageSelector.gd | 118 +++++++++ scenes/ui/components/LanguageSelector.tscn | 26 ++ scenes/ui/components/ValueStepper.gd | 190 ++++++++++++++ scenes/ui/components/ValueStepper.tscn | 26 ++ 16 files changed, 1122 insertions(+), 68 deletions(-) create mode 100644 CLAUDE.md create mode 100644 docs/UI_COMPONENTS.md create mode 100644 examples/LanguageSelectorExample.gd create mode 100644 examples/LanguageSelectorExample.tscn create mode 100644 examples/ValueStepperExample.gd create mode 100644 examples/ValueStepperExample.tscn create mode 100644 scenes/ui/components/LanguageSelector.gd create mode 100644 scenes/ui/components/LanguageSelector.tscn create mode 100644 scenes/ui/components/ValueStepper.gd create mode 100644 scenes/ui/components/ValueStepper.tscn diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6ac09a9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,2 @@ +- The documentation of the project is located in docs/ directory. +So the docs\CLAUDE.md does. Get it in context before doing anything else. diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index e9121cb..d761e06 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -88,6 +88,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ### Documentation Structure - **`docs/MAP.md`** - Complete project architecture and structure +- **`docs/UI_COMPONENTS.md`** - Custom UI components - **`docs/CODE_OF_CONDUCT.md`** - Coding standards and best practices - **`docs/TESTING.md`** - Testing guidelines and conventions - **This file** - Claude Code specific development guidelines @@ -130,6 +131,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - **Audio**: Use `AudioManager` for music and sound effects - **Localization**: Use `LocalizationManager` for language switching - **UI Components**: Extend `DebugMenuBase` for debug menus to avoid code duplication +- **Value Selection**: Use `ValueStepper` component for discrete option selection (language, resolution, difficulty) - **Memory Management**: Use `queue_free()` and await frame completion for safe cleanup - **Input Validation**: Always validate user inputs with type checking and bounds validation diff --git a/docs/MAP.md b/docs/MAP.md index 618166b..5b9142c 100644 --- a/docs/MAP.md +++ b/docs/MAP.md @@ -110,18 +110,22 @@ game.tscn (Gameplay Container) ### UI Components ``` scenes/ui/ -├── DebugToggle.tscn + DebugToggle.gd # Now available on all major scenes -├── DebugMenuBase.gd # Unified base class for debug menus -├── DebugMenu.tscn + DebugMenu.gd # Global debug controls (extends DebugMenuBase) -├── Match3DebugMenu.gd # Match-3 specific debug controls (extends DebugMenuBase) -├── MainMenu.tscn + MainMenu.gd -└── SettingsMenu.tscn + SettingsMenu.gd # With comprehensive input validation +├── components/ +│ └── ValueStepper.tscn + ValueStepper.gd # Reusable arrow-based value selector +├── DebugToggle.tscn + DebugToggle.gd # Available on all major scenes +├── DebugMenuBase.gd # Unified base class for debug menus +├── DebugMenu.tscn + DebugMenu.gd # Global debug controls (extends DebugMenuBase) +├── Match3DebugMenu.gd # Match-3 specific debug controls (extends DebugMenuBase) +├── MainMenu.tscn + MainMenu.gd # With gamepad/keyboard navigation +└── SettingsMenu.tscn + SettingsMenu.gd # With comprehensive input validation ``` **Code Quality Improvements:** +- **ValueStepper Component**: Reusable arrow-based selector for discrete values (language, resolution, difficulty) - **DebugMenuBase.gd**: Eliminates 90% code duplication between debug menu classes - **Input Validation**: All user inputs are validated and sanitized before processing - **Error Recovery**: Robust error handling with fallback mechanisms throughout UI +- **Navigation Support**: Full gamepad/keyboard navigation across all menus ## Modular Gameplay System diff --git a/docs/UI_COMPONENTS.md b/docs/UI_COMPONENTS.md new file mode 100644 index 0000000..b8b2585 --- /dev/null +++ b/docs/UI_COMPONENTS.md @@ -0,0 +1,281 @@ +# UI Components + +This document describes the custom UI components available in the Skelly project. + +## ValueStepper Component + +### Overview + +**ValueStepper** is a reusable UI control for stepping through discrete values with left/right arrow navigation. It provides an intuitive interface for selecting from predefined options and is particularly well-suited for game settings menus. + +**Location**: `scenes/ui/components/ValueStepper.tscn` and `scenes/ui/components/ValueStepper.gd` + +### Why ValueStepper Exists + +Godot's built-in controls have limitations for discrete option selection: +- **OptionButton**: Dropdown popups don't work well with gamepad navigation +- **SpinBox**: Designed for numeric values, not text options +- **HSlider**: Better for continuous values, not discrete choices + +ValueStepper fills this gap by providing: +- ✅ **Gamepad-friendly** discrete option selection +- ✅ **No popup complications** - values displayed inline +- ✅ **Dual input support** - mouse clicks + keyboard/gamepad +- ✅ **Clean horizontal layout** with current value always visible +- ✅ **Perfect for game settings** like language, difficulty, resolution + +### Features + +- **Multiple Data Sources**: Built-in support for language, resolution, difficulty +- **Custom Values**: Easy setup with custom arrays of values +- **Navigation Integration**: Built-in highlighting and input handling +- **Signal-Based**: Clean event communication with parent scenes +- **Visual Feedback**: Automatic highlighting and animations +- **Audio Support**: Integrated click sounds +- **Flexible Display**: Separate display names and internal values + +### Visual Structure + +``` +[<] [Current Value] [>] +``` + +- **Left Arrow Button** (`<`): Navigate to previous value +- **Value Display**: Shows current selection (e.g., "English", "Hard", "1920×1080") +- **Right Arrow Button** (`>`): Navigate to next value + +## API Reference + +### Signals + +```gdscript +signal value_changed(new_value: String, new_index: int) +``` +Emitted when the value changes, providing both the new value string and its index. + +### Properties + +```gdscript +@export var data_source: String = "language" +@export var custom_format_function: String = "" +``` + +- **data_source**: Determines the data type ("language", "resolution", "difficulty", or "custom") +- **custom_format_function**: Reserved for future custom formatting (currently unused) + +### Key Methods + +#### Setup and Configuration +```gdscript +func setup_custom_values(custom_values: Array[String], custom_display_names: Array[String] = []) +``` +Configure the stepper with custom values and optional display names. + +#### Value Management +```gdscript +func get_current_value() -> String +func set_current_value(value: String) +func change_value(direction: int) +``` +Get, set, or modify the current value programmatically. + +#### Navigation Integration +```gdscript +func set_highlighted(highlighted: bool) +func handle_input_action(action: String) -> bool +func get_control_name() -> String +``` +Integration methods for navigation systems and visual feedback. + +## Usage Examples + +### Basic Usage in Scene + +1. **Add to Scene**: Instance `ValueStepper.tscn` in your scene +2. **Set Data Source**: Configure the `data_source` property +3. **Connect Signal**: Connect the `value_changed` signal + +```gdscript +# In your scene script +@onready var language_stepper: ValueStepper = $LanguageStepper + +func _ready(): + language_stepper.value_changed.connect(_on_language_changed) + +func _on_language_changed(new_value: String, new_index: int): + print("Language changed to: ", new_value) +``` + +### Built-in Data Sources + +#### Language Selection +```gdscript +# Set data_source = "language" in editor or code +language_stepper.data_source = "language" +``` +Automatically loads available languages from SettingsManager. + +#### Resolution Selection +```gdscript +# Set data_source = "resolution" +resolution_stepper.data_source = "resolution" +``` +Provides common resolution options with display names. + +#### Difficulty Selection +```gdscript +# Set data_source = "difficulty" +difficulty_stepper.data_source = "difficulty" +``` +Provides difficulty levels: Easy, Normal, Hard, Nightmare. + +### Custom Values + +```gdscript +# Setup custom theme selector +var theme_values = ["light", "dark", "blue", "green"] +var theme_names = ["Light Theme", "Dark Theme", "Blue Theme", "Green Theme"] +theme_stepper.setup_custom_values(theme_values, theme_names) +theme_stepper.data_source = "theme" # For better logging +``` + +### Navigation System Integration + +```gdscript +# In a navigation-enabled menu +var navigable_controls: Array[Control] = [] + +func _setup_navigation(): + navigable_controls.append(volume_slider) + navigable_controls.append(language_stepper) # Add stepper to navigation + navigable_controls.append(back_button) + +func _update_visual_selection(): + for i in range(navigable_controls.size()): + var control = navigable_controls[i] + if control is ValueStepper: + control.set_highlighted(i == current_index) + else: + # Handle other control highlighting + pass + +func _handle_input(action: String): + var current_control = navigable_controls[current_index] + if current_control is ValueStepper: + if current_control.handle_input_action(action): + AudioManager.play_ui_click() + return true + return false +``` + +## Integration Patterns + +### Settings Menu Pattern +See `scenes/ui/SettingsMenu.gd` for a complete example of integrating ValueStepper into a settings menu with full navigation support. + +### Multiple Steppers Navigation +See `examples/ValueStepperExample.gd` for an example showing multiple steppers with keyboard/gamepad navigation. + +## Extending ValueStepper + +### Adding New Data Sources + +1. **Add to `_load_data()` method**: +```gdscript +func _load_data(): + match data_source: + "language": + _load_language_data() + "your_custom_type": + _load_your_custom_data() + # ... other cases +``` + +2. **Implement your loader**: +```gdscript +func _load_your_custom_data(): + values = ["value1", "value2", "value3"] + display_names = ["Display 1", "Display 2", "Display 3"] + current_index = 0 +``` + +3. **Add value application logic**: +```gdscript +func _apply_value_change(new_value: String, index: int): + match data_source: + "your_custom_type": + # Apply your custom logic here + YourManager.set_custom_setting(new_value) +``` + +### Custom Formatting + +Override `_update_display()` for custom display formatting: + +```gdscript +func _update_display(): + if data_source == "your_custom_type": + # Custom formatting logic + value_display.text = "Custom: " + display_names[current_index] + else: + super._update_display() # Call parent implementation +``` + +## Best Practices + +### When to Use ValueStepper +- ✅ **Discrete options**: Language, difficulty, resolution, theme +- ✅ **Settings menus**: Any option with predefined choices +- ✅ **Game configuration**: Graphics quality, control schemes +- ✅ **Limited options**: 2-10 options work best + +### When NOT to Use ValueStepper +- ❌ **Continuous values**: Use sliders for volume, brightness +- ❌ **Large lists**: Use ItemList or OptionButton for 20+ items +- ❌ **Text input**: Use LineEdit for user-entered text +- ❌ **Numeric input**: Use SpinBox for number entry + +### Performance Considerations +- ValueStepper is lightweight and suitable for multiple instances +- Data loading happens once in `_ready()` +- Visual updates are minimal (just text changes) + +### Accessibility +- Visual highlighting provides clear focus indication +- Audio feedback confirms user actions +- Keyboard and gamepad support for non-mouse users +- Consistent navigation patterns + +## Common Issues and Solutions + +### Stepper Not Responding to Input +- Ensure `handle_input_action()` is called from parent's `_input()` +- Check that the stepper has proper focus/highlighting +- Verify input actions are defined in project input map + +### Values Not Saving +- Override `_apply_value_change()` to handle persistence +- Connect to `value_changed` signal for custom save logic +- Ensure SettingsManager or your data manager is configured + +### Display Names Not Showing +- Check that `display_names` array is properly populated +- Ensure `display_names.size()` matches `values.size()` +- Verify `_update_display()` is called after data loading + +## File Structure + +``` +scenes/ui/components/ +├── ValueStepper.gd # Main component script +└── ValueStepper.tscn # Component scene + +examples/ +├── ValueStepperExample.gd # Usage example script +└── ValueStepperExample.tscn # Example scene + +docs/ +└── UI_COMPONENTS.md # This documentation +``` + +This component provides a solid foundation for any game's settings system and can be easily extended for project-specific needs. \ No newline at end of file diff --git a/examples/LanguageSelectorExample.gd b/examples/LanguageSelectorExample.gd new file mode 100644 index 0000000..41d4a0f --- /dev/null +++ b/examples/LanguageSelectorExample.gd @@ -0,0 +1,25 @@ +# Example of how to use the LanguageSelector component in any scene +extends Control + +@onready var language_selector: LanguageSelector = $LanguageSelector + +func _ready(): + DebugManager.log_info("LanguageSelector example ready", "Example") + + # Connect to language change events + if language_selector.language_changed.is_connected(_on_language_changed): + language_selector.language_changed.disconnect(_on_language_changed) + language_selector.language_changed.connect(_on_language_changed) + +func _on_language_changed(new_language: String): + DebugManager.log_info("Language changed to: " + new_language, "Example") + # Handle language change in your scene + # For example: update UI text, save preferences, etc. + +# Example of integrating with a navigation system +func handle_input_on_language_selector(action: String) -> bool: + return language_selector.handle_input_action(action) + +# Example of highlighting the selector when selected +func set_language_selector_highlighted(highlighted: bool): + language_selector.set_highlighted(highlighted) \ No newline at end of file diff --git a/examples/LanguageSelectorExample.tscn b/examples/LanguageSelectorExample.tscn new file mode 100644 index 0000000..1073b85 --- /dev/null +++ b/examples/LanguageSelectorExample.tscn @@ -0,0 +1,58 @@ +[gd_scene load_steps=3 format=3 uid="uid://bm3x8y4h9l2kv"] + +[ext_resource type="Script" path="res://examples/LanguageSelectorExample.gd" id="1_example"] +[ext_resource type="PackedScene" path="res://scenes/ui/components/LanguageSelector.tscn" id="2_language_selector"] + +[node name="LanguageSelectorExample" 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_example") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -20.0 +offset_top = -20.0 +offset_right = 20.0 +offset_bottom = 20.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Title" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Language Selector Component Example" +horizontal_alignment = 1 + +[node name="HSeparator" type="HSeparator" parent="VBoxContainer"] +layout_mode = 2 + +[node name="LanguageContainer" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="LanguageLabel" type="Label" parent="VBoxContainer/LanguageContainer"] +custom_minimum_size = Vector2(150, 0) +layout_mode = 2 +text = "Language:" + +[node name="LanguageSelector" parent="VBoxContainer/LanguageContainer" instance=ExtResource("2_language_selector")] +layout_mode = 2 + +[node name="HSeparator2" type="HSeparator" parent="VBoxContainer"] +layout_mode = 2 + +[node name="Instructions" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "• Click arrow buttons with mouse +• Use keyboard/gamepad navigation with left/right +• Component is fully reusable in any scene" +horizontal_alignment = 1 + +[connection signal="language_changed" from="VBoxContainer/LanguageContainer/LanguageSelector" to="." method="_on_language_changed"] diff --git a/examples/ValueStepperExample.gd b/examples/ValueStepperExample.gd new file mode 100644 index 0000000..735ddc2 --- /dev/null +++ b/examples/ValueStepperExample.gd @@ -0,0 +1,77 @@ +# Example of how to use the ValueStepper component in any scene +extends Control + +@onready var language_stepper: ValueStepper = $VBoxContainer/Examples/LanguageContainer/LanguageStepper +@onready var difficulty_stepper: ValueStepper = $VBoxContainer/Examples/DifficultyContainer/DifficultyStepper +@onready var resolution_stepper: ValueStepper = $VBoxContainer/Examples/ResolutionContainer/ResolutionStepper +@onready var custom_stepper: ValueStepper = $VBoxContainer/Examples/CustomContainer/CustomStepper + +# Example of setting up custom navigation +var navigable_steppers: Array[ValueStepper] = [] +var current_stepper_index: int = 0 + +func _ready(): + DebugManager.log_info("ValueStepper example ready", "Example") + + # Setup navigation array + navigable_steppers = [language_stepper, difficulty_stepper, resolution_stepper, custom_stepper] + + # Connect to value change events + for stepper in navigable_steppers: + if not stepper.value_changed.is_connected(_on_stepper_value_changed): + stepper.value_changed.connect(_on_stepper_value_changed) + + # Setup custom stepper with custom values + var themes = ["Light", "Dark", "Blue", "Green", "Purple"] + var theme_values = ["light", "dark", "blue", "green", "purple"] + custom_stepper.setup_custom_values(theme_values, themes) + custom_stepper.data_source = "theme" # For better logging + + # Highlight first stepper + _update_stepper_highlighting() + +func _input(event: InputEvent): + # Example navigation handling + if event.is_action_pressed("move_up"): + _navigate_steppers(-1) + get_viewport().set_input_as_handled() + elif event.is_action_pressed("move_down"): + _navigate_steppers(1) + get_viewport().set_input_as_handled() + elif event.is_action_pressed("move_left"): + _handle_stepper_input("move_left") + get_viewport().set_input_as_handled() + elif event.is_action_pressed("move_right"): + _handle_stepper_input("move_right") + get_viewport().set_input_as_handled() + +func _navigate_steppers(direction: int): + current_stepper_index = (current_stepper_index + direction) % navigable_steppers.size() + if current_stepper_index < 0: + current_stepper_index = navigable_steppers.size() - 1 + _update_stepper_highlighting() + DebugManager.log_info("Stepper navigation: index " + str(current_stepper_index), "Example") + +func _handle_stepper_input(action: String): + if current_stepper_index >= 0 and current_stepper_index < navigable_steppers.size(): + var stepper = navigable_steppers[current_stepper_index] + if stepper.handle_input_action(action): + AudioManager.play_ui_click() + +func _update_stepper_highlighting(): + for i in range(navigable_steppers.size()): + navigable_steppers[i].set_highlighted(i == current_stepper_index) + +func _on_stepper_value_changed(new_value: String, new_index: int): + DebugManager.log_info("Stepper value changed to: " + new_value + " (index: " + str(new_index) + ")", "Example") + # Handle value change in your scene + # For example: apply settings, save preferences, update UI, etc. + +# Example of programmatically setting values +func _on_reset_to_defaults_pressed(): + AudioManager.play_ui_click() + language_stepper.set_current_value("en") + difficulty_stepper.set_current_value("normal") + resolution_stepper.set_current_value("1920x1080") + custom_stepper.set_current_value("dark") + DebugManager.log_info("Reset all steppers to defaults", "Example") diff --git a/examples/ValueStepperExample.tscn b/examples/ValueStepperExample.tscn new file mode 100644 index 0000000..4124ec4 --- /dev/null +++ b/examples/ValueStepperExample.tscn @@ -0,0 +1,113 @@ +[gd_scene load_steps=3 format=3 uid="uid://cw03putw85q1o"] + +[ext_resource type="Script" path="res://examples/ValueStepperExample.gd" id="1_example"] +[ext_resource type="PackedScene" path="res://scenes/ui/components/ValueStepper.tscn" id="2_value_stepper"] + +[node name="ValueStepperExample" 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_example") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -200.0 +offset_top = -150.0 +offset_right = 200.0 +offset_bottom = 150.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Title" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "ValueStepper Component Examples" +horizontal_alignment = 1 + +[node name="HSeparator" type="HSeparator" parent="VBoxContainer"] +layout_mode = 2 + +[node name="Examples" type="VBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="LanguageContainer" type="HBoxContainer" parent="VBoxContainer/Examples"] +layout_mode = 2 + +[node name="LanguageLabel" type="Label" parent="VBoxContainer/Examples/LanguageContainer"] +custom_minimum_size = Vector2(120, 0) +layout_mode = 2 +text = "Language:" + +[node name="LanguageStepper" parent="VBoxContainer/Examples/LanguageContainer" instance=ExtResource("2_value_stepper")] +layout_mode = 2 +data_source = "language" + +[node name="DifficultyContainer" type="HBoxContainer" parent="VBoxContainer/Examples"] +layout_mode = 2 + +[node name="DifficultyLabel" type="Label" parent="VBoxContainer/Examples/DifficultyContainer"] +custom_minimum_size = Vector2(120, 0) +layout_mode = 2 +text = "Difficulty:" + +[node name="DifficultyStepper" parent="VBoxContainer/Examples/DifficultyContainer" instance=ExtResource("2_value_stepper")] +layout_mode = 2 +data_source = "difficulty" + +[node name="ResolutionContainer" type="HBoxContainer" parent="VBoxContainer/Examples"] +layout_mode = 2 + +[node name="ResolutionLabel" type="Label" parent="VBoxContainer/Examples/ResolutionContainer"] +custom_minimum_size = Vector2(120, 0) +layout_mode = 2 +text = "Resolution:" + +[node name="ResolutionStepper" parent="VBoxContainer/Examples/ResolutionContainer" instance=ExtResource("2_value_stepper")] +layout_mode = 2 +data_source = "resolution" + +[node name="CustomContainer" type="HBoxContainer" parent="VBoxContainer/Examples"] +layout_mode = 2 + +[node name="CustomLabel" type="Label" parent="VBoxContainer/Examples/CustomContainer"] +custom_minimum_size = Vector2(120, 0) +layout_mode = 2 +text = "Theme:" + +[node name="CustomStepper" parent="VBoxContainer/Examples/CustomContainer" instance=ExtResource("2_value_stepper")] +layout_mode = 2 +data_source = "custom" + +[node name="HSeparator2" type="HSeparator" parent="VBoxContainer"] +layout_mode = 2 + +[node name="Instructions" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Navigation: +• Up/Down arrows - Navigate between steppers +• Left/Right arrows - Change values +• Mouse clicks work on arrow buttons + +Features: +• Multiple data sources (language, difficulty, resolution) +• Custom values support (theme example) +• Gamepad/keyboard navigation +• Visual highlighting +• Signal-based value changes" +horizontal_alignment = 1 + +[node name="HSeparator3" type="HSeparator" parent="VBoxContainer"] +layout_mode = 2 + +[node name="ResetButton" type="Button" parent="VBoxContainer"] +layout_mode = 2 +text = "Reset to Defaults" + +[connection signal="pressed" from="VBoxContainer/ResetButton" to="." method="_on_reset_to_defaults_pressed"] diff --git a/scenes/game/gameplays/tile.gd b/scenes/game/gameplays/tile.gd index a7763c4..f0eeec5 100644 --- a/scenes/game/gameplays/tile.gd +++ b/scenes/game/gameplays/tile.gd @@ -36,7 +36,6 @@ func _set_tile_type(value: int) -> void: if value >= 0 and value < active_gem_types.size(): var texture_index = active_gem_types[value] sprite.texture = all_gem_textures[texture_index] - # print("Debug: Set texture_index ", texture_index, " for tile_type ", value) _scale_sprite_to_fit() else: DebugManager.log_error("Invalid tile type: " + str(value) + ". Available types: 0-" + str(active_gem_types.size() - 1), "Match3") diff --git a/scenes/ui/MainMenu.gd b/scenes/ui/MainMenu.gd index d6a8fa0..29c021b 100644 --- a/scenes/ui/MainMenu.gd +++ b/scenes/ui/MainMenu.gd @@ -2,8 +2,13 @@ extends Control signal open_settings +@onready var menu_buttons: Array[Button] = [] +var current_menu_index: int = 0 +var original_button_scales: Array[Vector2] = [] + func _ready(): DebugManager.log_info("MainMenu ready", "MainMenu") + _setup_menu_navigation() func _on_new_game_button_pressed(): AudioManager.play_ui_click() @@ -19,3 +24,56 @@ func _on_exit_button_pressed(): AudioManager.play_ui_click() DebugManager.log_info("Exit pressed", "MainMenu") get_tree().quit() + +func _setup_menu_navigation(): + menu_buttons.clear() + original_button_scales.clear() + + menu_buttons.append($MenuContainer/NewGameButton) + menu_buttons.append($MenuContainer/SettingsButton) + menu_buttons.append($MenuContainer/ExitButton) + + for button in menu_buttons: + original_button_scales.append(button.scale) + + _update_visual_selection() + +func _input(event: InputEvent) -> void: + if event.is_action_pressed("move_up"): + _navigate_menu(-1) + elif event.is_action_pressed("move_down"): + _navigate_menu(1) + elif event.is_action_pressed("action_south"): + _activate_current_button() + elif event.is_action_pressed("options_menu"): + AudioManager.play_ui_click() + DebugManager.log_info("Options menu shortcut pressed", "MainMenu") + open_settings.emit() + elif event.is_action_pressed("quit_game"): + AudioManager.play_ui_click() + DebugManager.log_info("Quit game shortcut pressed", "MainMenu") + get_tree().quit() + +func _navigate_menu(direction: int): + AudioManager.play_ui_click() + current_menu_index = (current_menu_index + direction) % menu_buttons.size() + if current_menu_index < 0: + current_menu_index = menu_buttons.size() - 1 + _update_visual_selection() + DebugManager.log_info("Menu navigation: index " + str(current_menu_index), "MainMenu") + +func _activate_current_button(): + if current_menu_index >= 0 and current_menu_index < menu_buttons.size(): + var button = menu_buttons[current_menu_index] + DebugManager.log_info("Activating button via keyboard/gamepad: " + button.text, "MainMenu") + button.pressed.emit() + +func _update_visual_selection(): + for i in range(menu_buttons.size()): + var button = menu_buttons[i] + if i == current_menu_index: + button.scale = original_button_scales[i] * 1.1 + button.modulate = Color(1.2, 1.2, 1.0) + else: + button.scale = original_button_scales[i] + button.modulate = Color.WHITE diff --git a/scenes/ui/SettingsMenu.gd b/scenes/ui/SettingsMenu.gd index 46df702..6588e1e 100644 --- a/scenes/ui/SettingsMenu.gd +++ b/scenes/ui/SettingsMenu.gd @@ -5,17 +5,22 @@ signal back_to_main_menu @onready var master_slider = $SettingsContainer/MasterVolumeContainer/MasterVolumeSlider @onready var music_slider = $SettingsContainer/MusicVolumeContainer/MusicVolumeSlider @onready var sfx_slider = $SettingsContainer/SFXVolumeContainer/SFXVolumeSlider -@onready var language_selector = $SettingsContainer/LanguageContainer/LanguageSelector +@onready var language_stepper = $SettingsContainer/LanguageContainer/LanguageStepper @export var settings_manager: Node = SettingsManager @export var localization_manager: Node = LocalizationManager -var language_codes = [] + +# Navigation system variables +var navigable_controls: Array[Control] = [] +var current_control_index: int = 0 +var original_control_scales: Array[Vector2] = [] +var original_control_modulates: Array[Color] = [] func _ready(): add_to_group("localizable") DebugManager.log_info("SettingsMenu ready", "Settings") - setup_language_selector() + # Language selector is initialized automatically var master_callback = _on_volume_slider_changed.bind("master_volume") if not master_slider.value_changed.is_connected(master_callback): @@ -29,21 +34,18 @@ func _ready(): if not sfx_slider.value_changed.is_connected(sfx_callback): sfx_slider.value_changed.connect(sfx_callback) - if not language_selector.item_selected.is_connected(Callable(self, "_on_language_selector_item_selected")): - language_selector.item_selected.connect(_on_language_selector_item_selected) + # Language stepper connections are handled in the .tscn file _update_controls_from_settings() update_text() + _setup_navigation_system() func _update_controls_from_settings(): master_slider.value = settings_manager.get_setting("master_volume") music_slider.value = settings_manager.get_setting("music_volume") sfx_slider.value = settings_manager.get_setting("sfx_volume") - var current_lang = settings_manager.get_setting("language") - var lang_index = language_codes.find(current_lang) - if lang_index >= 0: - language_selector.selected = lang_index + # Language display is handled by the ValueStepper component func _on_volume_slider_changed(value, setting_key): # Input validation for volume settings @@ -73,49 +75,35 @@ func _input(event): DebugManager.log_debug("Cancel/back action pressed in settings", "Settings") _exit_settings() get_viewport().set_input_as_handled() + return + + # Vertical navigation between controls + if event.is_action_pressed("move_up"): + _navigate_controls(-1) + get_viewport().set_input_as_handled() + elif event.is_action_pressed("move_down"): + _navigate_controls(1) + get_viewport().set_input_as_handled() + + # Horizontal navigation for current control + elif event.is_action_pressed("move_left"): + _adjust_current_control(-1) + get_viewport().set_input_as_handled() + elif event.is_action_pressed("move_right"): + _adjust_current_control(1) + get_viewport().set_input_as_handled() + + # Activate current control + elif event.is_action_pressed("action_south"): + _activate_current_control() + get_viewport().set_input_as_handled() func _on_back_button_pressed(): AudioManager.play_ui_click() DebugManager.log_info("Back button pressed", "Settings") _exit_settings() -func setup_language_selector(): - var languages_data = settings_manager.get_languages_data() - if languages_data.has("languages"): - language_selector.clear() - language_codes.clear() - for lang_code in languages_data.languages.keys(): - language_codes.append(lang_code) - var display_name = languages_data.languages[lang_code]["display_name"] - language_selector.add_item(display_name) - - var current_lang = settings_manager.get_setting("language") - var lang_index = language_codes.find(current_lang) - if lang_index >= 0: - language_selector.selected = lang_index - -func _on_language_selector_item_selected(index: int): - # Input validation for language selection - if index < 0: - DebugManager.log_error("Invalid language index (negative): " + str(index), "Settings") - return - - if index >= language_codes.size(): - DebugManager.log_error("Language index out of bounds: %d (max: %d)" % [index, language_codes.size() - 1], "Settings") - return - - var selected_lang = language_codes[index] - if not selected_lang or selected_lang.is_empty(): - DebugManager.log_error("Empty or null language code at index " + str(index), "Settings") - return - - if not settings_manager.set_setting("language", selected_lang): - DebugManager.log_error("Failed to set language setting: " + selected_lang, "Settings") - return - - DebugManager.log_info("Language changed to: " + selected_lang, "Settings") - localization_manager.change_language(selected_lang) func update_text(): $SettingsContainer/SettingsTitle.text = tr("settings_title") @@ -132,3 +120,101 @@ func _on_reset_setting_button_pressed() -> void: settings_manager.reset_settings_to_defaults() _update_controls_from_settings() localization_manager.change_language(settings_manager.get_setting("language")) + +func _setup_navigation_system(): + navigable_controls.clear() + original_control_scales.clear() + original_control_modulates.clear() + + # Add controls in navigation order + navigable_controls.append(master_slider) + navigable_controls.append(music_slider) + navigable_controls.append(sfx_slider) + navigable_controls.append(language_stepper) # Use the ValueStepper component + navigable_controls.append($BackButtonContainer/BackButton) + navigable_controls.append($ResetSettingsContainer/ResetSettingButton) + + # Store original visual properties + for control in navigable_controls: + original_control_scales.append(control.scale) + original_control_modulates.append(control.modulate) + + _update_visual_selection() + +func _navigate_controls(direction: int): + AudioManager.play_ui_click() + current_control_index = (current_control_index + direction) % navigable_controls.size() + if current_control_index < 0: + current_control_index = navigable_controls.size() - 1 + _update_visual_selection() + DebugManager.log_info("Settings navigation: index " + str(current_control_index), "Settings") + +func _adjust_current_control(direction: int): + if current_control_index < 0 or current_control_index >= navigable_controls.size(): + return + + var control = navigable_controls[current_control_index] + + # Handle sliders + if control is HSlider: + var slider = control as HSlider + var step = slider.step if slider.step > 0 else 0.1 + var new_value = slider.value + (direction * step) + new_value = clamp(new_value, slider.min_value, slider.max_value) + slider.value = new_value + AudioManager.play_ui_click() + DebugManager.log_info("Slider adjusted: %s = %f" % [_get_control_name(control), new_value], "Settings") + + # Handle language stepper with left/right + elif control == language_stepper: + if language_stepper.handle_input_action("move_left" if direction == -1 else "move_right"): + AudioManager.play_ui_click() + +func _activate_current_control(): + if current_control_index < 0 or current_control_index >= navigable_controls.size(): + return + + var control = navigable_controls[current_control_index] + + # Handle buttons + if control is Button: + AudioManager.play_ui_click() + DebugManager.log_info("Activating button via keyboard/gamepad: " + control.text, "Settings") + control.pressed.emit() + + # Handle language stepper (no action needed on activation, left/right handles it) + elif control == language_stepper: + DebugManager.log_info("Language stepper selected - use left/right to change", "Settings") + +func _update_visual_selection(): + for i in range(navigable_controls.size()): + var control = navigable_controls[i] + if i == current_control_index: + # Use the ValueStepper's built-in highlighting + if control == language_stepper: + language_stepper.set_highlighted(true) + else: + control.scale = original_control_scales[i] * 1.05 + control.modulate = Color(1.1, 1.1, 0.9) + else: + # Reset highlighting + if control == language_stepper: + language_stepper.set_highlighted(false) + else: + control.scale = original_control_scales[i] + control.modulate = original_control_modulates[i] + +func _get_control_name(control: Control) -> String: + if control == master_slider: + return "master_volume" + elif control == music_slider: + return "music_volume" + elif control == sfx_slider: + return "sfx_volume" + elif control == language_stepper: + return language_stepper.get_control_name() + else: + return "button" + +func _on_language_stepper_value_changed(new_value: String, new_index: int): + DebugManager.log_info("Language changed via ValueStepper: " + new_value + " (index: " + str(new_index) + ")", "Settings") diff --git a/scenes/ui/SettingsMenu.tscn b/scenes/ui/SettingsMenu.tscn index 7adb80a..563c28d 100644 --- a/scenes/ui/SettingsMenu.tscn +++ b/scenes/ui/SettingsMenu.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=3 format=3 uid="uid://57obmcwyos2g"] +[gd_scene load_steps=4 format=3 uid="uid://57obmcwyos2g"] [ext_resource type="Script" uid="uid://bv56qwni68qo" path="res://scenes/ui/SettingsMenu.gd" id="1_oqkcn"] [ext_resource type="PackedScene" path="res://scenes/ui/DebugToggle.tscn" id="2_debug"] +[ext_resource type="PackedScene" path="res://scenes/ui/components/ValueStepper.tscn" id="3_value_stepper"] [node name="SettingsMenu" type="Control" groups=["localizable"]] layout_mode = 3 @@ -85,21 +86,9 @@ custom_minimum_size = Vector2(150, 0) layout_mode = 2 text = "Language" -[node name="LanguageSelector" type="OptionButton" parent="SettingsContainer/LanguageContainer"] +[node name="LanguageStepper" parent="SettingsContainer/LanguageContainer" instance=ExtResource("3_value_stepper")] layout_mode = 2 -size_flags_horizontal = 4 -selected = 0 -item_count = 5 -popup/item_0/text = "English" -popup/item_0/id = 0 -popup/item_1/text = "Español" -popup/item_1/id = 1 -popup/item_2/text = "Français" -popup/item_2/id = 2 -popup/item_3/text = "Deutsch" -popup/item_3/id = 3 -popup/item_4/text = "Русский" -popup/item_4/id = 2 +data_source = "language" [node name="BackButtonContainer" type="Control" parent="."] layout_mode = 1 @@ -148,6 +137,6 @@ layout_mode = 1 [connection signal="value_changed" from="SettingsContainer/MasterVolumeContainer/MasterVolumeSlider" to="." method="_on_master_volume_changed"] [connection signal="value_changed" from="SettingsContainer/MusicVolumeContainer/MusicVolumeSlider" to="." method="_on_music_volume_changed"] [connection signal="value_changed" from="SettingsContainer/SFXVolumeContainer/SFXVolumeSlider" to="." method="_on_sfx_volume_changed"] -[connection signal="item_selected" from="SettingsContainer/LanguageContainer/LanguageSelector" to="." method="_on_language_selector_item_selected"] +[connection signal="value_changed" from="SettingsContainer/LanguageContainer/LanguageStepper" to="." method="_on_language_stepper_value_changed"] [connection signal="pressed" from="BackButtonContainer/BackButton" to="." method="_on_back_button_pressed"] [connection signal="pressed" from="ResetSettingsContainer/ResetSettingButton" to="." method="_on_reset_setting_button_pressed"] diff --git a/scenes/ui/components/LanguageSelector.gd b/scenes/ui/components/LanguageSelector.gd new file mode 100644 index 0000000..0152013 --- /dev/null +++ b/scenes/ui/components/LanguageSelector.gd @@ -0,0 +1,118 @@ +@tool +extends Control +class_name LanguageSelector + +signal language_changed(new_language: String) + +@onready var left_button: Button = $LanguageLeftButton +@onready var right_button: Button = $LanguageRightButton +@onready var language_display: Label = $LanguageDisplay + +@export var settings_manager: Node = SettingsManager +@export var localization_manager: Node = LocalizationManager + +var language_codes: Array[String] = [] +var original_scale: Vector2 +var original_modulate: Color +var is_highlighted: bool = false + +func _ready(): + DebugManager.log_info("LanguageSelector ready", "LanguageSelector") + + # Store original visual properties + original_scale = scale + original_modulate = modulate + + # Connect button signals + if left_button and not left_button.pressed.is_connected(_on_left_button_pressed): + left_button.pressed.connect(_on_left_button_pressed) + + if right_button and not right_button.pressed.is_connected(_on_right_button_pressed): + right_button.pressed.connect(_on_right_button_pressed) + + # Initialize language data + _load_language_data() + _update_language_display() + +func _load_language_data(): + if not settings_manager: + settings_manager = SettingsManager + + var languages_data = settings_manager.get_languages_data() + if languages_data.has("languages"): + language_codes.clear() + for lang_code in languages_data.languages.keys(): + language_codes.append(lang_code) + DebugManager.log_info("Loaded %d languages" % language_codes.size(), "LanguageSelector") + +func _update_language_display(): + if not settings_manager: + return + + var current_lang = settings_manager.get_setting("language") + var languages_data = settings_manager.get_languages_data() + + if languages_data.has("languages") and languages_data.languages.has(current_lang): + language_display.text = languages_data.languages[current_lang]["display_name"] + else: + language_display.text = current_lang if current_lang else "Unknown" + +func change_language(direction: int): + if language_codes.size() == 0: + DebugManager.log_warn("No languages available", "LanguageSelector") + return + + var current_lang = settings_manager.get_setting("language") + var current_index = language_codes.find(current_lang) + if current_index < 0: + current_index = 0 + + var new_index = (current_index + direction) % language_codes.size() + if new_index < 0: + new_index = language_codes.size() - 1 + + var new_lang = language_codes[new_index] + settings_manager.set_setting("language", new_lang) + + if localization_manager: + localization_manager.change_language(new_lang) + else: + LocalizationManager.change_language(new_lang) + + _update_language_display() + language_changed.emit(new_lang) + DebugManager.log_info("Language changed to: " + new_lang, "LanguageSelector") + +func set_highlighted(highlighted: bool): + is_highlighted = highlighted + if highlighted: + scale = original_scale * 1.05 + modulate = Color(1.1, 1.1, 0.9) + else: + scale = original_scale + modulate = original_modulate + +func handle_input_action(action: String) -> bool: + match action: + "move_left": + change_language(-1) + return true + "move_right": + change_language(1) + return true + _: + return false + +func _on_left_button_pressed(): + AudioManager.play_ui_click() + DebugManager.log_info("Language left button clicked", "LanguageSelector") + change_language(-1) + +func _on_right_button_pressed(): + AudioManager.play_ui_click() + DebugManager.log_info("Language right button clicked", "LanguageSelector") + change_language(1) + +# For navigation system integration +func get_control_name() -> String: + return "language_selector" diff --git a/scenes/ui/components/LanguageSelector.tscn b/scenes/ui/components/LanguageSelector.tscn new file mode 100644 index 0000000..848dea3 --- /dev/null +++ b/scenes/ui/components/LanguageSelector.tscn @@ -0,0 +1,26 @@ +[gd_scene load_steps=2 format=3 uid="uid://bc1h4c5t2m6vx"] + +[ext_resource type="Script" path="res://scenes/ui/components/LanguageSelector.gd" id="1_language_selector"] + +[node name="LanguageSelector" type="HBoxContainer"] +layout_mode = 2 +size_flags_vertical = 4 +script = ExtResource("1_language_selector") + +[node name="LanguageLeftButton" type="Button" parent="."] +layout_mode = 2 +text = "<" + +[node name="LanguageDisplay" type="Label" parent="."] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +size_flags_horizontal = 4 +text = "English" +horizontal_alignment = 1 + +[node name="LanguageRightButton" type="Button" parent="."] +layout_mode = 2 +text = ">" + +[connection signal="pressed" from="LanguageLeftButton" to="." method="_on_left_button_pressed"] +[connection signal="pressed" from="LanguageRightButton" to="." method="_on_right_button_pressed"] diff --git a/scenes/ui/components/ValueStepper.gd b/scenes/ui/components/ValueStepper.gd new file mode 100644 index 0000000..82c7f4b --- /dev/null +++ b/scenes/ui/components/ValueStepper.gd @@ -0,0 +1,190 @@ +@tool +extends Control +class_name ValueStepper + +## A reusable UI control for stepping through discrete values with arrow buttons +## +## ValueStepper provides left/right arrow navigation for cycling through predefined options. +## Perfect for settings like language, resolution, difficulty, graphics quality, etc. +## Supports both mouse clicks and keyboard/gamepad navigation. +## +## @tutorial(ValueStepper Usage): See docs/UI_COMPONENTS.md + +signal value_changed(new_value: String, new_index: int) + +@onready var left_button: Button = $LeftButton +@onready var right_button: Button = $RightButton +@onready var value_display: Label = $ValueDisplay + +## The data source for values. Override this for custom implementations. +@export var data_source: String = "language" +## Custom display format function. Leave empty to use default. +@export var custom_format_function: String = "" + +var values: Array[String] = [] +var display_names: Array[String] = [] +var current_index: int = 0 + +var original_scale: Vector2 +var original_modulate: Color +var is_highlighted: bool = false + +func _ready(): + DebugManager.log_info("ValueStepper ready for: " + data_source, "ValueStepper") + + # Store original visual properties + original_scale = scale + original_modulate = modulate + + # Connect button signals + if left_button and not left_button.pressed.is_connected(_on_left_button_pressed): + left_button.pressed.connect(_on_left_button_pressed) + + if right_button and not right_button.pressed.is_connected(_on_right_button_pressed): + right_button.pressed.connect(_on_right_button_pressed) + + # Initialize data + _load_data() + _update_display() + +## Loads data based on the data_source type +func _load_data(): + match data_source: + "language": + _load_language_data() + "resolution": + _load_resolution_data() + "difficulty": + _load_difficulty_data() + _: + DebugManager.log_warn("Unknown data_source: " + data_source, "ValueStepper") + +func _load_language_data(): + var languages_data = SettingsManager.get_languages_data() + if languages_data.has("languages"): + values.clear() + display_names.clear() + for lang_code in languages_data.languages.keys(): + values.append(lang_code) + display_names.append(languages_data.languages[lang_code]["display_name"]) + + # Set current index based on current language + var current_lang = SettingsManager.get_setting("language") + var index = values.find(current_lang) + current_index = max(0, index) + + DebugManager.log_info("Loaded %d languages" % values.size(), "ValueStepper") + +func _load_resolution_data(): + # Example resolution data - customize as needed + values = ["1920x1080", "1366x768", "1280x720", "1024x768"] + display_names = ["1920×1080 (Full HD)", "1366×768", "1280×720 (HD)", "1024×768"] + current_index = 0 + DebugManager.log_info("Loaded %d resolutions" % values.size(), "ValueStepper") + +func _load_difficulty_data(): + # Example difficulty data - customize as needed + values = ["easy", "normal", "hard", "nightmare"] + display_names = ["Easy", "Normal", "Hard", "Nightmare"] + current_index = 1 # Default to "normal" + DebugManager.log_info("Loaded %d difficulty levels" % values.size(), "ValueStepper") + +## Updates the display text based on current selection +func _update_display(): + if values.size() == 0 or current_index < 0 or current_index >= values.size(): + value_display.text = "N/A" + return + + if display_names.size() > current_index: + value_display.text = display_names[current_index] + else: + value_display.text = values[current_index] + +## Changes the current value by the specified direction (-1 for previous, +1 for next) +func change_value(direction: int): + if values.size() == 0: + DebugManager.log_warn("No values available for: " + data_source, "ValueStepper") + return + + var new_index = (current_index + direction) % values.size() + if new_index < 0: + new_index = values.size() - 1 + + current_index = new_index + var new_value = values[current_index] + + _update_display() + _apply_value_change(new_value, current_index) + value_changed.emit(new_value, current_index) + DebugManager.log_info("Value changed to: " + new_value + " (index: " + str(current_index) + ")", "ValueStepper") + +## Override this method for custom value application logic +func _apply_value_change(new_value: String, index: int): + match data_source: + "language": + SettingsManager.set_setting("language", new_value) + if LocalizationManager: + LocalizationManager.change_language(new_value) + "resolution": + # Apply resolution change logic here + DebugManager.log_info("Resolution would change to: " + new_value, "ValueStepper") + "difficulty": + # Apply difficulty change logic here + DebugManager.log_info("Difficulty would change to: " + new_value, "ValueStepper") + +## Sets up custom values for the stepper +func setup_custom_values(custom_values: Array[String], custom_display_names: Array[String] = []): + values = custom_values.duplicate() + display_names = custom_display_names.duplicate() if custom_display_names.size() > 0 else values.duplicate() + current_index = 0 + _update_display() + DebugManager.log_info("Setup custom values: " + str(values.size()) + " items", "ValueStepper") + +## Gets the current value +func get_current_value() -> String: + if values.size() > 0 and current_index >= 0 and current_index < values.size(): + return values[current_index] + return "" + +## Sets the current value by string +func set_current_value(value: String): + var index = values.find(value) + if index >= 0: + current_index = index + _update_display() + +## Visual highlighting for navigation systems +func set_highlighted(highlighted: bool): + is_highlighted = highlighted + if highlighted: + scale = original_scale * 1.05 + modulate = Color(1.1, 1.1, 0.9) + else: + scale = original_scale + modulate = original_modulate + +## Handle input actions for navigation integration +func handle_input_action(action: String) -> bool: + match action: + "move_left": + change_value(-1) + return true + "move_right": + change_value(1) + return true + _: + return false + +func _on_left_button_pressed(): + AudioManager.play_ui_click() + DebugManager.log_info("Left button clicked", "ValueStepper") + change_value(-1) + +func _on_right_button_pressed(): + AudioManager.play_ui_click() + DebugManager.log_info("Right button clicked", "ValueStepper") + change_value(1) + +## For navigation system integration +func get_control_name() -> String: + return data_source + "_stepper" diff --git a/scenes/ui/components/ValueStepper.tscn b/scenes/ui/components/ValueStepper.tscn new file mode 100644 index 0000000..00fef97 --- /dev/null +++ b/scenes/ui/components/ValueStepper.tscn @@ -0,0 +1,26 @@ +[gd_scene load_steps=2 format=3 uid="uid://cb6k05r8t7l4l"] + +[ext_resource type="Script" path="res://scenes/ui/components/ValueStepper.gd" id="1_value_stepper"] + +[node name="ValueStepper" type="HBoxContainer"] +layout_mode = 2 +size_flags_vertical = 4 +script = ExtResource("1_value_stepper") + +[node name="LeftButton" type="Button" parent="."] +layout_mode = 2 +text = "<" + +[node name="ValueDisplay" type="Label" parent="."] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +size_flags_horizontal = 4 +text = "Value" +horizontal_alignment = 1 + +[node name="RightButton" type="Button" parent="."] +layout_mode = 2 +text = ">" + +[connection signal="pressed" from="LeftButton" to="." method="_on_left_button_pressed"] +[connection signal="pressed" from="RightButton" to="." method="_on_right_button_pressed"]