Untitled
Anonymous
plain_text
02/24/2026 2:39 AM
6.7 KB
9
Indexable
extends Control
signal choice_selected(choice_index: int)
@onready var box: Panel = $Box as Panel
@onready var portrait_rect: TextureRect = $Box/BoxCol/ContentRow/PortraitContainer/Portrait as TextureRect
@onready var name_label: Label = $Box/BoxCol/ContentRow/TextCol/NameLabel as Label
@onready var body_text: RichTextLabel = $Box/BoxCol/ContentRow/TextCol/BodyText as RichTextLabel
@onready var choices_col: VBoxContainer = $Box/BoxCol/ContentRow/TextCol/Choices as VBoxContainer
@onready var hint_label: Label = $Box/BoxCol/HintLabel as Label
var _is_open: bool = false
var _is_typing: bool = false
var _full_text: String = ""
var _char_index: int = 0
var _type_timer: float = 0.0
var _type_speed: float = 0.03
var _has_choices: bool = false
# --- New: deferred start to avoid first-open scroll bug ---
var _pending_start: bool = false
var _layout_warmup_frames: int = 0
# Scroll helpers
var _scroll_requested: bool = false
func _ready() -> void:
if box == null or name_label == null or body_text == null or choices_col == null or hint_label == null or portrait_rect == null:
push_error("DialogueUI: One or more UI node paths are wrong. Check DialogueUI tree.")
hide_dialogue()
return
body_text.scroll_active = true
body_text.scroll_following = true
hide_dialogue()
func show_dialogue(speaker_name: String, text: String, portrait_tex: Texture2D = null, choices: Array = []) -> void:
_is_open = true
visible = true
box.visible = true
name_label.text = speaker_name
# Portrait
if portrait_tex != null:
portrait_rect.texture = portrait_tex
portrait_rect.visible = true
else:
portrait_rect.texture = null
portrait_rect.visible = false
# Save the text, but DO NOT start typing yet (layout not ready on first open)
_full_text = text
_char_index = 0
_type_timer = 0.0
_is_typing = true
body_text.text = "" # clear immediately so it feels responsive
# Choices
_clear_choices()
_has_choices = (choices.size() > 0)
if _has_choices:
_build_choices(choices)
# Hint
hint_label.visible = not _has_choices
hint_label.text = "E: continue" if not _has_choices else ""
# Ensure scrolling flags are on each time
body_text.scroll_active = true
body_text.scroll_following = true
# Critical: warm up layout for a couple frames, then start typewriter.
_pending_start = true
_layout_warmup_frames = 2
_request_scroll_to_bottom()
func hide_dialogue() -> void:
_is_open = false
_is_typing = false
_pending_start = false
_layout_warmup_frames = 0
_full_text = ""
_char_index = 0
_type_timer = 0.0
visible = false
func is_open() -> bool:
return _is_open
func is_typing() -> bool:
return _is_typing
func fast_reveal() -> void:
if not _is_typing:
return
_is_typing = false
body_text.text = _full_text
_request_scroll_to_bottom()
func _process(delta: float) -> void:
if not _is_open:
return
# Warm-up frames: allow containers to compute size/line wrapping.
# This is what fixes the "first dialogue doesn't scroll" problem.
if _pending_start:
_layout_warmup_frames -= 1
_request_scroll_to_bottom()
_flush_scroll_now()
if _layout_warmup_frames <= 0:
_pending_start = false
return
# Typewriter
if _is_typing:
var char_speed: float = _type_speed
if Input.is_action_pressed("fast_text"):
char_speed = 0.005
_type_timer += delta
while _type_timer >= char_speed and _char_index < _full_text.length():
_type_timer -= char_speed
_char_index += 1
body_text.text = _full_text.substr(0, _char_index)
_request_scroll_to_bottom()
if _char_index >= _full_text.length():
_is_typing = false
_request_scroll_to_bottom()
_flush_scroll_now()
func _unhandled_input(event: InputEvent) -> void:
if not _is_open:
return
if event.is_action_pressed("interact"):
if _is_typing:
fast_reveal()
return
# If there are no choices, owner handles E.
# We intentionally do nothing here.
func _clear_choices() -> void:
for c: Node in choices_col.get_children():
c.queue_free()
func _build_choices(choices: Array) -> void:
for i in range(choices.size()):
var label_text: String = ""
# Support BOTH formats:
# 1) [{ "label": "..." }, ...] (Aurora style)
# 2) ["...", "..."] (older scenes / quick ones)
if choices[i] is Dictionary:
label_text = str((choices[i] as Dictionary).get("label", ""))
else:
label_text = str(choices[i])
var b: Button = Button.new()
b.text = label_text
b.focus_mode = Control.FOCUS_ALL
# Capture stable index (avoids closure weirdness)
var idx: int = i
b.pressed.connect(func() -> void:
emit_signal("choice_selected", idx)
)
choices_col.add_child(b)
if choices_col.get_child_count() > 0:
var first: Node = choices_col.get_child(0)
if first is Control:
(first as Control).grab_focus()
# --- Scroll helpers ---
func _request_scroll_to_bottom() -> void:
_scroll_requested = true
func _flush_scroll_now() -> void:
if not _scroll_requested:
return
_scroll_requested = false
if body_text == null:
return
var max_line: int = max(body_text.get_line_count() - 1, 0)
body_text.scroll_to_line(max_line)Editor is loading...
Leave a Comment