Files
Dawn-Godot/ui/component/VNTextbox.gd
T
2026-06-12 09:16:19 -05:00

156 lines
4.5 KiB
GDScript

class_name ChatBox extends PanelContainer
const VN_REVEAL_TIME = 0.01
const _DEFAULT_ANCHOR_TOP = 1.0
const _DEFAULT_ANCHOR_RIGHT = 1.0
const _DEFAULT_ANCHOR_BOTTOM = 1.0
const _DEFAULT_OFFSET_TOP = -58.0
var label:AdvancedRichText;
var parsedOutText = ""
var revealTimer:float = 0;
var lineStarts:Array[int] = [];
var newlineIndexes:Array[int] = [];
var currentViewScrolled = true;
var isSpeedupDown = false;
var hasLetGoOfInteract:bool = true;
var worldTarget:Node3D = null
var isClosed:bool = false:
get():
return !self.visible;
set(value):
self.visible = !value;
signal chatboxClosing
func _ready() -> void:
label = $MarginContainer/Label
isClosed = true
func _setWorldLayout(hasTarget:bool) -> void:
if hasTarget:
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
offset_left = 0.0
offset_top = 0.0
offset_right = 0.0
offset_bottom = 0.0
custom_minimum_size = Vector2(90, 0)
else:
if label != null:
label.fit_content = false
anchor_left = 0.0
anchor_top = _DEFAULT_ANCHOR_TOP
anchor_right = _DEFAULT_ANCHOR_RIGHT
anchor_bottom = _DEFAULT_ANCHOR_BOTTOM
offset_left = 0.0
offset_top = _DEFAULT_OFFSET_TOP
offset_right = 0.0
offset_bottom = 0.0
custom_minimum_size = Vector2.ZERO
func _updateWorldPosition() -> void:
if worldTarget == null:
return
var camera:Camera3D = get_viewport().get_camera_3d()
if camera == null:
return
var screenPos:Vector2 = camera.unproject_position(worldTarget.global_position + Vector3(0, 2.5, 0))
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)
func _process(delta: float) -> void:
if isClosed:
return
_updateWorldPosition()
# _recalcText always resets fit_content; keep it on in world-space mode so
# the PanelContainer auto-sizes to the full speech-bubble content.
if worldTarget != null and !label.fit_content:
label.fit_content = true
if label.getFinalText() == "":
isClosed = true
return
if Input.is_action_just_released("interact") and !hasLetGoOfInteract:
hasLetGoOfInteract = true
return
# World-space (speech-bubble) mode: all text is shown immediately with no
# typing reveal. One press advances to the next page; release closes the last.
if worldTarget != null:
if (label.maxLines + label.startLine) < label.getTotalLineCount():
if Input.is_action_just_pressed("interact") and hasLetGoOfInteract:
label.startLine += label.maxLines
label.fit_content = true
else:
if Input.is_action_just_released("interact") and hasLetGoOfInteract:
chatboxClosing.emit()
isClosed = true
return
# Bottom-bar mode: typing reveal with paging.
if label.visible_characters >= label.getCharactersDisplayedCount():
if (label.maxLines + label.startLine) < label.getTotalLineCount():
if Input.is_action_just_pressed("interact") and hasLetGoOfInteract:
label.startLine += label.maxLines
label.visible_characters = 0
currentViewScrolled = false
return
currentViewScrolled = true
else:
if Input.is_action_just_released("interact") and hasLetGoOfInteract:
chatboxClosing.emit()
isClosed = true
currentViewScrolled = true
return
if Input.is_action_just_pressed("interact") and hasLetGoOfInteract:
isSpeedupDown = true
elif Input.is_action_just_released("interact") and hasLetGoOfInteract:
isSpeedupDown = false
elif !Input.is_action_pressed("interact") and hasLetGoOfInteract:
isSpeedupDown = false
revealTimer += delta
if isSpeedupDown:
revealTimer += delta
if revealTimer > VN_REVEAL_TIME:
revealTimer = 0
label.visible_characters += 1
func setText(text:String, target:Node3D = null) -> void:
worldTarget = target
_setWorldLayout(worldTarget != null)
revealTimer = 0
currentViewScrolled = false
label.startLine = 0
label.text = text
if worldTarget != null:
# Speech-bubble mode: show all text immediately and auto-size the container.
# _recalcText already sets visible_characters = -1; just enable fit_content.
label.fit_content = true
else:
label.visible_characters = 0
hasLetGoOfInteract = !Input.is_action_pressed("interact")
isSpeedupDown = false
isClosed = false
func setTextAndWait(text:String, target:Node3D = null) -> void:
self.setText(text, target);
await self.chatboxClosing
await get_tree().process_frame