Improved UI textbox

This commit is contained in:
2026-06-12 11:56:30 -05:00
parent f6a0bb156e
commit 2f3a4eab66
39 changed files with 574 additions and 706 deletions
+178
View File
@@ -0,0 +1,178 @@
class_name DialogueTextbox extends PanelContainer
const SCENE:PackedScene = preload("res://ui/component/DialogueTextbox.tscn")
enum AdvancementMode { PLAYER, TIMED }
const LINES_PER_PAGE:int = 4
const CHARS_PER_SECOND:float = 20.0
const SPEEDUP_MULTIPLIER:float = 8.0
const PAUSE_COMMA:float = 0.15
const PAUSE_SENTENCE:float = 0.4
const PAUSE_ELLIPSIS_DOT:float = 0.3
const READING_CHARS_PER_SECOND:float = 16.0
const MAX_WIDTH:float = 120.0
signal dismissed
var _entity:Entity = null
var _parsedText:String = ""
var _startLine:int = 0
var _linesPerPage:int = 0
var _revealTimer:float = 0.0
var _pauseTimer:float = 0.0
var _autoAdvanceTimer:float = 0.0
var _isRevealing:bool = false
var _isWaitingForInput:bool = false
var _hasLetGoOfInteract:bool = true
var _advancementMode:AdvancementMode = AdvancementMode.PLAYER
@onready var _speakerLabel:Label = $VBoxContainer/SpeakerLabel
@onready var _bodyLabel:RichTextLabel = $VBoxContainer/BodyLabel
func _ready() -> void:
size.x = MAX_WIDTH
visible = false
var scrollbar = _bodyLabel.get_v_scroll_bar()
if scrollbar:
scrollbar.modulate.a = 0.0
scrollbar.mouse_filter = Control.MOUSE_FILTER_IGNORE
func setup(line:DialogueLine, entity:Entity, mode:AdvancementMode = AdvancementMode.PLAYER) -> void:
_entity = entity
_advancementMode = mode
_speakerLabel.text = entity.displayName if entity else ""
_speakerLabel.visible = _speakerLabel.text != ""
_bodyLabel.text = line.text
_bodyLabel.visible_characters = 0
_bodyLabel.scroll_to_line(0)
_startLine = 0
_linesPerPage = LINES_PER_PAGE
_revealTimer = 0.0
_pauseTimer = 0.0
_autoAdvanceTimer = 0.0
_isWaitingForInput = false
_hasLetGoOfInteract = !Input.is_action_pressed("interact")
size.x = MAX_WIDTH
visible = true
_parsedText = _bodyLabel.get_parsed_text()
_isRevealing = true
func _process(delta:float) -> void:
if not visible:
return
_updateWorldPosition()
if _isRevealing:
_processReveal(delta)
return
if _isWaitingForInput:
_processAdvanceInput()
return
if _advancementMode == AdvancementMode.TIMED:
_processAutoAdvance(delta)
func _processReveal(delta:float) -> void:
if _pauseTimer > 0.0:
var speedMult:float = SPEEDUP_MULTIPLIER if Input.is_action_pressed("interact") else 1.0
_pauseTimer -= delta * speedMult
return
var speedMult:float = SPEEDUP_MULTIPLIER if Input.is_action_pressed("interact") else 1.0
_revealTimer += delta * speedMult
while _revealTimer >= 1.0 / CHARS_PER_SECOND:
_revealTimer -= 1.0 / CHARS_PER_SECOND
if not _revealNextChar():
return
func _revealNextChar() -> bool:
var totalChars:int = len(_parsedText)
if _bodyLabel.visible_characters >= totalChars:
_onRevealComplete()
return false
_bodyLabel.visible_characters += 1
var idx:int = _bodyLabel.visible_characters - 1
# Stop if this character has wrapped onto the next page.
if idx > 0:
var charLine:int = _bodyLabel.get_character_line(idx)
if charLine >= _startLine + _linesPerPage:
_bodyLabel.visible_characters -= 1
_onPageFull()
return false
if idx < len(_parsedText):
_pauseTimer = _getPauseForChar(idx)
return true
func _getPauseForChar(idx:int) -> float:
var ch:String = _parsedText[idx]
if ch == "," or ch == ";":
return PAUSE_COMMA
if ch == "." or ch == "!" or ch == "?":
if ch == ".":
var prevDot:bool = idx > 0 and _parsedText[idx - 1] == "."
var nextDot:bool = idx < len(_parsedText) - 1 and _parsedText[idx + 1] == "."
if prevDot or nextDot:
return PAUSE_ELLIPSIS_DOT
return PAUSE_SENTENCE
return 0.0
func _onPageFull() -> void:
_isRevealing = false
_isWaitingForInput = true
func _onRevealComplete() -> void:
_isRevealing = false
if _advancementMode == AdvancementMode.TIMED:
_autoAdvanceTimer = len(_parsedText) / READING_CHARS_PER_SECOND
else:
_isWaitingForInput = true
func _processAdvanceInput() -> void:
if Input.is_action_just_released("interact"):
_hasLetGoOfInteract = true
if not _hasLetGoOfInteract:
return
if Input.is_action_just_pressed("interact"):
_advance()
func _processAutoAdvance(delta:float) -> void:
_autoAdvanceTimer -= delta
if _autoAdvanceTimer <= 0.0:
_advance()
func _advance() -> void:
var hasMorePages:bool = _startLine + _linesPerPage < _bodyLabel.get_line_count()
if hasMorePages:
_startLine += _linesPerPage
_bodyLabel.scroll_to_line(_startLine)
_isWaitingForInput = false
_revealTimer = 0.0
_pauseTimer = 0.0
_isRevealing = true
else:
dismissed.emit()
visible = false
queue_free()
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)