diff --git a/README.md b/README.md index 45c145e..2926bf4 100644 --- a/README.md +++ b/README.md @@ -136,44 +136,52 @@ Klassisches Liar's Dice ohne Joker. Jeder startet mit 5 Würfeln und sieht nur die eigenen. 1. **Würfeln** – alle Würfel fallen physikalisch in den eigenen Becher. -2. **Boost wählen** – jeder bekommt 3 zufällige Booster angeboten und wählt - einen; er wirkt nur für die laufende Runde. -3. **Ansagen** – reihum wird ein Gebot "Anzahl x Augenzahl" über alle Würfel +2. **Ansagen** – reihum wird ein Gebot "Anzahl x Augenzahl" über alle Würfel am Tisch gemacht. Jedes Gebot muss höher sein: mehr Würfel, oder gleich viele bei höherer Augenzahl. -4. **Anfechten** – statt zu erhöhen kann man das Vorgebot anzweifeln. Alle +3. **Anfechten** – statt zu erhöhen kann man das Vorgebot anzweifeln. Alle Becher werden gehoben und gezählt: Stimmt das Gebot, verliert der Anfechter einen Würfel, sonst der Bieter. -Für Boosterwahl und Ansage hat man jeweils **30 Sekunden**; die Restzeit steht -im Booster-Fenster beziehungsweise neben „Deine Ansage" und wird unter zehn -Sekunden rot. Läuft die Zeit ab, entscheidet der Host automatisch – beim Booster -zufällig, beim Gebot mit der Bot-Logik. Ohne dieses Limit würde ein abwesender -oder getrennter Mitspieler den ganzen Tisch dauerhaft blockieren. +Für die Ansage hat man **30 Sekunden**; die Restzeit steht neben „Deine Ansage" +und wird unter zehn Sekunden rot. Läuft die Zeit ab, entscheidet der Host mit +der Bot-Logik. Ohne dieses Limit würde ein abwesender oder getrennter +Mitspieler den ganzen Tisch dauerhaft blockieren. Wer keine Würfel mehr hat, scheidet aus. Der Letzte gewinnt. -## Booster +## Karten -| Booster | Art | Wirkung | +Karten ersetzen die frühere Booster-Phase. Die kostete jede Runde eine eigene +Wartezeit und riss den Ablauf auseinander. + +* Wer einen Würfel abgibt, **zieht eine Karte**. Das ist der einzige Nachschub, + also bekommt der etwas zurück, der gerade hinten liegt. +* Gespielt wird **jederzeit während der Ansagen**, nicht nur am eigenen Zug. +* Mehr als **drei** Karten passen nicht auf die Hand. +* Die Hand liegt offen unten links; ein Klick spielt die Karte sofort. Nur + *Feinschliff* und *Glücksgriff* fragen noch, welcher Würfel gemeint ist. + +| Karte | Art | Wirkung | |---|---|---| | Neuwurf | Buff | Alle eigenen Würfel neu werfen | | Feinschliff | Buff | Einen gewählten eigenen Würfel neu werfen | | Glücksgriff | Buff | Ein gewählter Würfel wird zur 6 | -| Zwilling | Buff | Ein anderer eigener Würfel nimmt den Wert des gewählten an | +| Nachschub | Buff | Du bekommst einen Würfel dazu | +| Widerruf | Buff | Hebt Nebel, Störfeuer und Maulkorb auf, die auf dir liegen | | Spähen | Buff | Von jedem Gegner wird ein zufälliger Würfel aufgedeckt | -| Rütteln | Debuff | Jeder Gegner wirft einen zufälligen Würfel neu | +| Erhöhen | Debuff | Die laufende Ansage steigt um 1 | | Absenken | Debuff | Bei jedem Gegner sinkt ein zufälliger Würfel um 1 | | Nebel | Debuff | Jeder Gegner sieht einen eigenen Würfel nicht mehr | | Störfeuer | Debuff | Gegner müssen diese Runde um mindestens 2 erhöhen | -| Maulkorb | Debuff | Der nächste Spieler muss bieten und darf nicht anfechten | +| Maulkorb | Debuff | Wer gerade dran ist, muss bieten und darf nicht anfechten | ## Aufbau | Datei | Inhalt | |---|---| | `scripts/net.gd` | Autoload: WebSocket-Verbindung, Räume, Lobby, Spielstart | -| `scripts/boosters.gd` | Autoload: Booster-Katalog | +| `scripts/boosters.gd` | Autoload: Kartenkatalog | | `scripts/game.gd` | Spiellogik (host-autoritativ), RPCs, Zustandsabgleich | | `scripts/table.gd` | Aufbau der 3D-Szene: Tisch, Würfelzonen, Becher, Kamera | | `scripts/die.gd` | Physik-Würfel, Ablesen der Augenzahl aus der Ruhelage | diff --git a/scripts/ai.gd b/scripts/ai.gd index 3bfd36b..5c21624 100644 --- a/scripts/ai.gd +++ b/scripts/ai.gd @@ -102,38 +102,12 @@ static func _is_higher(count: int, face: int, bid: Dictionary, min_raise: int) - return count == bid.count and face > bid.face -static func choose_booster(offer: Array, values: Array, dice_left: int, rng: RandomNumberGenerator) -> Dictionary: - var scores: Array = [] - for id in offer: - var b: Dictionary = BoosterDB.get_booster(id) - var s := rng.randf() - if b.kind == BoosterDB.Kind.BUFF: - s += 0.35 - if dice_left <= 2 and b.kind == BoosterDB.Kind.BUFF: - s += 0.35 - scores.append(s) - var pick := 0 - for i in scores.size(): - if scores[i] > scores[pick]: - pick = i - var id: String = offer[pick] - return {"id": id, "target": _target_for(id, values)} - - -static func _target_for(id: String, values: Array) -> int: +## Welcher eigene Würfel soll dran glauben? Für Karten mit Zielwahl. +static func target_for(id: String, values: Array) -> int: if values.is_empty(): return 0 - if id == "twin": - # Würfel mit dem häufigsten Wert - var counts := {} - for v in values: - counts[v] = counts.get(v, 0) + 1 - var best_v: int = values[0] - for v: int in counts: - if counts[v] > counts.get(best_v, 0): - best_v = v - return values.find(best_v) - # sonst: schwächster Würfel + # Der schwächste Würfel: den neu zu werfen oder zur 6 zu machen bringt + # am meisten. var idx := 0 for i in values.size(): if values[i] < values[idx]: diff --git a/scripts/boosters.gd b/scripts/boosters.gd index dc97bae..6f58d63 100644 --- a/scripts/boosters.gd +++ b/scripts/boosters.gd @@ -1,9 +1,15 @@ extends Node -## Katalog aller Booster. Jede Runde bekommt jeder Spieler 3 davon -## zufällig angeboten und wählt genau einen aus. +## Katalog aller Karten. +## +## Karten gibt es nicht mehr rundenweise zur Auswahl. Wer einen Würfel +## verliert, zieht eine. Gespielt wird jederzeit während der Ansagen - deshalb +## wirkt jede Karte sofort und braucht keine eigene Phase mehr. enum Kind { BUFF, DEBUFF } +## Mehr als das passt nicht auf die Hand; sonst hortet man bis zum Schluss. +const HANDLIMIT := 3 + const ALL := [ { "id": "reroll_all", "kind": Kind.BUFF, "target": false, @@ -21,9 +27,14 @@ const ALL := [ "desc": "Ein gewählter Würfel wird zur 6.", }, { - "id": "twin", "kind": Kind.BUFF, "target": true, - "name": "Zwilling", - "desc": "Ein anderer deiner Würfel nimmt den Wert des gewählten an.", + "id": "gain_die", "kind": Kind.BUFF, "target": false, + "name": "Nachschub", + "desc": "Du bekommst einen Würfel dazu.", + }, + { + "id": "cancel", "kind": Kind.BUFF, "target": false, + "name": "Widerruf", + "desc": "Hebt Nebel, Störfeuer und Maulkorb auf, die auf dir liegen.", }, { "id": "peek", "kind": Kind.BUFF, "target": false, @@ -31,9 +42,9 @@ const ALL := [ "desc": "Von jedem Gegner wird dir ein zufälliger Würfel aufgedeckt.", }, { - "id": "enemy_reroll", "kind": Kind.DEBUFF, "target": false, - "name": "Rütteln", - "desc": "Jeder Gegner muss einen zufälligen Würfel neu werfen.", + "id": "bid_up", "kind": Kind.DEBUFF, "target": false, + "name": "Erhöhen", + "desc": "Die laufende Ansage steigt um 1.", }, { "id": "enemy_lower", "kind": Kind.DEBUFF, "target": false, @@ -53,7 +64,7 @@ const ALL := [ { "id": "gag", "kind": Kind.DEBUFF, "target": false, "name": "Maulkorb", - "desc": "Der nächste Spieler muss bieten und darf nicht anfechten.", + "desc": "Wer gerade dran ist, muss bieten und darf nicht anfechten.", }, ] @@ -66,17 +77,9 @@ func get_booster(id: String) -> Dictionary: func needs_target(id: String) -> bool: - var b := get_booster(id) - return b.get("target", false) + return get_booster(id).get("target", false) -func draw_offer(rng: RandomNumberGenerator, amount: int = 3) -> Array: - var pool := ALL.duplicate() - var out: Array = [] - for i in amount: - if pool.is_empty(): - break - var k := rng.randi_range(0, pool.size() - 1) - out.append(pool[k].id) - pool.remove_at(k) - return out +## Eine zufällige Karte, für den, der gerade einen Würfel verloren hat. +func zieh(rng: RandomNumberGenerator) -> String: + return String(ALL[rng.randi_range(0, ALL.size() - 1)].id) diff --git a/scripts/game.gd b/scripts/game.gd index ffeb3eb..0b6c597 100644 --- a/scripts/game.gd +++ b/scripts/game.gd @@ -2,7 +2,6 @@ extends Node3D ## Host-autoritative Spiellogik + Client-Ansicht. const START_DICE := 5 -const BOOST_TIMEOUT := 30.0 const BID_TIMEOUT := 30.0 const REVEAL_PAUSE := 5.0 ## Anzeigedauer des Rundenergebnisses @@ -23,8 +22,9 @@ var fogged: Dictionary = {} ## seat -> Array[int] verdeckter Würfel var peeked: Dictionary = {} ## viewer -> {target_seat: [idx, value]} var min_raise: Dictionary = {} ## seat -> int var must_bid: Dictionary = {} ## seat -> bool -var chosen_boost: Dictionary = {} -var offers: Dictionary = {} ## seat -> die drei angebotenen Booster +var hand: Dictionary = {} ## seat -> Array[String], Karten auf der Hand +var letzte_karte: Dictionary = {} ## seat -> zuletzt gespielte Karte, nur Anzeige +var _karte_laeuft := false ## eine Karte wird gerade aufgelöst var last_bids: Dictionary = {} ## seat -> letzte Ansage dieser Runde var log_lines: Array = [] var phase := "lobby" @@ -78,6 +78,7 @@ func setup(cfg: Dictionary) -> void: hud.pause_changed.connect(_on_pause_changed) for i in seats.size(): + hand[i] = [] table.set_dice_count(i, START_DICE) table.set_dice_visible(i, false) table.set_cup_down(i, i != local_seat) @@ -123,9 +124,6 @@ func _host_loop() -> void: await _phase_roll() # Zwischen den Phasen prüfen: wer während des Wurfs verlässt, darf nicht # noch in die Boosterphase laufen - dort ist das Netzwerk schon weg. - if not _alive(): - return - await _phase_boost() if not _alive(): return var result: Dictionary = await _phase_bid() @@ -201,7 +199,7 @@ func _phase_roll() -> void: peeked.clear() min_raise.clear() must_bid.clear() - chosen_boost.clear() + letzte_karte.clear() banner = "" _log("--- Neue Runde ---") @@ -256,137 +254,106 @@ func _await_settled(list: Array, timeout := 7.0) -> void: return -func _phase_boost() -> void: - phase = "boost" - _answered.clear() - offers.clear() - for i in _alive_seats(): - offers[i] = BoosterDB.draw_offer(rng) - if seats[i].ai: - var pick := DiceAI.choose_booster(offers[i], host_values[i], seats[i].count, rng) - chosen_boost[i] = pick - _answered[i] = true - else: - _send_offer(i, offers[i]) - deadline = _now() + BOOST_TIMEOUT - _push_state() +# ---------------------------------------------------------------- Karten - await _wait_until(_all_boosts_in, BOOST_TIMEOUT) - if not _alive(): +## Wer einen Würfel abgibt, zieht eine Karte. Das ist der einzige Nachschub. +func _zieh_karte(seat: int) -> void: + var karten: Array = hand.get(seat, []) + if karten.size() >= BoosterDB.HANDLIMIT: + _log("%s hat die Hand voll und zieht nicht nach." % seats[seat].name) return + var id := BoosterDB.zieh(rng) + karten.append(id) + hand[seat] = karten + _log("%s zieht %s." % [seats[seat].name, BoosterDB.get_booster(id).name]) - for i in _alive_seats(): - if not chosen_boost.has(i): - var fallback: String = offers[i][rng.randi_range(0, offers[i].size() - 1)] - chosen_boost[i] = {"id": fallback, "target": 0} - await _apply_boosters() + +func _do_play(seat: int, id: String, target: int) -> void: + if seat < 0 or phase != "bid" or not seats[seat].alive: + return + # Zwei Karten gleichzeitig würden sich beim Würfeln in die Quere kommen. + if _karte_laeuft: + return + var karten: Array = hand.get(seat, []) + if not karten.has(id): + return + karten.erase(id) + hand[seat] = karten + letzte_karte[seat] = id + _log("%s spielt %s." % [seats[seat].name, BoosterDB.get_booster(id).name]) + _karte_laeuft = true + await _wirkung(seat, id, target) + _karte_laeuft = false if not _alive(): return _snapshot_all() _push_state() -func _all_boosts_in() -> bool: - for i in _alive_seats(): - if not _answered.has(i) and not seats[i].ai: - return false - return true +func _wirkung(seat: int, id: String, target: int) -> void: + var vals: Array = host_values[seat] + var ziel: int = clampi(target, 0, maxi(0, vals.size() - 1)) + match id: + "reroll_all": + await _neu_werfen(seat, range(vals.size())) + "reroll_one": + await _neu_werfen(seat, [ziel]) + "set_six": + _set_die(seat, ziel, 6) + "gain_die": + seats[seat].count += 1 + table.set_dice_count(seat, seats[seat].count) + host_values[seat].append(1) + await _neu_werfen(seat, [host_values[seat].size() - 1]) + "cancel": + fogged.erase(seat) + min_raise.erase(seat) + must_bid.erase(seat) + "peek": + var info := {} + for o in _alive_seats(): + if o != seat and host_values[o].size() > 0: + var k := rng.randi_range(0, host_values[o].size() - 1) + info[o] = [k, host_values[o][k]] + peeked[seat] = info + "bid_up": + if not bid.is_empty(): + bid.count = int(bid.count) + 1 + last_bids[int(bid.seat)] = {"count": bid.count, "face": bid.face} + "enemy_lower": + for o in _alive_seats(): + if o != seat and host_values[o].size() > 0: + var k := rng.randi_range(0, host_values[o].size() - 1) + _set_die(o, k, maxi(1, host_values[o][k] - 1)) + "fog": + for o in _alive_seats(): + if o != seat and host_values[o].size() > 0: + var k := rng.randi_range(0, host_values[o].size() - 1) + var liste: Array = fogged.get(o, []) + if not liste.has(k): + liste.append(k) + fogged[o] = liste + "raise_min": + for o in _alive_seats(): + if o != seat: + min_raise[o] = 2 + "gag": + must_bid[turn] = true -func _send_offer(seat: int, ids: Array) -> void: - var peer: int = seats[seat].peer - if peer == multiplayer.get_unique_id(): - hud.show_booster_offer(ids, my_values) - else: - _offer.rpc_id(peer, ids) - - -func _apply_boosters() -> void: - var rerolls := {} ## seat -> Array[int] - var order := _alive_seats() - - for seat in order: - var pick: Dictionary = chosen_boost[seat] - var id: String = pick.id - var target: int = clampi(int(pick.get("target", 0)), 0, maxi(0, host_values[seat].size() - 1)) - var b: Dictionary = BoosterDB.get_booster(id) - _log("%s nutzt %s." % [seats[seat].name, b.name]) - - match id: - "reroll_all": - rerolls[seat] = range(host_values[seat].size()) - "reroll_one": - rerolls[seat] = _merge(rerolls.get(seat, []), [target]) - "enemy_reroll": - for o in order: - if o != seat and host_values[o].size() > 0: - var k := rng.randi_range(0, host_values[o].size() - 1) - rerolls[o] = _merge(rerolls.get(o, []), [k]) - "gag": - must_bid[_next_alive(seat)] = true - "raise_min": - for o in order: - if o != seat: - min_raise[o] = 2 - - if not rerolls.is_empty(): - _stream = true - for seat: int in rerolls: - _throw_subset(seat, rerolls[seat]) - await _await_settled(rerolls.keys()) - _stream = false - if not _alive(): - return - for seat: int in rerolls: - var dl: Array = table.dice_of(seat) - for k: int in rerolls[seat]: - if k < dl.size(): - host_values[seat][k] = dl[k].top_value() - table.freeze_dice(seat) - - for seat in order: - var pick: Dictionary = chosen_boost[seat] - var id: String = pick.id - var vals: Array = host_values[seat] - var target: int = clampi(int(pick.get("target", 0)), 0, maxi(0, vals.size() - 1)) - match id: - "set_six": - _set_die(seat, target, 6) - "twin": - if vals.size() > 1: - var others: Array = [] - for k in vals.size(): - if k != target: - others.append(k) - _set_die(seat, others[rng.randi_range(0, others.size() - 1)], vals[target]) - "enemy_lower": - for o in order: - if o != seat and host_values[o].size() > 0: - var k := rng.randi_range(0, host_values[o].size() - 1) - _set_die(o, k, maxi(1, host_values[o][k] - 1)) - "peek": - var info := {} - for o in order: - if o != seat and host_values[o].size() > 0: - var k := rng.randi_range(0, host_values[o].size() - 1) - info[o] = [k, host_values[o][k]] - peeked[seat] = info - "fog": - for o in order: - if o != seat and host_values[o].size() > 0: - var k := rng.randi_range(0, host_values[o].size() - 1) - var liste: Array = fogged.get(o, []) - if not liste.has(k): - liste.append(k) - fogged[o] = liste - - -func _merge(a: Array, b: Array) -> Array: - var out := a.duplicate() - for v in b: - if not out.has(v): - out.append(v) - return out +## Würfel neu werfen und die Augenzahl danach übernehmen. +func _neu_werfen(seat: int, idxs: Array) -> void: + _stream = true + _throw_subset(seat, idxs) + await _await_settled([seat]) + _stream = false + if not _alive(): + return + var dl: Array = table.dice_of(seat) + for k: int in idxs: + if k < dl.size(): + host_values[seat][k] = dl[k].top_value() + table.freeze_dice(seat) func _set_die(seat: int, idx: int, value: int) -> void: @@ -419,6 +386,10 @@ func _phase_bid() -> Dictionary: _push_state() if seats[turn].ai: await _sleep(1.1) + # Bots spielen ihre Karten, bevor sie ansagen. + await _bot_karte(turn) + if not _alive(): + return {} var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid, min_raise.get(turn, 1), must_bid.get(turn, false), rng) var res := _apply_action(turn, a) @@ -442,6 +413,22 @@ func _phase_bid() -> Dictionary: return {} +## Ein Bot legt manchmal eine Karte, statt sie bis zum Ausscheiden zu horten. +func _bot_karte(seat: int) -> void: + var karten: Array = hand.get(seat, []) + if karten.is_empty(): + return + # Volle Hand: raus damit. Sonst nur ab und zu, sonst ist sofort alles weg. + var lust := 1.0 if karten.size() >= BoosterDB.HANDLIMIT else 0.35 + if rng.randf() > lust: + return + var id: String = karten[rng.randi_range(0, karten.size() - 1)] + if id == "bid_up" and bid.is_empty(): + return + await _do_play(seat, id, DiceAI.target_for(id, host_values[seat])) + await _sleep(0.6) + + ## Gibt {} zurück wenn das Spiel weitergeht, sonst das Anfechtungs-Ergebnis. func _apply_action(seat: int, a: Dictionary) -> Dictionary: if a.type == "bid" and not _valid_bid(seat, int(a.count), int(a.face)): @@ -523,6 +510,7 @@ func _phase_reveal(result: Dictionary) -> void: return seats[loser].count -= 1 + _zieh_karte(loser) if seats[loser].count <= 0: seats[loser].alive = false seats[loser].count = 0 @@ -657,14 +645,14 @@ func _push_state() -> void: _state.rpc_id(peer, _state_for(i)) -## Erst nach der Wahl offenlegen, sonst verrät die Anzeige während der -## Boosterphase, wer schon gewählt hat. +## Wer hat diese Runde schon eine Karte gespielt? Beim Würfeln steht noch +## nichts an, danach ist es offen. func _round_boosters() -> Array: var out: Array = [] - if phase == "boost" or phase == "roll": + if phase == "roll": return out for i in seats.size(): - out.append(String(chosen_boost.get(i, {}).get("id", ""))) + out.append(String(letzte_karte.get(i, ""))) return out @@ -700,7 +688,8 @@ func _state_for(seat: int) -> Dictionary: "boosts": _round_boosters(), "actual": reveal_total if phase == "reveal" else 0, "bids": _seat_bids(), - "boost": chosen_boost.get(seat, {}).get("id", ""), + "boost": String(letzte_karte.get(seat, "")), + "hand": (hand.get(seat, []) as Array).duplicate(), } @@ -716,6 +705,9 @@ func _apply_state(st: Dictionary) -> void: my_values = st.values banner = st.banner if not multiplayer.is_server(): + # Die eigene Hand kommt vom Server; ohne sie wüsste der Client nur, + # was das HUD gerade zeichnet. + hand[local_seat] = st.get("hand", []) for i in mini(seats.size(), st.seats.size()): seats[i].alive = st.seats[i].alive if seats[i].count != st.seats[i].count: @@ -768,7 +760,7 @@ func _on_challenge_pressed() -> void: func _on_booster_chosen(id: String, target: int) -> void: if multiplayer.is_server(): - _do_boost(local_seat, id, target) + _do_play(local_seat, id, target) else: _req_boost.rpc_id(1, id, target) @@ -812,7 +804,7 @@ func _req_challenge() -> void: @rpc("any_peer", "reliable") func _req_boost(id: String, target: int) -> void: - _do_boost(_seat_of(multiplayer.get_remote_sender_id()), id, target) + _do_play(_seat_of(multiplayer.get_remote_sender_id()), id, target) func _seat_of(peer: int) -> int: @@ -843,18 +835,3 @@ func _do_challenge(seat: int) -> void: return _answered[seat] = _apply_action(seat, {"type": "challenge"}) - -func _do_boost(seat: int, id: String, target: int) -> void: - if seat < 0 or phase != "boost" or chosen_boost.has(seat) or not seats[seat].alive: - return - # Nur aus dem eigenen Angebot: sonst könnte ein veränderter Client sich - # jede Runde den stärksten Booster aus dem Katalog nehmen. - if not (offers.get(seat, []) as Array).has(id): - return - chosen_boost[seat] = {"id": id, "target": target} - _answered[seat] = true - - -@rpc("authority", "reliable") -func _offer(ids: Array) -> void: - hud.show_booster_offer(ids, my_values) diff --git a/scripts/hud.gd b/scripts/hud.gd index 368f248..7ab2047 100644 --- a/scripts/hud.gd +++ b/scripts/hud.gd @@ -39,6 +39,8 @@ 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 @@ -278,6 +280,19 @@ func _build() -> void: 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, -190, 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 @@ -482,6 +497,8 @@ func update_state(st: Dictionary, my_seat: int) -> void: 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 @@ -504,7 +521,7 @@ func update_state(st: Dictionary, my_seat: int) -> void: boosts_box.add_child(_label("Maulkorb: du musst bieten", 14, ACCENT)) any = true if not any: - boosts_box.add_child(_label("Keine Booster aktiv", 14, DIM)) + boosts_box.add_child(_label("Keine Karte gespielt", 14, DIM)) banner_label.text = st.banner @@ -551,7 +568,9 @@ func _process(_delta: float) -> void: var col := Color(0.95, 0.45, 0.4) if left <= 10.0 else DIM timer_bid.text = text if wants_bid else "" timer_bid.add_theme_color_override("font_color", col) - timer_boost.text = "Noch %s - danach wird zufällig gewählt" % text if wants_boost else "" + # 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) @@ -596,32 +615,29 @@ func set_banner(text: String) -> void: # ---------------------------------------------------------------- Booster -func show_booster_offer(ids: Array, values: Array) -> void: - _my_values = values - _pending_boost = "" - target_row.visible = false - boost_title.text = "Wähle einen Booster" - _clear(boost_cards) - for id: String in ids: +## 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) - var card := _panel(Color(0.10, 0.12, 0.17, 0.98)) - card.custom_minimum_size = Vector2(210, 170) - var v := VBoxContainer.new() - v.add_theme_constant_override("separation", 6) - card.add_child(v) - var kind_col := Color(0.5, 0.9, 0.6) if b.kind == BoosterDB.Kind.BUFF else Color(0.95, 0.5, 0.5) - v.add_child(_label("BUFF" if b.kind == BoosterDB.Kind.BUFF else "DEBUFF", 12, kind_col)) - v.add_child(_label(b.name, 20, ACCENT)) - var d := _label(b.desc, 14, Color(0.8, 0.82, 0.88)) - d.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - d.custom_minimum_size = Vector2(190, 60) - v.add_child(d) + if b.is_empty(): + continue var btn := Button.new() - btn.text = "Wählen" + btn.text = String(b.name) + btn.tooltip_text = String(b.desc) + btn.custom_minimum_size = Vector2(0, 40) + btn.disabled = not spielbar + var col := Color(0.5, 0.9, 0.6) if b.kind == BoosterDB.Kind.BUFF else Color(0.95, 0.5, 0.5) + btn.add_theme_color_override("font_color", col) + btn.add_theme_color_override("font_hover_color", Color.WHITE) btn.pressed.connect(_pick.bind(id)) - v.add_child(btn) - boost_cards.add_child(card) - boost_slot.visible = true + hand_box.add_child(btn) func _pick(id: String) -> void: @@ -633,6 +649,7 @@ func _pick(id: String) -> void: 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)