extends Node3D ## Host-autoritative Spiellogik + Client-Ansicht. const START_DICE := 5 const BID_TIMEOUT := 30.0 const REVEAL_PAUSE := 5.0 ## Anzeigedauer des Rundenergebnisses const WURF_ZEIT := 1.1 ## wie lange die geworfene Münze zu sehen ist, ## bevor ihre Wirkung einsetzt var table: GameTable var hud: CanvasLayer var rng := RandomNumberGenerator.new() var seats: Array = [] ## [{peer, name, ai, count, alive}] var local_seat := 0 ## -1 auf dem Spielserver: der sitzt nicht mit var host_values: Array = [] ## nur Host: seat -> Array[int] var my_values: Array = [] ## Client-Sicht (-1 = vernebelt) # Rundenzustand (nur Host) var bid: Dictionary = {} var turn := 0 var starter := 0 var fogged: Dictionary = {} ## seat -> Array[int] verdeckter Würfel var peeked: Dictionary = {} ## viewer -> {target_seat: Array[[idx, value]]} var min_raise: Dictionary = {} ## seat -> int var must_bid: Dictionary = {} ## seat -> bool var hand: Dictionary = {} ## seat -> Array[String], Münzen beim Spieler var mitte: Dictionary = {} ## seat -> zuletzt in die Mitte gelegte Münze var _muenze_laeuft := false ## eine Münze wird gerade aufgelöst var _wurf_nr := 0 ## zählt die Würfe, damit das HUD neue erkennt var last_bids: Dictionary = {} ## seat -> letzte Ansage dieser Runde var log_lines: Array = [] var phase := "lobby" var banner := "" var _answered: Dictionary = {} var _stream := false var _stream_acc := 0.0 var _last_phase := "" var deadline := 0.0 ## Sekunden-Zeitstempel, 0 = keine Frist var _pause_left := 0.0 var _quitting := false ## beim Verlassen: laufende Spielschleife sauber abbrechen var reveal_total := 0 ## beim Aufdecken: wie viele der angesagten Augenzahl wirklich da waren func _ready() -> void: rng.randomize() func setup(cfg: Dictionary) -> void: seats.clear() var peers: Array = cfg.peers for p in peers: seats.append({"peer": int(p), "name": String(cfg.names[p]), "ai": false, "count": START_DICE, "alive": true}) for i in int(cfg.ai): seats.append({"peer": 0, "name": "Bot %d" % (i + 1), "ai": true, "count": START_DICE, "alive": true}) # Der Spielserver spielt nicht mit und bekommt deshalb keinen Platz. var me := multiplayer.get_unique_id() local_seat = -1 for i in seats.size(): if seats[i].peer == me: local_seat = i table = GameTable.new() add_child(table) # Der Server hat keinen Platz, legt den Tisch aber fest so, als säße Platz 0 # unten. Diese Ausrichtung ist der Bezug für alle Würfelkoordinaten. table.build(seats.size(), maxi(local_seat, 0)) # Ohne Platz am Tisch gibt es nichts anzuzeigen und nichts zu bedienen. if local_seat >= 0: hud = preload("res://scripts/hud.gd").new() add_child(hud) hud.bid_pressed.connect(_on_bid_pressed) hud.challenge_pressed.connect(_on_challenge_pressed) hud.booster_chosen.connect(_on_booster_chosen) hud.quit_pressed.connect(_on_quit) hud.pause_changed.connect(_on_pause_changed) for i in seats.size(): # Die Startmünze zieht nur der Host. Beim Client kommt sie mit dem # ersten Zustand, sonst läge dort kurz eine andere. hand[i] = [BoosterDB.zieh(rng)] if multiplayer.is_server() else [] table.set_dice_count(i, START_DICE) table.set_dice_visible(i, false) table.set_cup_down(i, i != local_seat) host_values.append([]) _refresh_labels() if multiplayer.is_server(): multiplayer.peer_disconnected.connect(_on_peer_lost) _host_loop() func _on_peer_lost(peer: int) -> void: var seat := _seat_of(peer) if seat < 0 or seats[seat].ai: return seats[seat].ai = true seats[seat].peer = 0 _log("%s hat das Spiel verlassen - übernimmt ein Bot." % seats[seat].name) _push_state() func _refresh_labels() -> void: for i in seats.size(): var s = seats[i] # Die Würfelzahl steht im Panel oben rechts, hier reicht der Name. # Lange Namen werden gekürzt, damit sie auf den Becher passen. var short: String = s.name if s.name.length() <= 9 else s.name.substr(0, 8) + "…" var txt: String = short if s.alive else "%s · raus" % short var col := Color(1, 1, 1) if not s.alive: col = Color(0.5, 0.5, 0.55) elif phase == "bid" and i == turn: col = Color(1.0, 0.85, 0.3) table.set_label(i, txt, col) table.set_active_seat(turn if phase == "bid" else -1) # ---------------------------------------------------------------- Host-Ablauf func _host_loop() -> void: _log("Spiel gestartet - %d Spieler, je %d Würfel." % [seats.size(), START_DICE]) starter = 0 while _alive() and _alive_count() > 1: 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 var result: Dictionary = await _phase_bid() if not _alive(): return await _phase_reveal(result) phase = "over" var winner := "" for s in seats: if s.alive: winner = s.name banner = "%s gewinnt" % winner _log(banner) _push_state() func _alive_count() -> int: var n := 0 for s in seats: if s.alive: n += 1 return n func _alive_seats() -> Array: var out: Array = [] for i in seats.size(): if seats[i].alive: out.append(i) return out func _next_alive(from: int) -> int: var i := from for _n in seats.size(): i = (i + 1) % seats.size() if seats[i].alive: return i return from func _total_dice() -> int: var n := 0 for s in seats: if s.alive: n += s.count return n ## Solange falsch, darf die Spielschleife nicht weiterlaufen - beim Verlassen ## ist der Knoten sonst schon aus dem Baum und jeder Zugriff schlägt fehl. func _alive() -> bool: return not _quitting and is_inside_tree() ## Ein gerade getrennter Peer steht noch in `seats`, bis das Signal ankommt - ## ein RPC dorthin wäre ein Fehler in der Konsole. func _peer_live(peer: int) -> bool: return peer > 1 and multiplayer.get_peers().has(peer) func _now() -> float: return Time.get_ticks_msec() / 1000.0 func _phase_roll() -> void: phase = "roll" deadline = 0.0 reveal_total = 0 bid = {} fogged.clear() last_bids.clear() peeked.clear() min_raise.clear() must_bid.clear() mitte.clear() _wurf_nr = 0 banner = "" _log("--- Neue Runde ---") for i in _alive_seats(): table.set_dice_count(i, seats[i].count) table.set_dice_visible(i, i == local_seat) table.set_cup_down(i, i != local_seat) _push_state() _stream = true for i in _alive_seats(): table.throw_dice(i, rng) await _await_settled(_alive_seats()) _stream = false if not _alive(): return for i in _alive_seats(): host_values[i] = [] for d in table.dice_of(i): host_values[i].append(d.top_value()) table.freeze_dice(i) _snapshot_all() _push_state() func _await_settled(list: Array, timeout := 7.0) -> void: var step := 1.0 / float(Engine.physics_ticks_per_second) var t := 0.0 while t < timeout: if not _alive(): return await get_tree().physics_frame if not _alive(): return if get_tree().paused: continue t += step if t < 0.7: continue var ok := true for s in list: for d in table.dice_of(s): if d.freeze: continue if not d.is_settled(): ok = false elif d.flatness() < 0.86: d.nudge(rng) ok = false if ok: return # ---------------------------------------------------------------- Münzen ## Wer einen Würfel abgibt, bekommt eine Münze. Das ist der einzige Nachschub. func _zieh_muenze(seat: int) -> void: var muenzen: Array = hand.get(seat, []) if muenzen.size() >= BoosterDB.HANDLIMIT: _log("%s hat schon drei Münzen und bekommt keine." % seats[seat].name) return var id := BoosterDB.zieh(rng) muenzen.append(id) hand[seat] = muenzen _log("%s bekommt %s." % [seats[seat].name, BoosterDB.get_booster(id).name]) ## Ausspielen heißt: Münze in die Mitte legen und werfen. Wappen verstärkt, ## Zahl lässt es bei der Grundwirkung. Beides hilft - verschenkt ist nie ein Zug. func _do_play(seat: int, id: String, target: int) -> void: if seat < 0 or phase != "bid" or not seats[seat].alive: return # Zwei Münzen gleichzeitig würden sich beim Würfeln in die Quere kommen. if _muenze_laeuft: return var muenzen: Array = hand.get(seat, []) if not muenzen.has(id): return muenzen.erase(id) hand[seat] = muenzen _muenze_laeuft = true var stark := rng.randf() < BoosterDB.WAPPEN_CHANCE _wurf_nr += 1 mitte[seat] = {"id": id, "stark": stark, "nr": _wurf_nr} _log("%s wirft %s - %s." % [seats[seat].name, BoosterDB.get_booster(id).name, "Wappen" if stark else "Zahl"]) # Erst die Münze zeigen, dann wirken lassen. Andersherum sähe man die # Folge vor der Ursache. _push_state() await _sleep(WURF_ZEIT) if _alive(): await _wirkung(seat, id, target, stark) _muenze_laeuft = false if not _alive(): return # Der Widerruf hat keine eigene Verstärkung; bei Wappen bleibt er liegen. if stark and BoosterDB.keeps_on_wappen(id): var zurueck: Array = hand.get(seat, []) if zurueck.size() < BoosterDB.HANDLIMIT: zurueck.append(id) hand[seat] = zurueck _snapshot_all() _push_state() func _wirkung(seat: int, id: String, target: int, stark: bool) -> 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())) if stark and not host_values[seat].is_empty(): _set_die(seat, rng.randi_range(0, host_values[seat].size() - 1), 6) "reroll_one": await _neu_werfen(seat, _ziel_und_zufall(seat, ziel, stark)) "set_six": for k: int in _ziel_und_zufall(seat, ziel, stark): _set_die(seat, k, 6) "gain_die": seats[seat].count += 1 table.set_dice_count(seat, seats[seat].count) host_values[seat].append(1) var neu: int = host_values[seat].size() - 1 await _neu_werfen(seat, [neu]) if stark: _set_die(seat, neu, 6) "cancel": fogged.erase(seat) min_raise.erase(seat) must_bid.erase(seat) "peek": var info := {} for o in _alive_seats(): if o != seat: info[o] = _zufalls_paare(o, 2 if stark else 1) peeked[seat] = info "bid_up": if not bid.is_empty(): bid.count = int(bid.count) + (2 if stark else 1) last_bids[int(bid.seat)] = {"count": bid.count, "face": bid.face} "enemy_lower": var minus := 2 if stark else 1 for o in _alive_seats(): if o != seat: for k: int in _zufalls_idx(o, 1): _set_die(o, k, maxi(1, int(host_values[o][k]) - minus)) "fog": for o in _alive_seats(): if o == seat: continue var liste: Array = fogged.get(o, []) for k: int in _zufalls_idx(o, 2 if stark else 1): if not liste.has(k): liste.append(k) fogged[o] = liste "raise_min": for o in _alive_seats(): if o != seat: min_raise[o] = 3 if stark else 2 "gag": if stark: for o in _alive_seats(): if o != seat: must_bid[o] = true else: must_bid[turn] = true ## Bis zu `n` verschiedene Würfelplätze eines Sitzes, zufällig gezogen. func _zufalls_idx(seat: int, n: int) -> Array: var frei: Array = range(host_values[seat].size()) var out: Array = [] while out.size() < n and not frei.is_empty(): out.append(frei.pop_at(rng.randi_range(0, frei.size() - 1))) return out func _zufalls_paare(seat: int, n: int) -> Array: var out: Array = [] for k: int in _zufalls_idx(seat, n): out.append([k, host_values[seat][k]]) return out ## Der selbst gewählte Würfel, bei Wappen zusätzlich ein anderer zufälliger. func _ziel_und_zufall(seat: int, ziel: int, stark: bool) -> Array: var out: Array = [ziel] if not stark: return out var frei: Array = [] for k in host_values[seat].size(): if k != ziel: frei.append(k) if not frei.is_empty(): out.append(frei[rng.randi_range(0, frei.size() - 1)]) 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: if idx < 0 or idx >= host_values[seat].size(): return host_values[seat][idx] = value var dl: Array = table.dice_of(seat) if idx < dl.size() and dl[idx].is_inside_tree(): dl[idx].snap_to_value(value, rng) func _throw_subset(seat: int, idxs: Array) -> void: var dl: Array = table.dice_of(seat) var zone: Node3D = table.zones[seat] for k: int in idxs: if k >= dl.size(): continue var ang := rng.randf() * TAU var rad := GameTable.ZONE_RAD * rng.randf_range(0.25, 0.45) var off := Vector3(cos(ang) * rad, 0.42, sin(ang) * rad) dl[k].throw_from(zone.global_transform * off, rng) func _phase_bid() -> Dictionary: phase = "bid" turn = starter if seats[starter].alive else _next_alive(starter) while _alive(): _answered.clear() deadline = 0.0 if seats[turn].ai else _now() + BID_TIMEOUT _push_state() if seats[turn].ai: await _sleep(1.1) # Bots werfen ihre Münzen, bevor sie ansagen. await _bot_muenze(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) if not res.is_empty(): return res else: await _wait_until(func() -> bool: return _answered.has(turn) or seats[turn].ai, BID_TIMEOUT) if _answered.has(turn): var res: Dictionary = _answered[turn] if not res.is_empty(): return res else: # Zeit abgelaufen oder getrennt: automatisch entscheiden var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid, min_raise.get(turn, 1), must_bid.get(turn, false), rng) _log("%s wird automatisch gespielt." % seats[turn].name) var res := _apply_action(turn, a) if not res.is_empty(): return res turn = _next_alive(turn) return {} ## Ein Bot wirft manchmal eine Münze, statt sie bis zum Ausscheiden zu horten. func _bot_muenze(seat: int) -> void: var muenzen: Array = hand.get(seat, []) if muenzen.is_empty(): return # Voller Vorrat: raus damit. Sonst nur ab und zu, sonst ist alles sofort weg. var lust := 1.0 if muenzen.size() >= BoosterDB.HANDLIMIT else 0.35 if rng.randf() > lust: return var id: String = muenzen[rng.randi_range(0, muenzen.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)): # Wer einen Maulkorb hat, darf sich nicht über ein ungültiges Gebot in # die Anfechtung retten - dann wird auf das kleinste gültige korrigiert. var low := _lowest_legal_bid(seat) if not low.is_empty() and (bid.is_empty() or must_bid.get(seat, false)): a = {"type": "bid", "count": low.count, "face": low.face} elif not bid.is_empty(): a = {"type": "challenge"} else: a = {"type": "bid", "count": 1, "face": 1} if a.type == "challenge": _log("%s fechtet an!" % seats[seat].name) return {"challenger": seat, "bid": bid.duplicate()} bid = {"seat": seat, "count": int(a.count), "face": int(a.face)} last_bids[seat] = {"count": bid.count, "face": bid.face} must_bid.erase(seat) _log("%s sagt %d x %d an." % [seats[seat].name, bid.count, bid.face]) return {} ## Kleinstes Gebot, das dieser Platz noch ansagen darf ({} = gar keins mehr). func _lowest_legal_bid(seat: int) -> Dictionary: if bid.is_empty(): return {"count": 1, "face": 1} var mr: int = min_raise.get(seat, 1) if mr == 1 and int(bid.face) < 6: return {"count": int(bid.count), "face": int(bid.face) + 1} var c: int = int(bid.count) + mr if c <= _total_dice(): return {"count": c, "face": 1} return {} func _valid_bid(seat: int, count: int, face: int) -> bool: if face < 1 or face > 6 or count < 1 or count > _total_dice(): return false if bid.is_empty(): return true var mr: int = min_raise.get(seat, 1) if count >= bid.count + mr: return true if mr > 1: return false return count == bid.count and face > bid.face func _phase_reveal(result: Dictionary) -> void: phase = "reveal" deadline = 0.0 var b: Dictionary = result.bid var challenger: int = result.challenger var total := 0 for i in _alive_seats(): for v in host_values[i]: if v == b.face: total += 1 reveal_total = total var loser := challenger if total >= b.count else int(b.seat) _log("%d x %d angesagt - tatsächlich %d. %s verliert einen Würfel." % [ b.count, b.face, total, seats[loser].name]) banner = "%s verliert" % seats[loser].name for i in _alive_seats(): table.arrange_and_show(i, host_values[i], rng) table.set_cup_down(i, false) # Gezielt an die Mitspieler am Tisch: ein Peer, der nur in der Lobby sitzt, # hat keinen Spielknoten und würde das Paket ins Leere laufen lassen. var values := _all_values() for i in seats.size(): var peer: int = seats[i].peer if _peer_live(peer): _reveal.rpc_id(peer, values, banner) _push_state() await _sleep(REVEAL_PAUSE) if not _alive(): return seats[loser].count -= 1 if seats[loser].count <= 0: seats[loser].alive = false seats[loser].count = 0 host_values[loser] = [] table.set_dice_count(loser, 0) _log("%s scheidet aus." % seats[loser].name) else: _zieh_muenze(loser) starter = loser if seats[loser].alive else _next_alive(loser) # Nur wer noch spielt, bekommt den Becher wieder. Auf einer leeren Zone # sähe er aus, als lägen dort noch Würfel. for i in seats.size(): table.set_dice_visible(i, false) table.set_cup_down(i, seats[i].alive) banner = "" _push_state() func _all_values() -> Array: var out: Array = [] for i in seats.size(): out.append(host_values[i] if seats[i].alive else []) return out ## process_always = false: eine offene Pause hält den Timer wirklich an. func _sleep(sec: float) -> void: if not _alive(): return await get_tree().create_timer(sec, false).timeout func _wait_until(cond: Callable, timeout: float) -> bool: var t := 0.0 while t < timeout: if not _alive(): return false if cond.call(): return true await get_tree().process_frame if not _alive(): return false # Im Einzelspieler hält das Pausenmenü den Baum an - dann darf auch die # Frist nicht weiterlaufen, sonst ist der Zug nach dem Fortsetzen weg. if not get_tree().paused: t += get_process_delta_time() return _alive() and cond.call() func _log(line: String) -> void: log_lines.append(line) if log_lines.size() > 8: log_lines.pop_front() # ---------------------------------------------------------------- Sync func _physics_process(delta: float) -> void: if not _alive() or not _stream or not multiplayer.is_server(): return _stream_acc += delta if _stream_acc < 1.0 / 30.0: return _stream_acc = 0.0 for i in seats.size(): var peer: int = seats[i].peer if not _peer_live(peer): continue _dice_stream.rpc_id(peer, _pack(i)) func _snapshot_all() -> void: for i in seats.size(): var peer: int = seats[i].peer if _peer_live(peer): _dice_snapshot.rpc_id(peer, _pack(i)) func _pack(seat: int) -> PackedFloat32Array: var out := PackedFloat32Array() for d in table.dice_of(seat): var t: Transform3D = d.global_transform var q := t.basis.get_rotation_quaternion() out.append_array([t.origin.x, t.origin.y, t.origin.z, q.x, q.y, q.z, q.w]) return out func _unpack(data: PackedFloat32Array) -> void: var dl: Array = table.dice_of(local_seat) var n := mini(dl.size(), int(data.size() / 7)) # Jeder Client dreht seinen eigenen Platz nach unten, der Server nicht. # Ohne diese Drehung landen die eigenen Würfel in der Zone des Nachbarn. var dreh := Transform3D(Basis(Vector3.UP, TAU * local_seat / float(seats.size())), Vector3.ZERO) for i in n: var o := i * 7 var t := Transform3D(Basis(Quaternion(data[o + 3], data[o + 4], data[o + 5], data[o + 6])), Vector3(data[o], data[o + 1], data[o + 2])) dl[i].set_kinematic(true) dl[i].place(dreh * t) @rpc("authority", "unreliable_ordered") func _dice_stream(data: PackedFloat32Array) -> void: _unpack(data) @rpc("authority", "reliable") func _dice_snapshot(data: PackedFloat32Array) -> void: _unpack(data) @rpc("authority", "reliable") func _reveal(all_values: Array, text: String) -> void: # Auch die eigenen Würfel in den Bogen legen - sonst liegen sie als einzige # noch verstreut da, wo die Physik sie fallen liess. Dafür die Werte des # Hosts nehmen: my_values ist hier noch vernebelt, ein vernebelter Würfel # würde sonst mit beliebiger Seite nach oben liegen bleiben. for i in seats.size(): if i < all_values.size() and not all_values[i].is_empty(): table.arrange_and_show(i, all_values[i], rng) table.set_cup_down(i, false) banner = text hud.set_banner(text) func _push_state() -> void: if not _alive(): return _refresh_labels() for i in seats.size(): var peer: int = seats[i].peer if peer == multiplayer.get_unique_id(): _apply_state(_state_for(i)) elif _peer_live(peer): _state.rpc_id(peer, _state_for(i)) ## Welche Münze liegt von wem in der Mitte? Beim Würfeln steht noch nichts ## an, danach ist es offen. func _round_boosters() -> Array: var out: Array = [] if phase == "roll": return out for i in seats.size(): out.append(mitte.get(i, {})) return out ## Was jeder Platz in dieser Runde zuletzt angesagt hat. func _seat_bids() -> Array: var out: Array = [] for i in seats.size(): out.append(last_bids.get(i, {})) return out func _state_for(seat: int) -> Dictionary: var vis: Array = [] if phase == "roll": for _k in seats[seat].count: # während des Wurfs steht der Wert noch nicht fest vis.append(-1) elif phase == "reveal": vis = host_values[seat].duplicate() # aufgedeckt wird ohne Nebel else: var fg: Array = fogged.get(seat, []) for k in host_values[seat].size(): vis.append(-1 if fg.has(k) else host_values[seat][k]) var pub: Array = [] for s in seats: pub.append({"name": s.name, "count": s.count, "alive": s.alive, "ai": s.ai}) return { "phase": phase, "turn": turn, "bid": bid, "seats": pub, "values": vis, "banner": banner, "peek": peeked.get(seat, {}), "min_raise": min_raise.get(seat, 1), "must_bid": must_bid.get(seat, false), "total": _total_dice(), "time_left": 0.0 if deadline <= 0.0 else maxf(0.0, deadline - _now()), "open": _all_values() if phase == "reveal" else [], "boosts": _round_boosters(), "actual": reveal_total if phase == "reveal" else 0, "bids": _seat_bids(), "hand": (hand.get(seat, []) as Array).duplicate(), } @rpc("authority", "reliable") func _state(st: Dictionary) -> void: _apply_state(st) func _apply_state(st: Dictionary) -> void: phase = st.phase turn = st.turn bid = st.bid 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: seats[i].count = st.seats[i].count table.set_dice_count(i, seats[i].count) if phase != _last_phase: _last_phase = phase if phase == "roll": for i in seats.size(): if i != local_seat: table.set_dice_visible(i, false) table.set_cup_down(i, seats[i].alive) table.set_dice_visible(local_seat, true) _refresh_labels() _apply_fog_to_table() var bids: Array = st.get("bids", []) for i in mini(seats.size(), bids.size()): var b: Dictionary = bids[i] if b.is_empty(): table.set_bid(i, 0, 0) else: table.set_bid(i, int(b.count), int(b.face)) hud.update_state(st, local_seat) # ---------------------------------------------------------------- Aktionen ## Der Nebel-Debuff muss auch den Würfel auf dem Tisch verdecken. func _apply_fog_to_table() -> void: if phase == "roll" or local_seat < 0 or local_seat >= table.dice.size(): return var dl: Array = table.dice_of(local_seat) for i in mini(dl.size(), my_values.size()): dl[i].visible = my_values[i] >= 0 func _on_bid_pressed(count: int, face: int) -> void: if multiplayer.is_server(): _do_bid(local_seat, count, face) else: _req_bid.rpc_id(1, count, face) func _on_challenge_pressed() -> void: if multiplayer.is_server(): _do_challenge(local_seat) else: _req_challenge.rpc_id(1) func _on_booster_chosen(id: String, target: int) -> void: if multiplayer.is_server(): _do_play(local_seat, id, target) else: _req_boost.rpc_id(1, id, target) ## Im Einzelspieler hält das Pausenmenü das Spiel wirklich an. Sobald ein ## zweiter Mensch am Tisch sitzt, läuft die Runde weiter - sonst würde ein ## geöffnetes Menü alle anderen blockieren. func _on_pause_changed(on: bool) -> void: if not multiplayer.get_peers().is_empty(): return if on: _pause_left = 0.0 if deadline <= 0.0 else maxf(0.0, deadline - _now()) get_tree().paused = on if not on and _pause_left > 0.0: deadline = _now() + _pause_left _pause_left = 0.0 _push_state() func _on_quit() -> void: if _quitting: return _quitting = true get_tree().paused = false # Sofort ausblenden, damit das Verlassen unmittelbar wirkt. table.visible = false hud.visible = false Net.leave() get_tree().change_scene_to_file("res://scenes/main.tscn") @rpc("any_peer", "reliable") func _req_bid(count: int, face: int) -> void: _do_bid(_seat_of(multiplayer.get_remote_sender_id()), count, face) @rpc("any_peer", "reliable") func _req_challenge() -> void: _do_challenge(_seat_of(multiplayer.get_remote_sender_id())) @rpc("any_peer", "reliable") func _req_boost(id: String, target: int) -> void: _do_play(_seat_of(multiplayer.get_remote_sender_id()), id, target) func _seat_of(peer: int) -> int: if peer <= 0: # 0 steht für einen KI-Platz, das ist nie ein Absender return -1 for i in seats.size(): if seats[i].peer == peer: return i return -1 func _do_bid(seat: int, count: int, face: int) -> void: if seat < 0 or phase != "bid" or seat != turn or _answered.has(seat): return if not _valid_bid(seat, count, face): return _answered[seat] = _apply_action(seat, {"type": "bid", "count": count, "face": face}) func _do_challenge(seat: int) -> void: if seat < 0 or phase != "bid" or seat != turn or _answered.has(seat): return if bid.is_empty(): return # Der Maulkorb sperrt die Anfechtung nur, solange es überhaupt noch ein # gültiges Gebot gibt - sonst bliebe der Platz ohne jede Handlung. if must_bid.get(seat, false) and not _lowest_legal_bid(seat).is_empty(): return _answered[seat] = _apply_action(seat, {"type": "challenge"})