Improved UI textbox
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user