saumwacht-godot/scripts/luneraria/ui/components/lune_tooltip.gd
Martin Stickelbroeck 1b540f2a55 G3 (Zwischenstand): neues Ingame-HUD; Projekt wechselt nach Unity
Nicht abgenommen: Nach dem ersten Spieltest wurde entschieden, Champions of
Luneraria nach Unity 6 zu portieren. Dieser Stand ist die Referenz für das
HUD im Unity-Projekt (../luneraria-unity).

- HUD neu aus Komponenten mit eigenem Theme, Porträts aus den echten Modellen
  (einmal gerendert, zwischengespeichert), Lane-Übersicht mit deterministischer
  Bedrohung aus öffentlichem Zustand, Bossgrotte, nächste Welle, Ereignisse,
  Minimap mit zusammengefassten Armeen, Held/Items/Talente, Einheitendetails,
  Angriffschampions, Bereit-Sperre, UI-Größe, Team-Formen statt nur Farben
- Privatsphäre über LuneHudModel: fremdes Gold und fremde Entwürfe erscheinen nie
- Keine Regeländerung: Regeln 91/91, M2 73/73, Netz 43/43; verify_g3 715/715;
  verify_m1 38/38 und verify_m2 14/14 mit angepassten UI-Treibern
- Spieltest bis Welle 8 über die echte Oberfläche, Befunde in G3-HUD.md
- HELDEN-KONZEPT.md: vom Projektinhaber vorgegebene Helden (nicht umgesetzt)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:59:55 +02:00

141 lines
5.1 KiB
GDScript

class_name LuneTooltip
extends PanelContainer
## The one tooltip of the HUD. A single panel is reused for every hover: it appears after a
## short delay next to the hovered control, stays inside the window, never takes mouse input
## and hides as soon as the control is left, hidden, disabled for input, freed or clicked.
## Content is a small dictionary: title, subtitle, stats ([glyph, name, value] rows), text,
## note and warn. Components register with `LuneTooltip.attach(control, builder)`.
const DELAY: float = .28
const QUICK: float = .05
const WIDTH: float = 300.0
static var current: LuneTooltip
var _owner: Control
var _builder: Callable
var _wait: float = 0.0
var _last_hide: float = -10.0
var _clock: float = 0.0
var _column: VBoxContainer
## Last pointer position from input events (canvas coordinates); real and synthetic motion.
var _pointer: Vector2 = Vector2(-1, -1)
func _init() -> void:
theme_type_variation = "HudTooltip"
mouse_filter = Control.MOUSE_FILTER_IGNORE
visible = false
z_index = 100
_column = VBoxContainer.new()
_column.add_theme_constant_override("separation", 4)
_column.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_column)
current = self
static func attach(control: Control, builder: Callable) -> void:
control.mouse_entered.connect(func() -> void:
if current != null and is_instance_valid(current):
current.hover(control, builder))
control.mouse_exited.connect(func() -> void:
if current != null and is_instance_valid(current):
current.leave(control))
control.gui_input.connect(func(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and current != null and is_instance_valid(current):
current.leave(control))
func hover(control: Control, builder: Callable) -> void:
if _pointer.x < 0.0:
_pointer = control.get_global_mouse_position()
_owner = control
_builder = builder
_wait = QUICK if _clock - _last_hide < .6 else DELAY
visible = false
func leave(control: Control) -> void:
if control == _owner:
_hide()
func _hide() -> void:
if visible:
_last_hide = _clock
_owner = null
visible = false
## Refreshes the text of an open tooltip (values changed while hovering).
func refresh(control: Control) -> void:
if control == _owner and visible:
_show()
func _input(event: InputEvent) -> void:
if event is InputEventMouse:
_pointer = (event as InputEventMouse).position
func _process(delta: float) -> void:
_clock += delta
if _owner == null:
return
if not is_instance_valid(_owner) or not _owner.is_visible_in_tree() or not _owner.get_global_rect().has_point(_pointer):
_hide()
return
if not visible:
_wait -= delta
if _wait <= 0.0:
_show()
func _show() -> void:
var content: Dictionary = _builder.call() if _builder.is_valid() else {}
if content.is_empty():
visible = false
return
for child: Node in _column.get_children():
child.free()
if content.has("title"):
var head := HBoxContainer.new()
head.add_theme_constant_override("separation", 6)
head.mouse_filter = Control.MOUSE_FILTER_IGNORE
_column.add_child(head)
if content.has("glyph"):
LuneGlyph.make(str(content.glyph), content.get("glyph_color", LuneUiTheme.BRONZE_LIGHT), 18.0, head)
LuneUiTheme.label(str(content.title), "HudValue", head)
if content.has("subtitle"):
LuneUiTheme.label(str(content.subtitle), "HudMuted", _column)
var stats: Array = content.get("stats", [])
if not stats.is_empty():
var grid := GridContainer.new()
grid.columns = 3
grid.add_theme_constant_override("h_separation", 8)
grid.add_theme_constant_override("v_separation", 2)
grid.mouse_filter = Control.MOUSE_FILTER_IGNORE
_column.add_child(grid)
for row: Array in stats:
LuneGlyph.make(str(row[0]), LuneUiTheme.MUTED, 14.0, grid)
LuneUiTheme.label(str(row[1]), "HudMuted", grid)
var value := LuneUiTheme.label(str(row[2]), "HudText", grid)
value.size_flags_horizontal = Control.SIZE_EXPAND_FILL
for key: String in ["text", "note", "warn"]:
if content.has(key) and not str(content[key]).is_empty():
var line := LuneUiTheme.label(str(content[key]), "HudText" if key == "text" else "HudMuted", _column)
line.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
line.custom_minimum_size.x = WIDTH - 24.0
if key == "warn":
line.add_theme_color_override("font_color", LuneUiTheme.DANGER)
visible = true
reset_size()
custom_minimum_size.x = 0.0
await get_tree().process_frame
if not visible or _owner == null or not is_instance_valid(_owner):
return
_place()
func _place() -> void:
reset_size()
var parent_rect: Rect2 = (get_parent() as Control).get_global_rect() if get_parent() is Control else get_viewport_rect()
var anchor: Rect2 = _owner.get_global_rect()
var scale_factor: float = get_global_transform().get_scale().x
var extent: Vector2 = size * scale_factor
var at := Vector2(anchor.get_center().x - extent.x * .5, anchor.position.y - extent.y - 8.0)
if at.y < parent_rect.position.y + 6.0:
at.y = anchor.end.y + 8.0
at.x = clampf(at.x, parent_rect.position.x + 6.0, parent_rect.end.x - extent.x - 6.0)
at.y = clampf(at.y, parent_rect.position.y + 6.0, parent_rect.end.y - extent.y - 6.0)
global_position = at