feature updates

This commit is contained in:
2026-02-02 16:28:53 -05:00
parent bf9aa3fa23
commit 44c06530ac
83 changed files with 282641 additions and 11251 deletions

347
scripts/ui/ChoiceModal.gd Normal file
View File

@@ -0,0 +1,347 @@
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()