Files
pirate-dice/scripts/game.gd
team3 3f4532ad59 Booster-Phase raus, Karten auf die Hand
Die Boosterwahl kostete jede Runde eine eigene Phase mit 30 Sekunden Frist.
Zwischen Wurf und Ansage lag damit eine Wartezeit, die den Ablauf zerriss.

Jetzt gibt es Karten statt einer Phase:

* Wer einen Wuerfel abgibt, zieht eine Karte. Das ist der einzige Nachschub,
  also bekommt der etwas zurueck, der gerade hinten liegt.
* Gespielt wird jederzeit waehrend der Ansagen, nicht nur am eigenen Zug.
* Hoechstens drei Karten auf der Hand.
* Die Hand liegt offen unten links, ein Klick spielt. Nur Feinschliff und
  Gluecksgriff fragen noch nach dem Wuerfel.

Der Katalog ist angepasst, weil jede Karte jetzt sofort wirken muss:
neu sind Erhoehen (Ansage +1), Widerruf (hebt Nebel, Stoerfeuer und Maulkorb
auf dir auf) und Nachschub (ein Wuerfel dazu). Zwilling und Ruetteln sind
raus - beide brauchten den festen Zeitpunkt vor dem Wurf.

Bots spielen ihre Karten am eigenen Zug, bei voller Hand immer.

Zwei Karten gleichzeitig wuerden sich beim Wuerfeln in die Quere kommen,
deshalb wird immer nur eine aufgeloest.

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

838 lines
24 KiB
GDScript

extends Node3D
## Host-autoritative Spiellogik + Client-Ansicht.
const START_DICE := 5
const BID_TIMEOUT := 30.0
const REVEAL_PAUSE := 5.0 ## Anzeigedauer des Rundenergebnisses
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: [idx, value]}
var min_raise: Dictionary = {} ## seat -> int
var must_bid: Dictionary = {} ## seat -> bool
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"
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():
hand[i] = []
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)
# ---------------------------------------------------------------- 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()
letzte_karte.clear()
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
# ---------------------------------------------------------------- Karten
## 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])
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 _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
## 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 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)
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 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)):
# 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
_zieh_karte(loser)
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)
starter = loser if seats[loser].alive else _next_alive(loser)
for i in seats.size():
table.set_dice_visible(i, false)
table.set_cup_down(i, true)
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))
## 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 == "roll":
return out
for i in seats.size():
out.append(String(letzte_karte.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(),
"boost": String(letzte_karte.get(seat, "")),
"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, true)
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"})