class_name ChoiceModal extends CanvasLayer ## ChoiceModal - UI component for multi-modal ability choices ## Displays options like "Select 1 of 3 following actions" and returns selection signal choice_made(selected_indices: Array) signal choice_cancelled # UI Elements var backdrop: ColorRect var modal_panel: Panel var title_label: Label var options_container: VBoxContainer var confirm_button: Button var cancel_button: Button # State var _modes: Array = [] var _select_count: int = 1 var _select_up_to: bool = false var _selected_indices: Array = [] var _cancellable: bool = false var _option_buttons: Array = [] # Cached styles for option buttons (created once, reused) var _option_normal_style: StyleBoxFlat var _option_selected_style: StyleBoxFlat func _ready() -> void: layer = 200 # High z-index for modal overlay _create_cached_styles() _create_ui() visible = false func _create_cached_styles() -> void: # Normal button style _option_normal_style = StyleBoxFlat.new() _option_normal_style.bg_color = Color(0.15, 0.15, 0.2, 0.9) _option_normal_style.set_border_width_all(1) _option_normal_style.border_color = Color(0.3, 0.3, 0.4) _option_normal_style.set_corner_radius_all(4) _option_normal_style.set_content_margin_all(10) # Selected button style (gold highlight) _option_selected_style = StyleBoxFlat.new() _option_selected_style.bg_color = Color(0.25, 0.22, 0.15, 0.95) _option_selected_style.border_color = Color(0.7, 0.55, 0.2) _option_selected_style.set_border_width_all(2) _option_selected_style.set_corner_radius_all(4) _option_selected_style.set_content_margin_all(10) func _create_ui() -> void: # Backdrop - semi-transparent dark overlay backdrop = ColorRect.new() add_child(backdrop) backdrop.color = Color(0, 0, 0, 0.7) backdrop.set_anchors_preset(Control.PRESET_FULL_RECT) backdrop.mouse_filter = Control.MOUSE_FILTER_STOP backdrop.gui_input.connect(_on_backdrop_input) # Center container for modal var center = CenterContainer.new() add_child(center) center.set_anchors_preset(Control.PRESET_FULL_RECT) center.mouse_filter = Control.MOUSE_FILTER_IGNORE # Modal panel modal_panel = Panel.new() center.add_child(modal_panel) modal_panel.custom_minimum_size = Vector2(500, 200) var style = StyleBoxFlat.new() style.bg_color = Color(0.08, 0.08, 0.12, 0.98) style.border_color = Color(0.5, 0.4, 0.2) # Gold border style.set_border_width_all(2) style.set_corner_radius_all(8) style.set_content_margin_all(20) modal_panel.add_theme_stylebox_override("panel", style) # Main vertical layout var vbox = VBoxContainer.new() modal_panel.add_child(vbox) vbox.set_anchors_preset(Control.PRESET_FULL_RECT) vbox.offset_left = 20 vbox.offset_right = -20 vbox.offset_top = 20 vbox.offset_bottom = -20 vbox.add_theme_constant_override("separation", 15) # Title title_label = Label.new() vbox.add_child(title_label) title_label.text = "Select an action:" title_label.add_theme_font_size_override("font_size", 20) title_label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7)) title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER # Options container options_container = VBoxContainer.new() vbox.add_child(options_container) options_container.add_theme_constant_override("separation", 8) options_container.size_flags_vertical = Control.SIZE_EXPAND_FILL # Button row var button_row = HBoxContainer.new() vbox.add_child(button_row) button_row.add_theme_constant_override("separation", 15) button_row.alignment = BoxContainer.ALIGNMENT_CENTER # Confirm button (for multi-select) confirm_button = _create_button("Confirm", Color(0.2, 0.5, 0.3)) button_row.add_child(confirm_button) confirm_button.pressed.connect(_on_confirm_pressed) confirm_button.visible = false # Only shown for multi-select # Cancel button cancel_button = _create_button("Cancel", Color(0.5, 0.3, 0.3)) button_row.add_child(cancel_button) cancel_button.pressed.connect(_on_cancel_pressed) cancel_button.visible = false # Only shown if cancellable func _create_button(text: String, base_color: Color) -> Button: var button = Button.new() button.text = text button.custom_minimum_size = Vector2(100, 40) var normal_style = StyleBoxFlat.new() normal_style.bg_color = base_color normal_style.set_border_width_all(1) normal_style.border_color = base_color.lightened(0.3) normal_style.set_corner_radius_all(4) normal_style.set_content_margin_all(8) button.add_theme_stylebox_override("normal", normal_style) var hover_style = normal_style.duplicate() hover_style.bg_color = base_color.lightened(0.15) button.add_theme_stylebox_override("hover", hover_style) var pressed_style = normal_style.duplicate() pressed_style.bg_color = base_color.darkened(0.1) button.add_theme_stylebox_override("pressed", pressed_style) return button func _create_option_button(index: int, description: String) -> Button: var button = Button.new() button.text = str(index + 1) + ". " + description button.custom_minimum_size = Vector2(460, 50) button.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART button.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING # Normal state var normal_style = StyleBoxFlat.new() normal_style.bg_color = Color(0.15, 0.15, 0.2, 0.9) normal_style.set_border_width_all(1) normal_style.border_color = Color(0.3, 0.3, 0.4) normal_style.set_corner_radius_all(4) normal_style.set_content_margin_all(10) button.add_theme_stylebox_override("normal", normal_style) # Hover state var hover_style = normal_style.duplicate() hover_style.bg_color = Color(0.2, 0.2, 0.3, 0.95) hover_style.border_color = Color(0.5, 0.4, 0.2) button.add_theme_stylebox_override("hover", hover_style) # Pressed/selected state (gold highlight) var pressed_style = normal_style.duplicate() pressed_style.bg_color = Color(0.25, 0.22, 0.15, 0.95) pressed_style.border_color = Color(0.7, 0.55, 0.2) pressed_style.set_border_width_all(2) button.add_theme_stylebox_override("pressed", pressed_style) # Font settings button.add_theme_font_size_override("font_size", 14) button.add_theme_color_override("font_color", Color(0.85, 0.85, 0.85)) button.add_theme_color_override("font_hover_color", Color(1, 0.95, 0.8)) # Connect press signal button.pressed.connect(_on_option_pressed.bind(index)) return button ## Show modal and await selection ## Returns array of selected mode indices func show_choices( title: String, modes: Array, select_count: int = 1, select_up_to: bool = false, cancellable: bool = false ) -> Array: _modes = modes _select_count = select_count _select_up_to = select_up_to _cancellable = cancellable _selected_indices = [] _option_buttons = [] # Update title if select_up_to: title_label.text = "Select up to %d action%s:" % [select_count, "s" if select_count > 1 else ""] else: title_label.text = "Select %d action%s:" % [select_count, "s" if select_count > 1 else ""] # Clear and populate options for child in options_container.get_children(): child.queue_free() await get_tree().process_frame # Wait for queue_free for i in range(modes.size()): var mode = modes[i] var description = mode.get("description", "Option " + str(i + 1)) var button = _create_option_button(i, description) options_container.add_child(button) _option_buttons.append(button) # Show confirm button only for multi-select confirm_button.visible = (select_count > 1 or select_up_to) _update_confirm_button() # Show cancel if cancellable cancel_button.visible = cancellable # Resize panel to fit content await get_tree().process_frame var content_height = 20 + 30 + 15 + (modes.size() * 58) + 15 + 50 + 20 modal_panel.custom_minimum_size = Vector2(500, min(content_height, 600)) visible = true # Wait for selection var result = await _wait_for_selection() visible = false return result ## Internal: Wait for user selection using a callback pattern func _wait_for_selection() -> Array: var result: Array = [] # Create a one-shot signal connection var completed = false var on_choice = func(indices: Array): result = indices completed = true var on_cancel = func(): result = [] completed = true choice_made.connect(on_choice, CONNECT_ONE_SHOT) choice_cancelled.connect(on_cancel, CONNECT_ONE_SHOT) # Wait until completed while not completed: await get_tree().process_frame return result func _on_option_pressed(index: int) -> void: if _select_count == 1 and not _select_up_to: # Single select - immediately return choice_made.emit([index]) return # Multi-select - toggle selection if index in _selected_indices: _selected_indices.erase(index) else: if _selected_indices.size() < _select_count: _selected_indices.append(index) _update_option_visuals() _update_confirm_button() func _update_option_visuals() -> void: for i in range(_option_buttons.size()): var button = _option_buttons[i] as Button var is_selected = i in _selected_indices # Use cached styles instead of creating new ones each time if is_selected: button.add_theme_stylebox_override("normal", _option_selected_style) else: button.add_theme_stylebox_override("normal", _option_normal_style) func _update_confirm_button() -> void: if _select_up_to: confirm_button.disabled = false confirm_button.text = "Confirm (%d)" % _selected_indices.size() else: confirm_button.disabled = _selected_indices.size() != _select_count confirm_button.text = "Confirm (%d/%d)" % [_selected_indices.size(), _select_count] func _on_confirm_pressed() -> void: if _select_up_to or _selected_indices.size() == _select_count: choice_made.emit(_selected_indices.duplicate()) func _on_cancel_pressed() -> void: choice_cancelled.emit() func _on_backdrop_input(event: InputEvent) -> void: if event is InputEventMouseButton and event.pressed: if _cancellable: choice_cancelled.emit() func _input(event: InputEvent) -> void: if not visible: return # Keyboard shortcuts if event is InputEventKey and event.pressed: # Number keys 1-9 for quick selection if event.keycode >= KEY_1 and event.keycode <= KEY_9: var index = event.keycode - KEY_1 if index < _modes.size(): _on_option_pressed(index) get_viewport().set_input_as_handled() # Enter to confirm (multi-select only) elif event.keycode == KEY_ENTER or event.keycode == KEY_KP_ENTER: if confirm_button.visible and not confirm_button.disabled: _on_confirm_pressed() get_viewport().set_input_as_handled() # Escape to cancel elif event.keycode == KEY_ESCAPE: if _cancellable: choice_cancelled.emit() get_viewport().set_input_as_handled()