1128 lines
40 KiB
GDScript
1128 lines
40 KiB
GDScript
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
|
||
|
||
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
|
||
|
||
|
||
## Gezeichnete Münze. Bilder als Dateien gibt es im Projekt nicht; alles wird
|
||
## zur Laufzeit gemalt, so wie die Würfel auch. Vorn das Sinnbild der Wirkung
|
||
## (Wappen), hinten die glatte Zahlseite.
|
||
class MuenzIcon extends Control:
|
||
const RAND := Color(0.40, 0.29, 0.11) ## Prägekante
|
||
const FLAECHE := Color(0.78, 0.60, 0.26) ## Messing
|
||
const GLANZ := Color(0.91, 0.75, 0.39) ## Lichtkante
|
||
const GRAVUR := Color(0.19, 0.13, 0.04) ## eingeschlagenes Zeichen
|
||
|
||
var id := ""
|
||
var farbe := GRAVUR ## Zeichenfarbe, von den Zeichenhilfen benutzt
|
||
var kind_col := Color.WHITE
|
||
var seite := 0 ## 0 = Wappen (Sinnbild), 1 = Zahl
|
||
|
||
func _init(muenze: String, col: Color, px: float) -> void:
|
||
id = muenze
|
||
kind_col = col
|
||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
custom_minimum_size = Vector2(px, px)
|
||
size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||
pivot_offset = Vector2(px, px) * 0.5
|
||
|
||
func _draw() -> void:
|
||
var s := minf(size.x, size.y)
|
||
var m := size * 0.5
|
||
var r := s * 0.5
|
||
draw_circle(m, r, RAND)
|
||
draw_circle(m - Vector2(r, r) * 0.05, r * 0.95, GLANZ)
|
||
draw_circle(m, r * 0.89, FLAECHE)
|
||
# Der Farbring sagt, ob die Münze dir hilft oder den anderen schadet.
|
||
draw_arc(m, r * 0.79, 0, TAU, 40, kind_col, maxf(1.0, s * 0.05))
|
||
if seite == 1:
|
||
draw_arc(m, r * 0.52, 0, TAU, 32, GRAVUR, maxf(1.0, s * 0.04))
|
||
draw_circle(m, s * 0.09, GRAVUR)
|
||
return
|
||
_zeichen(m, s * 0.74)
|
||
|
||
func _zeichen(m: Vector2, s: float) -> void:
|
||
match id:
|
||
"peek":
|
||
_auge(m, s)
|
||
"count":
|
||
_striche(m, s)
|
||
"set_face":
|
||
_schiefer_wuerfel(m, s * 0.50)
|
||
"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)
|
||
"bid_up":
|
||
_pfeil(m, s * 0.52, true)
|
||
"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)
|
||
|
||
## Vier Striche und einer quer - eine Strichliste, das Zeichen fürs Zählen.
|
||
func _striche(mitte: Vector2, s: float) -> void:
|
||
var w := maxf(1.5, s * 0.07)
|
||
for i in 4:
|
||
var x: float = mitte.x + (float(i) - 1.5) * s * 0.16
|
||
draw_line(Vector2(x, mitte.y - s * 0.26), Vector2(x, mitte.y + s * 0.26), farbe, w)
|
||
draw_line(mitte + Vector2(-s * 0.32, s * 0.20), mitte + Vector2(s * 0.32, -s * 0.20), farbe, w)
|
||
|
||
## Ein gekippter Würfel mit einem Auge - der wird gerade umgedreht.
|
||
func _schiefer_wuerfel(mitte: Vector2, kante: float) -> void:
|
||
var h := kante * 0.5
|
||
var ecken := PackedVector2Array()
|
||
for e: Vector2 in [Vector2(-h, -h), Vector2(h, -h), Vector2(h, h), Vector2(-h, h)]:
|
||
ecken.append(mitte + e.rotated(0.35))
|
||
draw_colored_polygon(ecken, farbe)
|
||
draw_circle(mitte, kante * 0.11, FLAECHE)
|
||
|
||
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, FLAECHE)
|
||
|
||
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)
|
||
|
||
|
||
## 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 _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 := 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()
|
||
|
||
|
||
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)
|
||
# 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(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(DieIcon.new(int(st.bid.face), px, bg))
|
||
|
||
_zeichne_hand(st)
|
||
_zeichne_mitte(st, my_seat)
|
||
_zeichne_verlauf(st, my_seat)
|
||
_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
|
||
|
||
## 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 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 = _my_seat < st.seats.size() and st.seats[_my_seat].alive
|
||
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) == _my_seat
|
||
var gebot: bool = not st.bid.is_empty()
|
||
# 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" % [muenzen, spielbar, gebot]
|
||
if sig == _hand_sig:
|
||
return
|
||
_hand_sig = sig
|
||
_clear(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)
|
||
# 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(_pick.bind(id))
|
||
hand_box.add_child(btn)
|
||
_info_an(btn, id, false, 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 := MuenzIcon.new(id, col, 72.0)
|
||
ic.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||
v.add_child(ic)
|
||
|
||
var name_l := _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 == _mitte_sig:
|
||
return
|
||
_mitte_sig = sig
|
||
_clear(boosts_box)
|
||
|
||
boosts_box.add_child(_label("Topf" if not boosts.is_empty() else "Topf: leer", 13, 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
|
||
boosts_box.add_child(line)
|
||
_info_an(line, id, true)
|
||
line.add_child(MuenzIcon.new(id, _kind_col(b), 22.0))
|
||
line.add_child(_label(String(b.get("name", "")), 14, _kind_col(b)))
|
||
if bool(m.get("stark", false)):
|
||
line.add_child(_label("· Wappen", 14, ACCENT))
|
||
if wer < st.seats.size():
|
||
line.add_child(_label("von " + _short(String(st.seats[wer].name)), 13,
|
||
ACCENT if wer == my_seat else 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)
|
||
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(_label(text, 14, ACCENT))
|
||
line.add_child(DieIcon.new(int(f), 18.0))
|
||
if st.must_bid:
|
||
boosts_box.add_child(_label("Maulkorb: du musst bieten", 14, 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 == _verlauf_sig:
|
||
return
|
||
_verlauf_sig = sig
|
||
_clear(verlauf_box)
|
||
if verlauf.is_empty():
|
||
return
|
||
verlauf_box.add_child(_label("Verlauf", 13, 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 := 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)
|
||
verlauf_box.add_child(line)
|
||
var nm := _label(_short(String(st.seats[wer].name)), 13, col)
|
||
nm.custom_minimum_size = Vector2(64, 0)
|
||
line.add_child(nm)
|
||
line.add_child(_label("%d ×" % int(e.get("count", 0)), 13, col))
|
||
line.add_child(DieIcon.new(int(e.get("face", 1)), 16.0))
|
||
if bool(e.get("erhoeht", false)):
|
||
line.add_child(_label("erhöht", 12, 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
|
||
_info_ctrl = ctrl
|
||
info_panel.visible = false
|
||
info_name.text = String(b.name)
|
||
info_desc.text = String(b.desc)
|
||
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 get_tree().process_frame
|
||
if _info_ctrl != ctrl or not is_instance_valid(ctrl) or not is_instance_valid(anker):
|
||
return
|
||
info_panel.reset_size()
|
||
var gr := info_panel.size
|
||
var r := 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
|
||
info_panel.position = Vector2(
|
||
clampf(r.position.x, 8.0, maxf(8.0, root.size.x - gr.x - 8.0)),
|
||
clampf(y, 8.0, maxf(8.0, root.size.y - gr.y - 8.0)))
|
||
info_panel.visible = true
|
||
|
||
|
||
func _info_weg(ctrl: Control) -> void:
|
||
if _info_ctrl != ctrl:
|
||
return
|
||
_info_ctrl = null
|
||
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:
|
||
_letzter_wurf = 0
|
||
return
|
||
if nr <= _letzter_wurf:
|
||
return
|
||
_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)),
|
||
_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
|
||
wurf_label.text = "%s wirft %s\n%s" % [wer, b.name,
|
||
"Wappen - " + String(b.get("stark", "")) if stark else "Zahl - Grundwirkung"]
|
||
wurf_label.add_theme_color_override("font_color", ACCENT if stark else DIM)
|
||
wurf_slot.visible = true
|
||
Klang.muenze()
|
||
if _wurf_tween != null and _wurf_tween.is_valid():
|
||
_wurf_tween.kill()
|
||
_wurf_tween = create_tween()
|
||
_wurf_tween.tween_interval(2.6)
|
||
_wurf_tween.tween_callback(func() -> void: 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 == _log_sig:
|
||
return
|
||
_log_sig = sig
|
||
_clear(log_box)
|
||
var ab := maxi(0, zeilen.size() - 5)
|
||
for i in range(ab, zeilen.size()):
|
||
var l := _label(String(zeilen[i]), 13, ACCENT if i == zeilen.size() - 1 else DIM)
|
||
l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
l.custom_minimum_size = Vector2(340, 0)
|
||
log_box.add_child(l)
|
||
|
||
|
||
|
||
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 := 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
|