Files
pirate-dice/scripts/hud.gd
2026-09-16 17:00:41 +02:00

762 lines
26 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 exact_pressed
signal rematch_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(150.0, 108.0)
const TIMER_W := 52.0 ## Breite der Zeitanzeige in der Ansage-Leiste
const INFO_W := 280.0 ## Textbreite im Erklärfeld beim Überfahren
const GRUEN := Color(0.55, 0.84, 0.55) ## überzähliger Würfel beim Aufdecken
const ROT := Color(0.94, 0.55, 0.52) ## fehlender Würfel beim Aufdecken
const Icons = preload("res://scripts/icons.gd")
## Vorrat, Topf, Erklärung, Verlauf, Protokoll - ausgelagert nach hud_panels.gd.
var panels := preload("res://scripts/hud_panels.gd").new(self)
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 _actual := 0 ## beim Aufdecken: tatsächlich gefundene Würfel
var face_buttons: Array[Button] = []
var bid_btn: Button
var challenge_btn: Button
var exact_btn: Button
var verlauf_box: VBoxContainer
var _verlauf_sig := "" ## Abbild des Gebotsverlaufs, gegen unnötigen Neuaufbau
var action_panel: PanelContainer
var hand_panel: PanelContainer
var hand_box: HBoxContainer
var boost_slot: CenterContainer
var pause_slot: Control
var over_slot: Control
var over_title: Label
var over_again: Button
var pause_hint: Label
var boost_panel: PanelContainer
var boost_cards: HBoxContainer
var boost_title: Label
var turn_label: Label
var info_panel: PanelContainer
var info_name: Label
var info_desc: Label
var info_stark: Label
var wurf_slot: CenterContainer
var wurf_label: Label
var log_box: VBoxContainer
var _log_sig := ""
## Auf dem Handy gibt es kein Überfahren: der erste Tipp zeigt die Erklärung,
## der zweite wirft. Am Rechner bleibt alles beim Klick.
var _touch := DisplayServer.is_touchscreen_available()
var _tipp_id := "" ## Münze, deren Erklärung nach dem ersten Tipp offen ist
var timer_bid: Label
var timer_boost: Label
var target_row: HBoxContainer
var _face := 1
var _deadline := 0.0 ## lokaler Zeitstempel, 0 = keine Frist
var _phase := ""
var _my_turn := false
var _pending_boost := ""
var _letzter_wurf := 0 ## Nummer des zuletzt gezeigten Münzwurfs
var _info_ctrl: Control ## worauf die Maus gerade liegt
var _zug_box := StyleBoxFlat.new() ## Hinterlegung der Zeile, die dran ist
var _leer_box := StyleBoxEmpty.new()
var _hand_sig := "" ## Abbild des Vorrats, gegen unnötigen Neuaufbau
var _mitte_sig := "" ## dasselbe für die Liste der gelegten Münzen
var _wurf_tween: Tween
var _my_values: Array = []
var _my_seat := 0
func _ready() -> void:
_zug_box.bg_color = Color(0.30, 0.24, 0.07, 0.85)
_zug_box.set_corner_radius_all(6)
_zug_box.border_color = ACCENT
_zug_box.set_border_width_all(1)
_zug_box.set_content_margin_all(5)
_leer_box.set_content_margin_all(5)
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)
var rechts := VBoxContainer.new()
rechts.add_theme_constant_override("separation", 10)
pl.add_child(rechts)
players_box = VBoxContainer.new()
players_box.add_theme_constant_override("separation", 6)
rechts.add_child(players_box)
# Der Gebotsverlauf der Runde. Bluffs liest man daran, wer wie schnell
# hochgegangen ist - nicht am letzten Gebot allein.
verlauf_box = VBoxContainer.new()
verlauf_box.add_theme_constant_override("separation", 2)
rechts.add_child(verlauf_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("", 21, 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)
# --- Wer ist dran: direkt über der Ansageleiste, wo der Blick ohnehin liegt
turn_label = _label("", 19, ACCENT)
turn_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
turn_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
_place(turn_label, 0, 1, 1, 1, 16, -146, -16, -114)
root.add_child(turn_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 := Icons.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)
exact_btn = Button.new()
exact_btn.text = "Genau"
exact_btn.tooltip_text = "Die Ansage trifft exakt. Stimmt es, verlieren alle anderen einen Würfel."
exact_btn.custom_minimum_size = Vector2(96, 42)
exact_btn.pressed.connect(_on_exact)
av.add_child(exact_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)
# --- Münzvorrat unten links, über der Ansageleiste
hand_panel = _panel()
_place(hand_panel, 0, 1, 0, 1, 16, -300, 16, -124)
root.add_child(hand_panel)
var hv := VBoxContainer.new()
hv.add_theme_constant_override("separation", 4)
hand_panel.add_child(hv)
hv.add_child(_label("Münzen", 13, DIM))
hand_box = HBoxContainer.new()
hand_box.add_theme_constant_override("separation", 6)
hv.add_child(hand_box)
hand_panel.visible = false
# --- Booster-Overlay
# --- Wurfansage, links unter dem Topf. Die Münze selbst fällt in 3D auf
# den Tisch; hier steht nur, wer was geworfen hat und wie es fiel.
wurf_slot = _slot(0, 0, 0, 0, 16, 100, 336, 200)
wurf_slot.visible = false
wurf_label = _label("", 16, ACCENT)
wurf_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
wurf_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
wurf_slot.add_child(wurf_label)
# --- Protokoll unten rechts: was der Topf tut, wer geht, wer zurückkommt.
var lp := _panel()
# Am unteren Rand verankert und nach oben wachsend - so gross wie der Text.
_place(lp, 1, 1, 1, 1, -376, -124, -16, -124)
lp.grow_horizontal = Control.GROW_DIRECTION_BEGIN
lp.grow_vertical = Control.GROW_DIRECTION_BEGIN
root.add_child(lp)
log_box = VBoxContainer.new()
log_box.add_theme_constant_override("separation", 2)
lp.add_child(log_box)
# --- Erklärung beim Überfahren. Ein eigenes Feld statt Tooltip: das kommt
# sofort und verschwindet nicht, wenn die Runde weiterläuft.
info_panel = _panel(Color(0.05, 0.06, 0.09, 0.97))
info_panel.visible = false
info_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
root.add_child(info_panel)
var iv := VBoxContainer.new()
iv.add_theme_constant_override("separation", 4)
info_panel.add_child(iv)
info_name = _label("", 16, ACCENT)
iv.add_child(info_name)
info_desc = _label("", 14, Color(0.82, 0.84, 0.90))
info_desc.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
info_desc.custom_minimum_size = Vector2(INFO_W, 0)
iv.add_child(info_desc)
info_stark = _label("", 14, ACCENT)
info_stark.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
info_stark.custom_minimum_size = Vector2(INFO_W, 0)
iv.add_child(info_stark)
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 eine Münze", 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)
# --- Spielende: wer gewonnen hat, und wie es weitergeht
over_slot = Control.new()
over_slot.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
over_slot.visible = false
root.add_child(over_slot)
var oc := CenterContainer.new()
oc.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
over_slot.add_child(oc)
var op := _panel(Color(0.05, 0.06, 0.09, 0.98))
oc.add_child(op)
var ov := VBoxContainer.new()
ov.custom_minimum_size = Vector2(300, 0)
ov.add_theme_constant_override("separation", 10)
op.add_child(ov)
over_title = _label("", 26, ACCENT)
over_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
ov.add_child(over_title)
over_again = Button.new()
over_again.text = "Nochmal"
over_again.custom_minimum_size = Vector2(0, 42)
over_again.pressed.connect(func() -> void: rematch_pressed.emit())
ov.add_child(over_again)
var menue := Button.new()
menue.text = "Zum Menü"
menue.custom_minimum_size = Vector2(0, 42)
menue.pressed.connect(_on_quit)
ov.add_child(menue)
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:
return true
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 _bid_face < 6:
# Gleiche Anzahl reicht, wenn die Augenzahl steigt.
_set_count(_bid_count)
_on_face(_bid_face + 1)
else:
_set_count(_bid_count + 1)
_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 _on_exact() -> void:
exact_pressed.emit()
## Tastatur: Esc Pause, 1-6 Augenzahl, +/- Anzahl, Enter Ansagen,
## A Anfechten, G Genau. Die Knöpfe entscheiden, was gerade erlaubt ist -
## eine Taste drückt nur, was auch klickbar wäre.
func _unhandled_key_input(event: InputEvent) -> void:
if not (event is InputEventKey and event.pressed and not event.echo):
return
var k: Key = event.keycode
if k == KEY_ESCAPE:
_set_paused(not pause_slot.visible)
elif k >= KEY_1 and k <= KEY_6:
var f := int(k - KEY_1) + 1
if _my_turn and _face_allowed(f):
_on_face(f)
elif k == KEY_PLUS or k == KEY_KP_ADD or k == KEY_UP:
if not plus_btn.disabled:
_step_count(1)
elif k == KEY_MINUS or k == KEY_KP_SUBTRACT or k == KEY_DOWN:
if not minus_btn.disabled:
_step_count(-1)
elif k == KEY_ENTER or k == KEY_KP_ENTER:
if not bid_btn.disabled:
_on_bid()
elif k == KEY_A:
if not challenge_btn.disabled:
_on_challenge()
elif k == KEY_G:
if not exact_btn.disabled:
_on_exact()
else:
return
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)
# Wer dran ist, bekommt eine hinterlegte Zeile. Ein Pfeil und etwas
# Farbe gehen in der Liste unter.
var zeile := PanelContainer.new()
zeile.add_theme_stylebox_override("panel", _zug_box if turn else _leer_box)
players_box.add_child(zeile)
var entry := VBoxContainer.new()
entry.add_theme_constant_override("separation", 2)
zeile.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(Icons.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)
# Beim Aufdecken bleibt die Ansage stehen, wie sie war: die angesagten
# Würfel normal, die fehlenden davon rot. Gab es mehr als angesagt,
# kommen die überzähligen grün dahinter.
var faerben: bool = st.phase == "reveal" and _actual >= 0
var angesagt := int(st.bid.count)
var gezeigt := maxi(angesagt, _actual) if faerben else angesagt
var px := _fit_dice(gezeigt)
for i in gezeigt:
var bg := Color.TRANSPARENT
if faerben:
if i >= angesagt:
bg = GRUEN
elif i >= _actual:
bg = ROT
bid_row.add_child(Icons.DieIcon.new(int(st.bid.face), px, bg))
panels._zeichne_hand(st)
panels._zeichne_mitte(st, my_seat)
panels._zeichne_verlauf(st, my_seat)
panels._zeichne_log(st)
# Am Ende steht der Sieger im Endbildschirm; das Banner darüber wäre doppelt.
banner_label.text = "" if st.phase == "over" else st.banner
if st.phase == "over" and not over_slot.visible:
over_title.text = String(st.banner)
# Nochmal: solo immer, im Netz nur für den Raumbesitzer - der Server
# setzt den Raum dann für alle neu auf.
over_again.visible = Net.room_code == Net.SOLO or Net.is_owner
over_slot.visible = true
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
if st.phase == "bid" and int(st.turn) < st.seats.size():
var wer := "%s ist dran" % _short(String(st.seats[st.turn].name))
if my_turn:
turn_label.text = "Du bist dran"
elif not st.seats[my_seat].alive:
turn_label.text = "Du bist raus · " + wer
else:
turn_label.text = wer
turn_label.add_theme_color_override("font_color", ACCENT if my_turn else DIM)
else:
turn_label.text = ""
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))
_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 + 1 <= _count_max or _bid_face < 6
challenge_btn.disabled = not my_turn or st.bid.is_empty() \
or (st.must_bid and gebot_moeglich)
exact_btn.disabled = challenge_btn.disabled
_count_min = clampi(maxi(1, _bid_count), 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)
# Läuft die Ansagezeit ab, während die Würfelauswahl offen steht, wird
# automatisch angesagt - die Münze bleibt dann ungeworfen liegen.
timer_boost.text = "Noch %s bis zur Ansage" % 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
for paar: Array in peek.get(seat, []):
var idx := int(paar[0])
if idx >= 0 and idx < out.size():
out[idx] = int(paar[1])
return out
func set_banner(text: String) -> void:
banner_label.text = text
# ---------------------------------------------------------------- Booster
func _pick(id: String) -> void:
# Handy: erster Tipp öffnet nur die Erklärung (die kommt über den
# nachgeahmten Mauszeiger von selbst), der zweite wirft wirklich.
if _touch and _tipp_id != id:
_tipp_id = id
return
_tipp_id = ""
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)
boost_slot.visible = true
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 := Icons.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