Files
pirate-dice/scripts/hud.gd
2026-09-07 04:29:23 +02:00

649 lines
21 KiB
GDScript
Raw 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 CanvasLayer
## Komplettes Spiel-UI (programmatisch aufgebaut).
signal bid_pressed(count: int, face: int)
signal challenge_pressed
signal booster_chosen(id: String, target: int)
signal quit_pressed
signal pause_changed(paused: bool)
const ACCENT := Color(1.0, 0.82, 0.32)
const DIM := Color(0.62, 0.64, 0.70)
## Freie Fläche in der Tischmitte zwischen den Plätzen. Die Würfel der letzten
## Wette werden so gross gewählt, dass sie dort hineinpassen.
const BID_BOX := Vector2(200.0, 150.0)
const TIMER_W := 52.0 ## Breite der Zeitanzeige in der Ansage-Leiste
const GRUEN := Color(0.55, 0.84, 0.55) ## vorhandener Würfel beim Aufdecken
const ROT := Color(0.94, 0.55, 0.52) ## fehlender Würfel beim Aufdecken
var root: Control
var players_box: VBoxContainer
var bid_box: VBoxContainer
var bid_name: Label
var bid_row: HFlowContainer
var boosts_box: VBoxContainer
var banner_label: Label
var count_label: Label
var minus_btn: Button
var plus_btn: Button
var _count := 1
var _count_min := 1
var _count_max := 30
var _bid_count := 0 ## aktuelles Gebot, 0 = noch keins
var _bid_face := 0
var _min_raise := 1
var _actual := 0 ## beim Aufdecken: tatsächlich gefundene Würfel
var face_buttons: Array[Button] = []
var bid_btn: Button
var challenge_btn: Button
var action_panel: PanelContainer
var boost_slot: CenterContainer
var pause_slot: Control
var pause_hint: Label
var boost_panel: PanelContainer
var boost_cards: HBoxContainer
var boost_title: Label
var timer_bid: Label
var timer_boost: Label
var target_row: HBoxContainer
var _face := 1
## Ein gezeichnetes Würfelbild: Augen 1-6 oder ein Fragezeichen bei unbekanntem
## Wert. Wird überall dort benutzt, wo früher Ziffern oder Sternchen standen.
class DieIcon extends Control:
const PIPS := {
1: [Vector2(0, 0)],
2: [Vector2(-1, -1), Vector2(1, 1)],
3: [Vector2(-1, -1), Vector2(0, 0), Vector2(1, 1)],
4: [Vector2(-1, -1), Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1)],
5: [Vector2(-1, -1), Vector2(-1, 1), Vector2(0, 0), Vector2(1, -1), Vector2(1, 1)],
6: [Vector2(-1, -1), Vector2(-1, 0), Vector2(-1, 1),
Vector2(1, -1), Vector2(1, 0), Vector2(1, 1)],
}
var value := 0
var _box := StyleBoxFlat.new()
func _init(v: int, px: float = 0.0, bg := Color.TRANSPARENT) -> void:
value = v
mouse_filter = Control.MOUSE_FILTER_IGNORE
if px > 0.0:
custom_minimum_size = Vector2(px, px)
# sonst zieht der Container das Symbol in die Höhe und es wird unrund
size_flags_vertical = Control.SIZE_SHRINK_CENTER
if bg.a > 0.0:
_box.bg_color = bg
else:
_box.bg_color = Color(0.93, 0.92, 0.88) if v > 0 else Color(0.26, 0.28, 0.34)
_box.set_corner_radius_all(4)
_box.border_color = Color(0, 0, 0, 0.35)
_box.set_border_width_all(1)
func _draw() -> void:
# Immer im grössten Quadrat zeichnen, das in die Fläche passt.
var s := minf(size.x, size.y)
var mid := size * 0.5
draw_style_box(_box, Rect2(mid - Vector2(s, s) * 0.5, Vector2(s, s)))
if value <= 0:
var f := get_theme_default_font()
var fs := int(s * 0.66)
var w := f.get_string_size("?", HORIZONTAL_ALIGNMENT_LEFT, -1, fs).x
draw_string(f, Vector2(mid.x - w * 0.5, mid.y + fs * 0.35), "?",
HORIZONTAL_ALIGNMENT_LEFT, -1, fs, Color(0.85, 0.88, 0.94))
return
for p: Vector2 in PIPS[value]:
draw_circle(mid + p * (s * 0.24), s * 0.105, Color(0.09, 0.08, 0.1))
var _deadline := 0.0 ## lokaler Zeitstempel, 0 = keine Frist
var _phase := ""
var _my_turn := false
var _pending_boost := ""
var _my_values: Array = []
var _my_seat := 0
func _ready() -> void:
layer = 1
process_mode = Node.PROCESS_MODE_ALWAYS
root = Control.new()
root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
root.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(root)
_build()
func _panel(col := Color(0.06, 0.07, 0.10, 0.86)) -> PanelContainer:
var p := PanelContainer.new()
var sb := StyleBoxFlat.new()
sb.bg_color = col
sb.set_corner_radius_all(8)
sb.set_content_margin_all(10)
sb.border_color = Color(1, 1, 1, 0.10)
sb.set_border_width_all(1)
p.add_theme_stylebox_override("panel", sb)
return p
func _label(text: String, size: int, col := Color.WHITE) -> Label:
var l := Label.new()
l.text = text
l.add_theme_font_size_override("font_size", size)
l.add_theme_color_override("font_color", col)
return l
func _place(c: Control, al: float, at: float, ar: float, ab: float,
ol: float, ot: float, orr: float, ob: float) -> void:
c.anchor_left = al
c.anchor_top = at
c.anchor_right = ar
c.anchor_bottom = ab
c.offset_left = ol
c.offset_top = ot
c.offset_right = orr
c.offset_bottom = ob
func _slot(al: float, at: float, ar: float, ab: float,
ol: float, ot: float, orr: float, ob: float) -> CenterContainer:
var cc := CenterContainer.new()
cc.mouse_filter = Control.MOUSE_FILTER_IGNORE
_place(cc, al, at, ar, ab, ol, ot, orr, ob)
root.add_child(cc)
return cc
func _build() -> void:
# --- Spielerliste oben rechts
var pl := _panel()
_place(pl, 1, 0, 1, 0, -16, 16, -16, 16)
pl.grow_horizontal = Control.GROW_DIRECTION_BEGIN
root.add_child(pl)
players_box = VBoxContainer.new()
players_box.add_theme_constant_override("separation", 6)
pl.add_child(players_box)
# --- Aktive Booster oben links, Breite richtet sich nach dem Inhalt
var bo := _panel()
_place(bo, 0, 0, 0, 0, 16, 16, 16, 16)
root.add_child(bo)
boosts_box = VBoxContainer.new()
boosts_box.add_theme_constant_override("separation", 3)
bo.add_child(boosts_box)
# --- Letzte Wette in der Tischmitte, als so viele Würfel wie angesagt
var bid_slot := _slot(0, 0, 1, 1, 0, 0, 0, 0)
bid_box = VBoxContainer.new()
bid_box.add_theme_constant_override("separation", 8)
bid_slot.add_child(bid_box)
bid_name = _label("", 30, ACCENT)
bid_name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
# Auf die freie Mitte begrenzen, sonst läuft der Text über die Nachbarplätze.
bid_name.custom_minimum_size = Vector2(BID_BOX.x, 0)
bid_name.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
bid_box.add_child(bid_name)
bid_row = HFlowContainer.new()
bid_row.alignment = FlowContainer.ALIGNMENT_CENTER
bid_row.custom_minimum_size = Vector2(BID_BOX.x, 0)
bid_row.add_theme_constant_override("h_separation", 5)
bid_row.add_theme_constant_override("v_separation", 5)
bid_box.add_child(bid_row)
# --- Banner oben, im freien Band zwischen den beiden Seitenpanels
banner_label = _label("", 28, ACCENT)
banner_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
banner_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
banner_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
banner_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
var edge := 240.0
_place(banner_label, 0, 0, 1, 0, edge, 20, -edge, 150)
root.add_child(banner_label)
# --- Ansage: kompakte Leiste unten in der Mitte
action_panel = _panel()
_slot(0, 1, 1, 1, 16, -110, -16, -16).add_child(action_panel)
var av := HBoxContainer.new()
av.add_theme_constant_override("separation", 16)
action_panel.add_child(av)
# Gegenstück zur Zeitanzeige am rechten Ende, damit die Leiste symmetrisch
# bleibt, auch wenn dort gerade keine Zeit läuft.
var spacer := Control.new()
spacer.custom_minimum_size = Vector2(TIMER_W, 0)
spacer.mouse_filter = Control.MOUSE_FILTER_IGNORE
av.add_child(spacer)
av.add_child(_label("Anzahl", 15))
minus_btn = Button.new()
minus_btn.text = "−"
minus_btn.custom_minimum_size = Vector2(42, 42)
minus_btn.pressed.connect(_step_count.bind(-1))
av.add_child(minus_btn)
count_label = _label("1", 22)
count_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
count_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
count_label.custom_minimum_size = Vector2(38, 0)
av.add_child(count_label)
plus_btn = Button.new()
plus_btn.text = "+"
plus_btn.custom_minimum_size = Vector2(42, 42)
plus_btn.pressed.connect(_step_count.bind(1))
av.add_child(plus_btn)
var frow := HBoxContainer.new()
frow.add_theme_constant_override("separation", 4)
av.add_child(frow)
# Die Auswahl hebt sich sonst kaum vom unbenutzten Knopf ab.
var gewaehlt := StyleBoxFlat.new()
gewaehlt.bg_color = Color(0.32, 0.26, 0.08)
gewaehlt.set_corner_radius_all(6)
gewaehlt.border_color = ACCENT
gewaehlt.set_border_width_all(2)
for f in range(1, 7):
var b := Button.new()
b.toggle_mode = true
b.custom_minimum_size = Vector2(42, 42)
b.add_theme_stylebox_override("pressed", gewaehlt)
b.add_theme_stylebox_override("hover_pressed", gewaehlt)
b.pressed.connect(_on_face.bind(f))
var ic := DieIcon.new(f)
ic.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
ic.offset_left = 7
ic.offset_top = 7
ic.offset_right = -7
ic.offset_bottom = -7
b.add_child(ic)
frow.add_child(b)
face_buttons.append(b)
face_buttons[0].button_pressed = true
bid_btn = Button.new()
bid_btn.text = "Ansagen"
bid_btn.custom_minimum_size = Vector2(120, 42)
bid_btn.pressed.connect(_on_bid)
av.add_child(bid_btn)
challenge_btn = Button.new()
challenge_btn.text = "Anfechten"
challenge_btn.custom_minimum_size = Vector2(120, 42)
challenge_btn.pressed.connect(_on_challenge)
av.add_child(challenge_btn)
timer_bid = _label("", 18, DIM)
timer_bid.custom_minimum_size = Vector2(TIMER_W, 0)
timer_bid.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
timer_bid.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
av.add_child(timer_bid)
# --- Booster-Overlay
boost_slot = _slot(0, 0, 1, 1, 0, 0, 0, 0)
boost_slot.visible = false
boost_panel = _panel(Color(0.05, 0.06, 0.09, 0.97))
boost_slot.add_child(boost_panel)
var bvv := VBoxContainer.new()
bvv.add_theme_constant_override("separation", 12)
boost_panel.add_child(bvv)
boost_title = _label("Wähle einen Booster", 24, ACCENT)
boost_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
bvv.add_child(boost_title)
timer_boost = _label("", 16, DIM)
timer_boost.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
bvv.add_child(timer_boost)
boost_cards = HBoxContainer.new()
boost_cards.add_theme_constant_override("separation", 12)
bvv.add_child(boost_cards)
target_row = HBoxContainer.new()
target_row.alignment = BoxContainer.ALIGNMENT_CENTER
target_row.add_theme_constant_override("separation", 6)
target_row.visible = false
bvv.add_child(target_row)
# --- Pausenmenü (Esc)
pause_slot = Control.new()
pause_slot.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
pause_slot.visible = false
root.add_child(pause_slot)
var dim := ColorRect.new()
dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
dim.color = Color(0, 0, 0, 0.55)
pause_slot.add_child(dim)
var pc := CenterContainer.new()
pc.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
pause_slot.add_child(pc)
var pp := _panel(Color(0.05, 0.06, 0.09, 0.98))
pc.add_child(pp)
var pv := VBoxContainer.new()
pv.custom_minimum_size = Vector2(280, 0)
pv.add_theme_constant_override("separation", 10)
pp.add_child(pv)
var pt := _label("Pause", 26, ACCENT)
pt.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
pv.add_child(pt)
pause_hint = _label("", 13, DIM)
pause_hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
pv.add_child(pause_hint)
var resume := Button.new()
resume.text = "Fortsetzen"
resume.custom_minimum_size = Vector2(0, 42)
resume.pressed.connect(_set_paused.bind(false))
pv.add_child(resume)
var leave := Button.new()
leave.text = "Verlassen"
leave.custom_minimum_size = Vector2(0, 42)
leave.pressed.connect(_on_quit)
pv.add_child(leave)
func _on_quit() -> void:
quit_pressed.emit()
## Ein Gebot muss höher sein als das vorige. Bei gleicher Anzahl geht das nur
## über eine höhere Augenzahl - die kleineren werden deshalb ausgeblendet.
func _face_allowed(f: int) -> bool:
if _bid_count <= 0 or _count >= _bid_count + _min_raise:
return true
if _min_raise > 1:
return false
return _count == _bid_count and f > _bid_face
func _refresh_faces() -> void:
var first_ok := 0
for i in face_buttons.size():
var ok := _my_turn and _face_allowed(i + 1)
face_buttons[i].disabled = not ok
face_buttons[i].modulate = Color(1, 1, 1, 1.0 if ok else 0.3)
if ok and first_ok == 0:
first_ok = i + 1
# Steht die gewählte Augenzahl nicht mehr zur Wahl, auf die kleinste mögliche
if _my_turn and first_ok > 0 and not _face_allowed(_face):
_on_face(first_ok)
## Beim Drankommen auf das kleinstmögliche Gebot stellen - von dort aus ist es
## am wenigsten Klickarbeit bis zur gewünschten Ansage.
func _lowest_bid() -> void:
if _bid_count <= 0:
_set_count(1)
_on_face(1)
elif _min_raise == 1 and _bid_face < 6:
# Gleiche Anzahl reicht, wenn die Augenzahl steigt.
_set_count(_bid_count)
_on_face(_bid_face + 1)
else:
_set_count(_bid_count + _min_raise)
_on_face(1)
func _step_count(delta: int) -> void:
_set_count(_count + delta)
func _set_count(v: int) -> void:
_count = clampi(v, _count_min, _count_max)
count_label.text = str(_count)
minus_btn.disabled = not _my_turn or _count <= _count_min
plus_btn.disabled = not _my_turn or _count >= _count_max
_refresh_faces()
func _on_bid() -> void:
bid_pressed.emit(_count, _face)
func _on_challenge() -> void:
challenge_pressed.emit()
func _unhandled_key_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo \
and event.keycode == KEY_ESCAPE:
_set_paused(not pause_slot.visible)
get_viewport().set_input_as_handled()
func _set_paused(on: bool) -> void:
pause_slot.visible = on
if on:
pause_hint.text = "Die Runde läuft weiter." if multiplayer.get_peers().size() > 0 \
else "Das Spiel ist angehalten."
pause_changed.emit(on)
## queue_free() nimmt den Knoten erst am Frameende aus dem Baum - bis dahin
## lägen alte und neue Einträge nebeneinander im Container.
func _clear(box: Node) -> void:
for c in box.get_children():
box.remove_child(c)
c.queue_free()
func _on_face(f: int) -> void:
_face = f
for i in face_buttons.size():
face_buttons[i].button_pressed = (i + 1 == f)
# ---------------------------------------------------------------- Updates
func update_state(st: Dictionary, my_seat: int) -> void:
_my_seat = my_seat
_my_values = st.values
_phase = st.phase
_actual = int(st.get("actual", 0))
var left := float(st.get("time_left", 0.0))
_deadline = 0.0 if left <= 0.0 else Time.get_ticks_msec() / 1000.0 + left
_clear(players_box)
for i in st.seats.size():
var s: Dictionary = st.seats[i]
var turn: bool = st.phase == "bid" and i == st.turn
var col := ACCENT if turn else (Color.WHITE if s.alive else DIM)
var entry := VBoxContainer.new()
entry.add_theme_constant_override("separation", 2)
players_box.add_child(entry)
var nm := _label(("▸ " if turn else "") + _short(String(s.name)) + (" (KI)" if s.ai else ""), 14, col)
nm.clip_text = true
nm.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
entry.add_child(nm)
if not s.alive:
entry.add_child(_label(" ausgeschieden", 13, DIM))
continue
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 4)
entry.add_child(row)
for v: int in _seat_dice(st, i, my_seat):
row.add_child(DieIcon.new(v, 21.0))
_clear(bid_row)
if st.bid.is_empty():
bid_name.text = "Kein Gebot"
bid_name.add_theme_color_override("font_color", Color(1, 1, 1, 0.5))
else:
bid_name.text = String(st.seats[st.bid.seat].name)
bid_name.add_theme_color_override("font_color", ACCENT)
var px := _fit_dice(int(st.bid.count))
# Beim Aufdecken zeigen die Farben, wie viele der angesagten Würfel es
# wirklich gab: grün die vorhandenen, rot die fehlenden.
var faerben: bool = st.phase == "reveal"
for i in int(st.bid.count):
var bg := Color.TRANSPARENT
if faerben:
bg = GRUEN if i < _actual else ROT
bid_row.add_child(DieIcon.new(int(st.bid.face), px, bg))
_clear(boosts_box)
var boosts: Array = st.get("boosts", [])
var any := false
for i in mini(boosts.size(), st.seats.size()):
if String(boosts[i]) == "":
continue
any = true
var b: Dictionary = BoosterDB.get_booster(String(boosts[i]))
var line := HBoxContainer.new()
line.add_theme_constant_override("separation", 5)
boosts_box.add_child(line)
line.add_child(_label(_short(String(st.seats[i].name)) + ":", 14,
ACCENT if i == my_seat else Color(0.78, 0.80, 0.86)))
var kind := Color(0.5, 0.9, 0.6) if b.kind == BoosterDB.Kind.BUFF else Color(0.95, 0.5, 0.5)
line.add_child(_label(String(b.get("name", "")), 14, kind))
if int(st.min_raise) > 1:
boosts_box.add_child(_label("Störfeuer: mind. %d erhöhen" % st.min_raise, 14, ACCENT))
any = true
if st.must_bid:
boosts_box.add_child(_label("Maulkorb: du musst bieten", 14, ACCENT))
any = true
if not any:
boosts_box.add_child(_label("Keine Booster aktiv", 14, DIM))
banner_label.text = st.banner
var my_turn: bool = st.phase == "bid" and st.turn == my_seat and st.seats[my_seat].alive
var kam_dran := my_turn and not _my_turn
_my_turn = my_turn
action_panel.modulate = Color(1, 1, 1, 1.0 if my_turn else 0.45)
bid_btn.disabled = not my_turn
_count_max = maxi(1, int(st.total))
_min_raise = maxi(1, int(st.min_raise))
_bid_count = 0 if st.bid.is_empty() else int(st.bid.count)
_bid_face = 0 if st.bid.is_empty() else int(st.bid.face)
# Ein Maulkorb sperrt das Anfechten - ausser es gäbe gar kein gültiges
# Gebot mehr, dann bliebe sonst kein einziger Knopf übrig.
var gebot_moeglich: bool = _bid_count + _min_raise <= _count_max \
or (_min_raise == 1 and _bid_face < 6)
challenge_btn.disabled = not my_turn or st.bid.is_empty() \
or (st.must_bid and gebot_moeglich)
var low := 1
if _bid_count > 0:
low = _bid_count + (_min_raise if _min_raise > 1 else 0)
_count_min = clampi(low, 1, _count_max)
_set_count(_count)
if kam_dran:
_lowest_bid()
if st.phase != "boost":
boost_slot.visible = false
## Läuft die Restzeit herunter, solange eine Eingabe von dir erwartet wird.
## Nach Ablauf entscheidet der Host automatisch.
func _process(_delta: float) -> void:
if pause_slot != null and pause_slot.visible:
return
var wants_boost := boost_slot != null and boost_slot.visible
var wants_bid := _my_turn and _phase == "bid"
if _deadline <= 0.0 or not (wants_boost or wants_bid):
timer_bid.text = ""
timer_boost.text = ""
return
var left := maxf(0.0, _deadline - Time.get_ticks_msec() / 1000.0)
var text := "%d s" % ceili(left)
var col := Color(0.95, 0.45, 0.4) if left <= 10.0 else DIM
timer_bid.text = text if wants_bid else ""
timer_bid.add_theme_color_override("font_color", col)
timer_boost.text = "Noch %s - danach wird zufällig gewählt" % text if wants_boost else ""
timer_boost.add_theme_color_override("font_color", col)
## Eigene Würfel sind bekannt, gegnerische nicht - ausser sie wurden gespäht
## oder es wird gerade aufgedeckt.
## Lange Namen kürzen, damit die Panels nicht in die Breite laufen.
func _short(name: String) -> String:
return name if name.length() <= 12 else name.substr(0, 11) + "…"
## Grösste Würfelgrösse, bei der `n` Würfel noch in die freie Mitte passen.
func _fit_dice(n: int) -> float:
const GAP := 4.0
for px: float in [34.0, 30.0, 26.0, 22.0, 18.0, 15.0, 12.0]:
var per_row := maxi(1, int((BID_BOX.x + GAP) / (px + GAP)))
var rows := ceili(float(n) / per_row)
if rows * (px + GAP) - GAP <= BID_BOX.y:
return px
return 12.0
func _seat_dice(st: Dictionary, seat: int, my_seat: int) -> Array:
if seat == my_seat:
return st.values
var open: Array = st.get("open", [])
if seat < open.size() and not open[seat].is_empty():
return open[seat]
var out: Array = []
for _k in int(st.seats[seat].count):
out.append(-1)
var peek: Dictionary = st.peek
if peek.has(seat):
var idx := int(peek[seat][0])
if idx >= 0 and idx < out.size():
out[idx] = int(peek[seat][1])
return out
func set_banner(text: String) -> void:
banner_label.text = text
# ---------------------------------------------------------------- Booster
func show_booster_offer(ids: Array, values: Array) -> void:
_my_values = values
_pending_boost = ""
target_row.visible = false
boost_title.text = "Wähle einen Booster"
_clear(boost_cards)
for id: String in ids:
var b: Dictionary = BoosterDB.get_booster(id)
var card := _panel(Color(0.10, 0.12, 0.17, 0.98))
card.custom_minimum_size = Vector2(210, 170)
var v := VBoxContainer.new()
v.add_theme_constant_override("separation", 6)
card.add_child(v)
var kind_col := Color(0.5, 0.9, 0.6) if b.kind == BoosterDB.Kind.BUFF else Color(0.95, 0.5, 0.5)
v.add_child(_label("BUFF" if b.kind == BoosterDB.Kind.BUFF else "DEBUFF", 12, kind_col))
v.add_child(_label(b.name, 20, ACCENT))
var d := _label(b.desc, 14, Color(0.8, 0.82, 0.88))
d.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
d.custom_minimum_size = Vector2(190, 60)
v.add_child(d)
var btn := Button.new()
btn.text = "Wählen"
btn.pressed.connect(_pick.bind(id))
v.add_child(btn)
boost_cards.add_child(card)
boost_slot.visible = true
func _pick(id: String) -> void:
if not BoosterDB.needs_target(id) or _my_values.is_empty():
booster_chosen.emit(id, 0)
boost_slot.visible = false
return
_pending_boost = id
boost_title.text = "%s - welchen Würfel?" % BoosterDB.get_booster(id).name
_clear(boost_cards)
_clear(target_row)
for i in _my_values.size():
var b := Button.new()
b.custom_minimum_size = Vector2(56, 56)
b.pressed.connect(_pick_target.bind(i))
var ic := DieIcon.new(int(_my_values[i]))
ic.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
ic.offset_left = 10
ic.offset_top = 10
ic.offset_right = -10
ic.offset_bottom = -10
b.add_child(ic)
target_row.add_child(b)
target_row.visible = true
func _pick_target(idx: int) -> void:
booster_chosen.emit(_pending_boost, idx)
boost_slot.visible = false
target_row.visible = false