185 lines
5.3 KiB
GDScript
185 lines
5.3 KiB
GDScript
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
|
|
@onready var _advanceIndicator:Label = $VBoxContainer/AdvanceIndicator
|
|
|
|
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()
|
|
_advanceIndicator.visible = _isWaitingForInput
|
|
if _isWaitingForInput:
|
|
_advanceIndicator.modulate.a = 0.5 + 0.5 * sin(Time.get_ticks_msec() / 300.0)
|
|
|
|
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:
|
|
_advanceIndicator.visible = false
|
|
_advanceIndicator.modulate.a = 1.0
|
|
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)
|