class_name DialogueChoiceBox extends PanelContainer const SCENE:PackedScene = preload("res://ui/component/DialogueChoiceBox.tscn") const MAX_WIDTH:float = 120.0 signal chosen(response:DialogueResponse) var _responses:Array[DialogueResponse] = [] var _entity:Entity = null var _selectedIndex:int = 0 var _hasLetGoOfInteract:bool = true @onready var _list:VBoxContainer = $VBoxContainer/List func _ready() -> void: size.x = MAX_WIDTH visible = false func setup(responses:Array[DialogueResponse], entity:Entity) -> void: _entity = entity _responses = responses.filter(func(r): return r.is_allowed) _selectedIndex = 0 _hasLetGoOfInteract = !Input.is_action_pressed("interact") for child in _list.get_children(): child.queue_free() var innerWidth:float = _labelWidth() var sep:int = _list.get_theme_constant("separation") var totalHeight:float = 0.0 for i in range(_responses.size()): var label:Label = Label.new() label.text = _responses[i].text label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART label.focus_mode = Control.FOCUS_ALL _list.add_child(label) label.size.x = innerWidth totalHeight += label.get_minimum_size().y if i > 0: totalHeight += sep var idx:int = i label.focus_entered.connect(func(): _onFocused(idx)) # Prevent focus escaping the list at the edges var count:int = _list.get_child_count() for i in range(count): var label:Label = _list.get_child(i) label.focus_neighbor_top = _list.get_child(max(0, i - 1)).get_path() label.focus_neighbor_bottom = _list.get_child(min(count - 1, i + 1)).get_path() var style:StyleBox = get_theme_stylebox("panel") if style: totalHeight += style.get_margin(SIDE_TOP) + style.get_margin(SIDE_BOTTOM) size = Vector2(MAX_WIDTH, totalHeight) _updateWorldPosition() visible = true _updateSelection() _list.get_child(0).grab_focus() func _labelWidth() -> float: var style:StyleBox = get_theme_stylebox("panel") if style == null: return MAX_WIDTH return MAX_WIDTH - style.get_margin(SIDE_LEFT) - style.get_margin(SIDE_RIGHT) func _process(_delta:float) -> void: if not visible: return _updateWorldPosition() func _input(event:InputEvent) -> void: if not visible: return if event.is_action_released("interact"): _hasLetGoOfInteract = true return if event.is_action_pressed("interact") and _hasLetGoOfInteract: _confirm() get_viewport().set_input_as_handled() func _onFocused(idx:int) -> void: _selectedIndex = idx _updateSelection() func _confirm() -> void: if not visible: return visible = false chosen.emit(_responses[_selectedIndex]) queue_free() func _updateSelection() -> void: var children:Array = _list.get_children() for i in children.size(): var label:Label = children[i] if i == _selectedIndex: label.text = "▶ " + _responses[i].text label.add_theme_color_override("font_color", Color.YELLOW) else: label.text = _responses[i].text label.remove_theme_color_override("font_color") func _updateWorldPosition() -> void: if _entity == null: return var camera:Camera3D = get_viewport().get_camera_3d() if camera == null: return var worldPos:Vector3 = _entity.global_position + Vector3(0, 2.5, 0) var screenPos:Vector2 = camera.unproject_position(worldPos) var viewportSize:Vector2 = get_viewport().get_visible_rect().size position = screenPos - size * 0.5 position.x = clamp(position.x, 0.0, viewportSize.x - size.x) position.y = clamp(position.y, 0.0, viewportSize.y - size.y)