Files
pirate-dice/scripts/hud_panels.gd
2026-09-16 16:15:13 +02:00

254 lines
9.5 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends RefCounted
## Die Münzfelder des HUD: Vorrat, Topf, Erklärung beim Überfahren, die
## Wurfansage, dazu Gebotsverlauf und Protokoll. Arbeitet auf dem HUD `h`,
## dem die Knoten und Signale gehören. Kein class_name (headless kein Cache).
const Icons = preload("res://scripts/icons.gd")
var h: CanvasLayer
func _init(hud: CanvasLayer) -> void:
h = hud
## Der Vorrat liegt offen unten links. Ein Klick wirft die Münze sofort; nur
## Münzen mit eigenem Ziel fragen noch nach dem Würfel. Die Wirkung steht im
## Tooltip - auf eine Münze passt nur das Sinnbild.
func _zeichne_hand(st: Dictionary) -> void:
if h.hand_box == null:
return
var muenzen: Array = st.get("hand", [])
# Wer raus ist, schaut nur noch zu. Münzen, die er nie werfen kann,
# stünden nur klickbar im Weg.
var dabei: bool = h._my_seat < st.seats.size() and st.seats[h._my_seat].alive
h.hand_panel.visible = dabei and not muenzen.is_empty()
# Geworfen wird nur am eigenen Zug. Was eine Ansage braucht, wartet auf eine.
var spielbar: bool = st.phase == "bid" and int(st.turn) == h._my_seat
var gebot: bool = not st.bid.is_empty()
var voll: bool = st.get("voll", false)
# Jeder Zustand kommt mehrmals an. Ein Neuaufbau würde die Münze unter dem
# Zeiger wegreissen und die Erklärung mitnehmen.
var sig := "%s|%s|%s|%s" % [muenzen, spielbar, gebot, voll]
if sig == h._hand_sig:
return
h._hand_sig = sig
h._clear(h.hand_box)
for id: String in muenzen:
var b: Dictionary = BoosterDB.get_booster(id)
if b.is_empty():
continue
var col := _kind_col(b)
var btn := Button.new()
btn.custom_minimum_size = Vector2(104, 120)
btn.disabled = not spielbar or (BoosterDB.needs_bid(id) and not gebot) \
or (BoosterDB.needs_room(id) and voll)
# Der gesperrte Knopf dunkelt nur seinen Rand ab; Münze und Name darauf
# blieben sonst so hell wie bei einer werfbaren.
btn.modulate = Color(1, 1, 1, 1.0 if not btn.disabled else 0.4)
# Ein gesperrter Knopf meldet keine Maus mehr; die Erklärung soll aber
# auch dann kommen, wenn gerade nicht geworfen werden darf.
btn.mouse_filter = Control.MOUSE_FILTER_STOP
btn.pressed.connect(h._pick.bind(id))
h.hand_box.add_child(btn)
_info_an(btn, id, false, h.hand_panel)
var v := VBoxContainer.new()
v.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
v.offset_top = 8
v.offset_bottom = -6
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
v.add_theme_constant_override("separation", 4)
btn.add_child(v)
var ic := Icons.MuenzIcon.new(id, col, 72.0)
ic.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
v.add_child(ic)
var name_l: Label = h._label(String(b.name), 14, col)
name_l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name_l.mouse_filter = Control.MOUSE_FILTER_IGNORE
v.add_child(name_l)
## Der Topf: was diese Runde geworfen wurde und wem es gehört, wenn er
## das Aufdecken gewinnt. Darunter, was gerade auf einem selbst liegt.
## Auch hier gilt: nur neu bauen, wenn sich etwas geändert hat.
func _zeichne_mitte(st: Dictionary, my_seat: int) -> void:
var boosts: Array = st.get("boosts", [])
# Der neueste Wurf muss bei jedem Zustand geprüft werden, auch ohne Neubau.
var neu := {}
for m in boosts:
if int(m.get("nr", 0)) > int(neu.get("nr", 0)):
neu = m
_pruefe_wurf(neu, st)
var zaehlung: Dictionary = st.get("zaehlung", {})
var sig := "%s|%s|%s" % [boosts, zaehlung, st.must_bid]
if sig == h._mitte_sig:
return
h._mitte_sig = sig
h._clear(h.boosts_box)
h.boosts_box.add_child(h._label("Topf" if not boosts.is_empty() else "Topf: leer", 13, h.DIM))
for m in boosts:
var id := String(m.get("id", ""))
var b: Dictionary = BoosterDB.get_booster(id)
if b.is_empty():
continue
var wer := int(m.get("seat", 0))
var line := HBoxContainer.new()
line.add_theme_constant_override("separation", 5)
# Der Container muss die Maus annehmen, sonst gibt es kein Überfahren.
line.mouse_filter = Control.MOUSE_FILTER_STOP
h.boosts_box.add_child(line)
_info_an(line, id, true)
line.add_child(Icons.MuenzIcon.new(id, _kind_col(b), 22.0))
line.add_child(h._label(String(b.get("name", "")), 14, _kind_col(b)))
if bool(m.get("stark", false)):
line.add_child(h._label("· Wappen", 14, h.ACCENT))
if wer < st.seats.size():
line.add_child(h._label("von " + h._short(String(st.seats[wer].name)), 13,
h.ACCENT if wer == my_seat else h.DIM))
# Ergebnis vom Zählen: so oft liegt die Augenzahl am Tisch.
for f in zaehlung:
var line := HBoxContainer.new()
line.add_theme_constant_override("separation", 5)
h.boosts_box.add_child(line)
var von := int(zaehlung[f][0])
var bis := int(zaehlung[f][1])
var text := "Am Tisch: %d ×" % von if von == bis else "Am Tisch: %d–%d ×" % [von, bis]
line.add_child(h._label(text, 14, h.ACCENT))
line.add_child(Icons.DieIcon.new(int(f), 18.0))
if st.must_bid:
h.boosts_box.add_child(h._label("Maulkorb: du musst bieten", 14, h.ACCENT))
## Die letzten Ansagen der Runde, die jüngste unten und hervorgehoben.
func _zeichne_verlauf(st: Dictionary, my_seat: int) -> void:
var verlauf: Array = st.get("verlauf", [])
var sig := str(verlauf)
if sig == h._verlauf_sig:
return
h._verlauf_sig = sig
h._clear(h.verlauf_box)
if verlauf.is_empty():
return
h.verlauf_box.add_child(h._label("Verlauf", 13, h.DIM))
var ab := maxi(0, verlauf.size() - 6)
for i in range(ab, verlauf.size()):
var e: Dictionary = verlauf[i]
var wer := int(e.get("seat", 0))
if wer >= st.seats.size():
continue
var letzte: bool = i == verlauf.size() - 1
var col: Color = h.ACCENT if letzte else (Color(0.85, 0.87, 0.92) if wer != my_seat else Color(0.95, 0.85, 0.55))
var line := HBoxContainer.new()
line.add_theme_constant_override("separation", 5)
h.verlauf_box.add_child(line)
var nm: Label = h._label(h._short(String(st.seats[wer].name)), 13, col)
nm.custom_minimum_size = Vector2(64, 0)
line.add_child(nm)
line.add_child(h._label("%d ×" % int(e.get("count", 0)), 13, col))
line.add_child(Icons.DieIcon.new(int(e.get("face", 1)), 16.0))
if bool(e.get("erhoeht", false)):
line.add_child(h._label("erhöht", 12, h.DIM))
## Erklärung zu einer Münze, sobald die Maus daraufliegt. `unter` legt sie
## unter das Feld statt darüber - oben links wäre sonst kein Platz. `anker`
## ist, woran sie sich ausrichtet: beim Vorrat das ganze Feld, sonst läge sie
## halb über dessen Überschrift.
func _info_an(ctrl: Control, id: String, unter: bool, anker: Control = null) -> void:
var an: Control = anker if anker != null else ctrl
ctrl.mouse_entered.connect(_info_zeigen.bind(ctrl, id, unter, an))
ctrl.mouse_exited.connect(_info_weg.bind(ctrl))
func _info_zeigen(ctrl: Control, id: String, unter: bool, anker: Control) -> void:
var b: Dictionary = BoosterDB.get_booster(id)
if b.is_empty():
return
h._info_ctrl = ctrl
h.info_panel.visible = false
h.info_name.text = String(b.name)
h.info_desc.text = String(b.desc)
h.info_stark.text = "Wappen: %s" % b.get("stark", "")
# Die Höhe steht erst nach dem Umbruch fest, deshalb ein Bild später
# setzen. Bis dahin bleibt das Feld unsichtbar, sonst springt es.
await h.get_tree().process_frame
if h._info_ctrl != ctrl or not is_instance_valid(ctrl) or not is_instance_valid(anker):
return
h.info_panel.reset_size()
var gr: Vector2 = h.info_panel.size
var r: Rect2 = anker.get_global_rect()
var y: float = r.position.y + r.size.y + 8.0 if unter else r.position.y - gr.y - 8.0
h.info_panel.position = Vector2(
clampf(r.position.x, 8.0, maxf(8.0, h.root.size.x - gr.x - 8.0)),
clampf(y, 8.0, maxf(8.0, h.root.size.y - gr.y - 8.0)))
h.info_panel.visible = true
func _info_weg(ctrl: Control) -> void:
if h._info_ctrl != ctrl:
return
h._info_ctrl = null
h.info_panel.visible = false
func _kind_col(b: Dictionary) -> Color:
return Color(0.5, 0.9, 0.6) if b.kind == BoosterDB.Kind.BUFF else Color(0.95, 0.5, 0.5)
## Jeder Wurf hat eine laufende Nummer. Steigt sie, ist eine neue Münze
## gefallen und wird gezeigt. Zu Rundenbeginn ist die Mitte leer, dann faengt
## die Zählung wieder bei null an.
func _pruefe_wurf(neu: Dictionary, st: Dictionary) -> void:
var nr := int(neu.get("nr", 0))
if nr == 0:
h._letzter_wurf = 0
return
if nr <= h._letzter_wurf:
return
h._letzter_wurf = nr
var seat := int(neu.get("seat", 0))
if seat >= st.seats.size():
return
_zeig_wurf(String(neu.get("id", "")), bool(neu.get("stark", false)),
h._short(String(st.seats[seat].name)))
## Der Ausgang steht schon fest - der Host hat ihn ausgewürfelt. Die Münze
## fällt in 3D auf den Tisch; hier steht der Text dazu, kurz und dann weg.
func _zeig_wurf(id: String, stark: bool, wer: String) -> void:
var b: Dictionary = BoosterDB.get_booster(id)
if b.is_empty():
return
h.wurf_label.text = "%s wirft %s\n%s" % [wer, b.name,
"Wappen - " + String(b.get("stark", "")) if stark else "Zahl - Grundwirkung"]
h.wurf_label.add_theme_color_override("font_color", h.ACCENT if stark else h.DIM)
h.wurf_slot.visible = true
Klang.muenze()
if h._wurf_tween != null and h._wurf_tween.is_valid():
h._wurf_tween.kill()
h._wurf_tween = h.create_tween()
h._wurf_tween.tween_interval(2.6)
h._wurf_tween.tween_callback(func() -> void: h.wurf_slot.visible = false)
## Die letzten Zeilen des Spielprotokolls.
func _zeichne_log(st: Dictionary) -> void:
var zeilen: Array = st.get("log", [])
var sig := str(zeilen)
if sig == h._log_sig:
return
h._log_sig = sig
h._clear(h.log_box)
var ab := maxi(0, zeilen.size() - 5)
for i in range(ab, zeilen.size()):
var l: Label = h._label(String(zeilen[i]), 13, h.ACCENT if i == zeilen.size() - 1 else h.DIM)
l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
l.custom_minimum_size = Vector2(340, 0)
h.log_box.add_child(l)