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)