Das Dreieck fuer Buff und Debuff sagte nichts, was die Farbe nicht schon sagt - gruen fuer Buff, rot fuer Debuff. Die Ansage in der Tischmitte war so gross, dass sie bei vier Plaetzen auf den Bechern der Nachbarn lag. Schrift kleiner und das Feld enger, damit die Wuerfel frueher umbrechen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
791 lines
26 KiB
GDScript
791 lines
26 KiB
GDScript
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(150.0, 108.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)
|
||
|
||
|
||
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("", 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)
|
||
|
||
# --- 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 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
|