Files
whale-town-front-v2/scenes/ui/ChatBubble.gd

131 lines
4.1 KiB
GDScript

extends Control
const DEFAULT_DURATION: float = 5.0
const TARGET_OFFSET: Vector2 = Vector2(0, -84)
const TARGET_TOP_GAP: float = 10.0
const FALLBACK_TARGET_HEIGHT: float = 64.0
const MIN_TEXT_WIDTH: float = 96.0
const MAX_TEXT_WIDTH: float = 230.0
const TEXT_FONT_SIZE: float = 18.0
const LINE_HEIGHT: float = 24.0
@onready var label: Label = $PanelContainer/Label
@onready var panel: PanelContainer = $PanelContainer
var _targetNode: Node2D
var _targetOffset: Vector2 = TARGET_OFFSET
var _originalText: String = ""
func _process(_delta: float) -> void:
if _targetNode == null or not is_instance_valid(_targetNode):
return
_update_position()
func set_text(text: String, targetNode: Node2D = null, targetOffset: Vector2 = TARGET_OFFSET) -> void:
_originalText = text
_targetNode = targetNode
_targetOffset = targetOffset
_update_size()
if _targetNode != null:
_update_position()
await get_tree().create_timer(DEFAULT_DURATION).timeout
queue_free()
func _update_size() -> void:
if panel == null:
return
var metrics := _get_text_metrics(_originalText)
label.text = str(metrics.get("display_text", _originalText))
label.autowrap_mode = TextServer.AUTOWRAP_OFF
label.custom_minimum_size = Vector2(float(metrics.get("width", MIN_TEXT_WIDTH)), float(metrics.get("height", LINE_HEIGHT)))
label.size = label.custom_minimum_size
panel.reset_size()
reset_size()
panel.size = panel.get_combined_minimum_size()
size = panel.size
func _update_position() -> void:
var anchor := _get_target_top_screen_position()
var bubbleSize := _get_bubble_size()
global_position = Vector2(
anchor.x - bubbleSize.x * 0.5 + _targetOffset.x,
anchor.y - bubbleSize.y - TARGET_TOP_GAP
).round()
func _get_bubble_size() -> Vector2:
if panel != null:
var combined := panel.get_combined_minimum_size()
if combined.x > 0.0 and combined.y > 0.0:
return combined
if size.x > 0.0 and size.y > 0.0:
return size
return Vector2(280.0, 52.0)
func _get_text_metrics(text: String) -> Dictionary:
var lines: Array[String] = []
var currentLine := ""
var currentWidth := 0.0
var maxLineWidth := 0.0
for index in range(text.length()):
var character := text.substr(index, 1)
var characterWidth := _measure_text_width(character)
if not currentLine.is_empty() and currentWidth + characterWidth > MAX_TEXT_WIDTH:
lines.append(currentLine)
maxLineWidth = max(maxLineWidth, currentWidth)
currentLine = character
currentWidth = characterWidth
else:
currentLine += character
currentWidth += characterWidth
if not currentLine.is_empty() or lines.is_empty():
lines.append(currentLine)
maxLineWidth = max(maxLineWidth, currentWidth)
return {
"display_text": "\n".join(lines),
"width": clamp(maxLineWidth + 4.0, MIN_TEXT_WIDTH, MAX_TEXT_WIDTH),
"height": LINE_HEIGHT * float(lines.size())
}
func _measure_text_width(text: String) -> float:
if label != null:
var font := label.get_theme_font("font")
if font != null:
var fontSize := label.get_theme_font_size("font_size")
if fontSize <= 0:
fontSize = int(TEXT_FONT_SIZE)
var measured := font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1.0, fontSize).x
if measured > 0.0:
return measured
var estimated := 0.0
for index in range(text.length()):
var code := text.unicode_at(index)
estimated += TEXT_FONT_SIZE * 0.56 if code <= 0x7f else TEXT_FONT_SIZE
return estimated
func _get_target_top_screen_position() -> Vector2:
var targetScreenPosition: Vector2 = _targetNode.get_global_transform_with_canvas().origin
var visualTopOffset := _get_target_visual_top_offset()
return targetScreenPosition + visualTopOffset
func _get_target_visual_top_offset() -> Vector2:
var topOffset := Vector2(0.0, -FALLBACK_TARGET_HEIGHT)
if _targetNode == null:
return topOffset
var sprite := _targetNode.get_node_or_null("Sprite2D") as Sprite2D
if sprite == null:
return topOffset
var rect := sprite.get_rect()
var localTop := sprite.position + Vector2(0.0, rect.position.y * sprite.scale.y)
var nodeToCanvas := _targetNode.get_global_transform_with_canvas()
var topScreenPosition := nodeToCanvas * localTop
var targetScreenPosition: Vector2 = nodeToCanvas.origin
return topScreenPosition - targetScreenPosition