Files
pirate-dice/scripts/hud.gd
team3 3619f45bd8 Handkarten als Karten zeichnen statt als Knopfreihe
Auf der Hand standen nur die Namen. Was eine Karte tut, stand allein im
Tooltip - man musste jede einzeln anfahren.

Jetzt ist jede Karte eine Karte: gezeichnetes Sinnbild, Name, Wirkungstext
und ein Dreieck fuer Buff (gruen, hoch) oder Debuff (rot, runter).

Die Sinnbilder werden zur Laufzeit gemalt, wie die Wuerfel auch - Bilddateien
gibt es im Projekt nicht. Je Karte ein eigenes: Wuerfel, Kreispfeil, Auge,
Wolke, Pfeile, durchgestrichener Kreis.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 16:47:30 +02:00

815 lines
27 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) ## überzähliger 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 hand_panel: PanelContainer
var hand_box: HBoxContainer
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.
## Gezeichnetes Sinnbild je Karte. Bilder als Dateien gibt es im Projekt nicht;
## alles wird zur Laufzeit gemalt, so wie die Würfel auch.
class KartenIcon extends Control:
var id := ""
var farbe := Color.WHITE
func _init(karte: String, col: Color, px: float) -> void:
id = karte
farbe = col
mouse_filter = Control.MOUSE_FILTER_IGNORE
custom_minimum_size = Vector2(px, px)
size_flags_vertical = Control.SIZE_SHRINK_CENTER
func _draw() -> void:
var s := minf(size.x, size.y)
var m := size * 0.5
match id:
"reroll_all":
_wuerfel(m + Vector2(-s * 0.16, -s * 0.06), s * 0.42, 5)
_wuerfel(m + Vector2(s * 0.18, s * 0.10), s * 0.42, 2)
"reroll_one":
_wuerfel(m + Vector2(0, s * 0.10), s * 0.38, 3)
_bogen(m + Vector2(0, s * 0.10), s * 0.34)
"set_six":
_wuerfel(m, s * 0.54, 6)
"gain_die":
_wuerfel(m + Vector2(-s * 0.10, 0), s * 0.46, 4)
_plus(m + Vector2(s * 0.26, -s * 0.20), s * 0.20)
"cancel":
draw_arc(m, s * 0.30, 0, TAU, 32, farbe, s * 0.08)
draw_line(m + Vector2(-s * 0.21, -s * 0.21), m + Vector2(s * 0.21, s * 0.21),
farbe, s * 0.08)
"peek":
_auge(m, s)
"bid_up":
_pfeil(m, s * 0.52, true)
"raise_min":
_pfeil(m + Vector2(-s * 0.14, 0), s * 0.44, true)
_pfeil(m + Vector2(s * 0.14, 0), s * 0.44, true)
"enemy_lower":
_pfeil(m, s * 0.52, false)
"fog":
draw_circle(m + Vector2(-s * 0.16, s * 0.06), s * 0.16, farbe)
draw_circle(m + Vector2(s * 0.16, s * 0.06), s * 0.16, farbe)
draw_circle(m + Vector2(0, -s * 0.06), s * 0.20, farbe)
"gag":
var r := Rect2(m - Vector2(s * 0.28, s * 0.16), Vector2(s * 0.56, s * 0.32))
draw_rect(r, farbe, false, s * 0.07)
draw_line(m + Vector2(-s * 0.28, s * 0.22), m + Vector2(s * 0.28, -s * 0.22),
farbe, s * 0.08)
_:
draw_circle(m, s * 0.26, farbe)
func _wuerfel(mitte: Vector2, kante: float, augen: int) -> void:
var r := Rect2(mitte - Vector2(kante, kante) * 0.5, Vector2(kante, kante))
draw_rect(r, farbe, true)
var punkt := kante * 0.10
var weg := kante * 0.26
for p: Vector2 in DieIcon.PIPS[augen]:
draw_circle(mitte + p * weg, punkt, Color(0.08, 0.09, 0.12))
## Offener Kreisbogen mit Spitze - das Zeichen fürs Neuwerfen.
func _bogen(mitte: Vector2, r: float) -> void:
draw_arc(mitte, r, PI * 0.85, TAU * 0.92, 28, farbe, maxf(1.5, r * 0.20))
var spitze := mitte + Vector2(cos(TAU * 0.92), sin(TAU * 0.92)) * r
draw_colored_polygon([spitze + Vector2(-r * 0.30, -r * 0.10),
spitze + Vector2(r * 0.24, -r * 0.16), spitze + Vector2(r * 0.02, r * 0.28)], farbe)
func _plus(mitte: Vector2, r: float) -> void:
draw_rect(Rect2(mitte - Vector2(r, r * 0.28), Vector2(r * 2, r * 0.56)), farbe, true)
draw_rect(Rect2(mitte - Vector2(r * 0.28, r), Vector2(r * 0.56, r * 2)), farbe, true)
## Der Schaft sitzt hinter der Spitze, sonst überlagern sich beide zu
## einem Klecks.
func _pfeil(mitte: Vector2, h: float, hoch: bool) -> void:
var d := -1.0 if hoch else 1.0
var schaft_y: float = mitte.y - d * h * 0.48
var oben: float = minf(mitte.y, schaft_y)
draw_rect(Rect2(Vector2(mitte.x - h * 0.13, oben),
Vector2(h * 0.26, absf(mitte.y - schaft_y))), farbe, true)
draw_colored_polygon([mitte + Vector2(0, d * h * 0.50),
mitte + Vector2(-h * 0.34, d * h * 0.02),
mitte + Vector2(h * 0.34, d * h * 0.02)], farbe)
func _auge(mitte: Vector2, s: float) -> void:
var punkte := PackedVector2Array()
for i in 17:
var t := -1.0 + 2.0 * i / 16.0
punkte.append(mitte + Vector2(t * s * 0.34, -sqrt(maxf(0.0, 1.0 - t * t)) * s * 0.20))
for i in 17:
var t := 1.0 - 2.0 * i / 16.0
punkte.append(mitte + Vector2(t * s * 0.34, sqrt(maxf(0.0, 1.0 - t * t)) * s * 0.20))
draw_polyline(punkte, farbe, s * 0.05, true)
draw_circle(mitte, s * 0.11, farbe)
## Kleines Zeichen für Buff oder Debuff: Dreieck hoch oder runter.
class ArtIcon extends Control:
var buff := true
func _init(ist_buff: bool, px: float) -> void:
buff = ist_buff
mouse_filter = Control.MOUSE_FILTER_IGNORE
custom_minimum_size = Vector2(px, px)
size_flags_vertical = Control.SIZE_SHRINK_CENTER
func _draw() -> void:
var s := minf(size.x, size.y)
var m := size * 0.5
var col := Color(0.5, 0.9, 0.6) if buff else Color(0.95, 0.5, 0.5)
var d := -1.0 if buff else 1.0
draw_colored_polygon([m + Vector2(0, d * s * 0.34),
m + Vector2(-s * 0.34, -d * s * 0.24), m + Vector2(s * 0.34, -d * s * 0.24)], col)
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)
# --- Handkarten 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("Karten", 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
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)
# 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"
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(DieIcon.new(int(st.bid.face), px, bg))
_zeichne_hand(st)
_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 Karte gespielt", 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)
# Läuft die Ansagezeit ab, während die Würfelauswahl offen steht, wird
# automatisch angesagt - die Karte bleibt dann ungespielt auf der Hand.
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
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
## Die Hand liegt offen unten links. Ein Klick spielt die Karte sofort;
## nur Karten mit eigenem Ziel fragen noch nach dem Würfel.
func _zeichne_hand(st: Dictionary) -> void:
if hand_box == null:
return
var karten: Array = st.get("hand", [])
hand_panel.visible = not karten.is_empty()
_clear(hand_box)
var spielbar: bool = st.phase == "bid"
for id: String in karten:
var b: Dictionary = BoosterDB.get_booster(id)
if b.is_empty():
continue
var buff: bool = b.kind == BoosterDB.Kind.BUFF
var col := Color(0.5, 0.9, 0.6) if buff else Color(0.95, 0.5, 0.5)
var btn := Button.new()
btn.custom_minimum_size = Vector2(168, 132)
btn.tooltip_text = String(b.desc)
btn.disabled = not spielbar
btn.pressed.connect(_pick.bind(id))
hand_box.add_child(btn)
var v := VBoxContainer.new()
v.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
v.offset_left = 9
v.offset_top = 7
v.offset_right = -9
v.offset_bottom = -7
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
v.add_theme_constant_override("separation", 2)
btn.add_child(v)
var kopf := HBoxContainer.new()
kopf.mouse_filter = Control.MOUSE_FILTER_IGNORE
v.add_child(kopf)
kopf.add_child(KartenIcon.new(id, col, 34.0))
var luecke := Control.new()
luecke.size_flags_horizontal = Control.SIZE_EXPAND_FILL
luecke.mouse_filter = Control.MOUSE_FILTER_IGNORE
kopf.add_child(luecke)
kopf.add_child(ArtIcon.new(buff, 16.0))
var name_l := _label(String(b.name), 15, col)
name_l.mouse_filter = Control.MOUSE_FILTER_IGNORE
v.add_child(name_l)
var d := _label(String(b.desc), 11, Color(0.78, 0.80, 0.86))
d.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
d.mouse_filter = Control.MOUSE_FILTER_IGNORE
d.size_flags_vertical = Control.SIZE_EXPAND_FILL
v.add_child(d)
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)
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 := 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