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
-1
View File
@@ -1,7 +1,6 @@
class_name RootUI extends Control
@export var debugMenu:DebugMenu
@export var mainChatBox:ChatBox
@export var gameMenu:GameMenu
@export var pauseMenu:PauseMenu
@export var chatBoxContainer:Control
+4 -10
View File
@@ -1,12 +1,11 @@
[gd_scene load_steps=6 format=3 uid="uid://baos0arpiskbp"]
[gd_scene load_steps=5 format=3 uid="uid://baos0arpiskbp"]
[ext_resource type="PackedScene" uid="uid://bkx3l0kckf4a8" path="res://ui/component/VNTextbox.tscn" id="1_1mtk3"]
[ext_resource type="Script" uid="uid://dq3qyyayugt5l" path="res://ui/RootUI.gd" id="1_son71"]
[ext_resource type="PackedScene" uid="uid://c0i5e2dj11d8c" path="res://ui/pause/PauseMenu.tscn" id="2_atyu8"]
[ext_resource type="PackedScene" uid="uid://b38dr0wkix76t" path="res://ui/debugmenu/DebugMenu.tscn" id="4_u132g"]
[ext_resource type="PackedScene" uid="uid://bv5r2x9m4k7n1" path="res://ui/gamemenu/GameMenu.tscn" id="5_gmenu"]
[node name="RootUI" type="Control" node_paths=PackedStringArray("debugMenu", "mainChatBox", "gameMenu", "pauseMenu", "chatBoxContainer")]
[node name="RootUI" type="Control" node_paths=PackedStringArray("debugMenu", "gameMenu", "pauseMenu", "chatBoxContainer")]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
@@ -16,10 +15,9 @@ grow_vertical = 2
mouse_filter = 2
script = ExtResource("1_son71")
debugMenu = NodePath("DebugMenu")
mainChatBox = NodePath("ChatBox")
gameMenu = NodePath("GameMenu")
pauseMenu = NodePath("PauseMenu")
chatBoxContainer = NodePath("WorldChatBoxContainer")
chatBoxContainer = NodePath("ChatBoxContainer")
metadata/_custom_type_script = "uid://dq3qyyayugt5l"
[node name="DebugMenu" parent="." instance=ExtResource("4_u132g")]
@@ -35,7 +33,7 @@ process_mode = 3
visible = false
layout_mode = 1
[node name="WorldChatBoxContainer" type="Control" parent="."]
[node name="ChatBoxContainer" type="Control" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
@@ -43,7 +41,3 @@ anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
[node name="ChatBox" parent="." instance=ExtResource("1_1mtk3")]
visible = false
layout_mode = 1
+26 -34
View File
@@ -2,51 +2,43 @@ extends Node
var rootUi:RootUI = null
# True for the entire duration of a DialogueAction run, including the frames
# between lines where the textbox is momentarily closed.
# True whenever any dialogue resource is being processed by DialogueManager.
# Driven by DialogueManager.dialogue_started / dialogue_ended signals.
var dialogueActive:bool = false
var _chatBoxes:Array = []
# True only during a CONVERSATION-mode sequence. Blocks player movement.
var activeConversation:bool = false
var DEBUG_MENU:
func _ready() -> void:
DialogueManager.dialogue_started.connect(_onDialogueStarted)
DialogueManager.dialogue_ended.connect(_onDialogueEnded)
func _onDialogueStarted(_resource:DialogueResource) -> void:
dialogueActive = true
func _onDialogueEnded(_resource:DialogueResource) -> void:
dialogueActive = false
var chatBoxContainer:Control:
get():
if rootUi && rootUi.debugMenu:
if rootUi:
return rootUi.chatBoxContainer
return null
var DEBUG_MENU:DebugMenu:
get():
if rootUi:
return rootUi.debugMenu
return null
var MAIN_CHATBOX:
var GAME_MENU:GameMenu:
get():
if rootUi && rootUi.mainChatBox:
return rootUi.mainChatBox
return null
var GAME_MENU:
get():
if rootUi && rootUi.gameMenu:
if rootUi:
return rootUi.gameMenu
return null
var PAUSE_MENU:
var PAUSE_MENU:PauseMenu:
get():
if rootUi && rootUi.pauseMenu:
if rootUi:
return rootUi.pauseMenu
return null
func addChatBox(box:WorldChatBox) -> void:
_chatBoxes.append(box)
func removeChatBox(box:WorldChatBox) -> void:
_chatBoxes.erase(box)
func hasAdvanceableChatBox() -> bool:
for box in _chatBoxes:
if box.mode == WorldChatBox.Mode.ADVANCEABLE:
return true
return false
func spawnWorldChatBox(target:Node3D = null) -> WorldChatBox:
assert(rootUi != null)
var chatbox:WorldChatBox = WorldChatBox.SCENE.instantiate()
rootUi.chatBoxContainer.add_child(chatbox)
chatbox.worldTarget = target
return chatbox
+87
View File
@@ -0,0 +1,87 @@
class_name DialogueChoiceBox extends PanelContainer
const SCENE:PackedScene = preload("res://ui/component/DialogueChoiceBox.tscn")
const MAX_WIDTH:float = 120.0
signal chosen(response:DialogueResponse)
var _responses:Array[DialogueResponse] = []
var _entity:Entity = null
var _selectedIndex:int = 0
var _hasLetGoOfInteract:bool = true
@onready var _list:VBoxContainer = $VBoxContainer/List
func _ready() -> void:
size.x = MAX_WIDTH
visible = false
func setup(responses:Array[DialogueResponse], entity:Entity) -> void:
_entity = entity
_responses = responses.filter(func(r): return r.is_allowed)
_selectedIndex = 0
_hasLetGoOfInteract = !Input.is_action_pressed("interact")
for child in _list.get_children():
child.queue_free()
for response in _responses:
var label:Label = Label.new()
label.text = response.text
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_list.add_child(label)
size.x = MAX_WIDTH
visible = true
_updateSelection()
func _process(_delta:float) -> void:
if not visible:
return
_updateWorldPosition()
_processInput()
func _processInput() -> void:
if Input.is_action_just_released("interact"):
_hasLetGoOfInteract = true
if Input.is_action_just_pressed("ui_up") or Input.is_action_just_pressed("move_forward"):
_selectedIndex = max(0, _selectedIndex - 1)
_updateSelection()
if Input.is_action_just_pressed("ui_down") or Input.is_action_just_pressed("move_back"):
_selectedIndex = min(_responses.size() - 1, _selectedIndex + 1)
_updateSelection()
if _hasLetGoOfInteract and Input.is_action_just_pressed("interact"):
_confirm()
func _confirm() -> void:
if _responses.is_empty():
return
var response:DialogueResponse = _responses[_selectedIndex]
chosen.emit(response)
visible = false
queue_free()
func _updateSelection() -> void:
var children:Array = _list.get_children()
for i in children.size():
var label:Label = children[i]
if i == _selectedIndex:
label.add_theme_color_override("font_color", Color.YELLOW)
else:
label.remove_theme_color_override("font_color")
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 - Vector2(size.x * 0.5, size.y)
position.x = clamp(position.x, 0.0, viewportSize.x - size.x)
position.y = clamp(position.y, 0.0, viewportSize.y - size.y)
+1
View File
@@ -0,0 +1 @@
uid://cbgsqs5t10l06
+18
View File
@@ -0,0 +1,18 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Theme" path="res://ui/UI Theme.tres" id="1"]
[ext_resource type="Script" path="res://ui/component/DialogueChoiceBox.gd" id="2"]
[node name="DialogueChoiceBox" type="PanelContainer"]
custom_minimum_size = Vector2(120, 0)
mouse_filter = 2
theme = ExtResource("1")
script = ExtResource("2")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2
theme_override_constants/separation = 1
[node name="List" type="VBoxContainer" parent="VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 2
+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)
+1
View File
@@ -0,0 +1 @@
uid://drivjdgk70cqq
+27
View File
@@ -0,0 +1,27 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Theme" path="res://ui/UI Theme.tres" id="1"]
[ext_resource type="Script" path="res://ui/component/DialogueTextbox.gd" id="2"]
[node name="DialogueTextbox" type="PanelContainer"]
custom_minimum_size = Vector2(120, 70)
mouse_filter = 2
theme = ExtResource("1")
script = ExtResource("2")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2
theme_override_constants/separation = 1
[node name="SpeakerLabel" type="Label" parent="VBoxContainer"]
layout_mode = 2
text = ""
visible = false
[node name="BodyLabel" type="RichTextLabel" parent="VBoxContainer"]
layout_mode = 2
custom_minimum_size = Vector2(0, 36)
size_flags_vertical = 3
bbcode_enabled = true
scroll_active = true
autowrap_mode = 3
-155
View File
@@ -1,155 +0,0 @@
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
-1
View File
@@ -1 +0,0 @@
uid://h8lw23ypcfty
-63
View File
@@ -1,63 +0,0 @@
[gd_scene load_steps=4 format=3 uid="uid://bkx3l0kckf4a8"]
[ext_resource type="Theme" uid="uid://dm7ee4aqjr2dl" path="res://ui/UI Theme.tres" id="1_wx4lp"]
[ext_resource type="Script" uid="uid://h8lw23ypcfty" path="res://ui/component/VNTextbox.gd" id="2_uo1gm"]
[ext_resource type="Script" uid="uid://bjj6upgk1uvxd" path="res://ui/component/advancedrichtext/AdvancedRichText.gd" id="3_m60k3"]
[node name="ChatBox" type="PanelContainer"]
clip_contents = true
anchors_preset = 12
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = -58.0
grow_horizontal = 2
grow_vertical = 0
theme = ExtResource("1_wx4lp")
script = ExtResource("2_uo1gm")
[node name="MarginContainer" type="MarginContainer" parent="."]
clip_contents = true
layout_mode = 2
theme = ExtResource("1_wx4lp")
theme_override_constants/margin_left = 4
theme_override_constants/margin_top = 4
theme_override_constants/margin_right = 4
theme_override_constants/margin_bottom = 4
[node name="Label" type="RichTextLabel" parent="MarginContainer"]
layout_mode = 2
theme = ExtResource("1_wx4lp")
bbcode_enabled = true
text = "Hello, I'm an NPC!
This is the second line here, I am purposefully adding a tonne of words so that it is forced to go across multiple lines and you can see how the word wrapping works, not only using Godot's built in word wrapping but with my advanced visibile characters smart wrapping. Now I am doing a multiline thing
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10"
script = ExtResource("3_m60k3")
userText = "Hello, I'm an NPC!
This is the second line here, I am purposefully adding a tonne of words so that it is forced to go across multiple lines and you can see how the word wrapping works, not only using Godot's built in word wrapping but with my advanced visibile characters smart wrapping. Now I am doing a multiline thing
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10"
_finalText = "Hello, I'm an NPC!
This is the second line here, I am purposefully adding a tonne of words so that it is forced to go across multiple lines and you can see how the word wrapping works, not only using Godot's built in word wrapping but with my advanced visibile characters smart wrapping. Now I am doing a multiline thing
Line 1
Line 2"
_newLineIndexes = Array[int]([0])
_lines = PackedStringArray("Hello, I\'m an NPC!", "This is the second line here, I am purposefully adding a tonne of words so that it is forced to go across multiple lines and you can see how the word wrapping works, not only using Godot\'s built in word wrapping but with my advanced visibile characters smart wrapping. Now I am doing a multiline thing", "Line 1", "Line 2", "Line 3", "Line 4", "Line 5", "Line 6", "Line 7", "Line 8", "Line 9", "Line 10")
maxLines = 4
-65
View File
@@ -1,65 +0,0 @@
class_name WorldChatBox extends PanelContainer
enum Mode { ADVANCEABLE, TIMED }
const SCENE = preload("res://ui/component/WorldChatBox.tscn")
signal chatboxClosed
var mode:Mode = Mode.TIMED
var worldTarget:Node3D = null
var _timer:float = 0.0
var _label:Label
func _ready() -> void:
_label = $MarginContainer/Label
UI.addChatBox(self)
visible = false
func showTimed(text:String, duration:float) -> void:
mode = Mode.TIMED
_timer = duration
_label.text = text
visible = true
func resetTimer(duration:float) -> void:
_timer = duration
func showAdvanceable(text:String) -> void:
mode = Mode.ADVANCEABLE
_label.text = text
visible = true
func showAndWait(text:String) -> void:
showAdvanceable(text)
await chatboxClosed
func close() -> void:
UI.removeChatBox(self)
chatboxClosed.emit()
queue_free()
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 !visible:
return
_updateWorldPosition()
match mode:
Mode.TIMED:
_timer -= delta
if _timer <= 0.0:
close()
Mode.ADVANCEABLE:
if Input.is_action_just_pressed("interact"):
close()
-1
View File
@@ -1 +0,0 @@
uid://m1keb2pw8bqq
-21
View File
@@ -1,21 +0,0 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Theme" uid="uid://dm7ee4aqjr2dl" path="res://ui/UI Theme.tres" id="1_wx4lp"]
[ext_resource type="Script" path="res://ui/component/WorldChatBox.gd" id="2_wcb"]
[node name="WorldChatBox" type="PanelContainer"]
custom_minimum_size = Vector2(90, 0)
mouse_filter = 2
theme = ExtResource("1_wx4lp")
script = ExtResource("2_wcb")
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 3
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 3
[node name="Label" type="Label" parent="MarginContainer"]
layout_mode = 2
autowrap_mode = 3
@@ -1,141 +0,0 @@
@tool
class_name AdvancedRichText extends RichTextLabel
@export_multiline var userText:String = "" # The text the user is asking for
@export_multiline var _finalText:String = "" # The final text after processing (translation, wrapping, etc.)
@export var _newLineIndexes:Array[int] = [] # The indexes of where each line starts in finalText
@export var _lines:PackedStringArray = [];
# Hides the original RichTextLabel text property
func _set(property: StringName, value) -> bool:
if property == "text":
userText = value
_recalcText()
return true
elif property == "richtextlabel_text":
text = value
return true
return false
func _get(property: StringName):
if property == "text":
return userText
elif property == "richtextlabel_text":
return text
return null
@export var translate:bool = true:
set(value):
translate = value
_recalcText()
get():
return translate
@export var smartWrap:bool = true:
set(value):
smartWrap = value
_recalcText()
get():
return smartWrap
@export var maxLines:int = -1:
set(value):
maxLines = value
_recalcText()
get():
return maxLines
@export var startLine:int = 0:
set(value):
startLine = value
_recalcText()
get():
return startLine
# Returns count of characters that can be displayed, assuming visible_chars = -1
func getCharactersDisplayedCount() -> int:
# Count characters
var count = 0
var lineCount = min(startLine + maxLines, _lines.size()) - startLine
for i in range(startLine, startLine + lineCount):
count += _lines[i].length()
if lineCount > 1:
count += lineCount - 1 # Add newlines
return count
func getFinalText() -> String:
return _finalText
func getTotalLineCount() -> int:
return _lines.size()
func _enter_tree() -> void:
self.threaded = false;
func _recalcText() -> void:
_lines.clear()
if userText.is_empty():
self.richtextlabel_text = ""
return
# Translate if needed
var textTranslated = userText
if self.translate:
textTranslated = tr(textTranslated)
# Replace input bb tags.
var regex = RegEx.new()
regex.compile(r"\[input action=(.*?)\](.*?)\[/input\]")
var inputIconText = textTranslated
for match in regex.search_all(textTranslated):
var action = match.get_string(1).to_lower()
var height:int = get_theme_font_size("normal_font_size")
var img_tag = "[img height=%d valign=center,center]res://ui/input/%s.tres[/img]" % [ height, action ]
inputIconText = inputIconText.replace(match.get_string(0), img_tag)
# Perform smart wrapping
var wrappedText = inputIconText
if smartWrap:
var unwrappedText = wrappedText.strip_edges()
self.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART;
self.richtextlabel_text = unwrappedText
self.visible_characters = -1;
self.fit_content = false;
_newLineIndexes = [];
# Determine where the wrapped newlines are
var line = 0;
var wasNewLine = false;
for i in range(0, self.richtextlabel_text.length()):
var tLine = self.get_character_line(i);
if tLine == line:
wasNewLine = false
if self.richtextlabel_text[i] == "\n":
wasNewLine = true
continue;
if !wasNewLine:
_newLineIndexes.append(i);
line = tLine;
# Create fake pre-wrapped text.
wrappedText = "";
for i in range(0, self.richtextlabel_text.length()):
if _newLineIndexes.find(i) != -1 and i != 0:
wrappedText += "\n";
wrappedText += self.richtextlabel_text[i];
# Handle max and start line(s)
var maxText = wrappedText
if maxLines > 0:
_lines = maxText.split("\n", true);
var selectedLines = [];
for i in range(startLine, min(startLine + maxLines, _lines.size())):
selectedLines.append(_lines[i]);
maxText = "\n".join(selectedLines);
_finalText = maxText
self.richtextlabel_text = maxText
# print("Updated text")
@@ -1 +0,0 @@
uid://bjj6upgk1uvxd
+1 -1
View File
@@ -41,7 +41,7 @@ func _unhandled_input(event:InputEvent) -> void:
if event.is_action_pressed("menu"):
if visible:
close()
elif !UI.dialogueActive && UI.MAIN_CHATBOX.isClosed:
elif !UI.dialogueActive:
open()
get_viewport().set_input_as_handled()
return