156 lines
4.5 KiB
GDScript
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
|