forked from xiangwang25/whale-town-front-v2
Initial WhaleTown V2 frontend
This commit is contained in:
130
scenes/ui/ChatBubble.gd
Normal file
130
scenes/ui/ChatBubble.gd
Normal file
@@ -0,0 +1,130 @@
|
||||
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
|
||||
Reference in New Issue
Block a user