254 lines
9.5 KiB
GDScript
254 lines
9.5 KiB
GDScript
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)
|