update
This commit is contained in:
26
README.md
26
README.md
@@ -25,14 +25,27 @@ Das Spiel startet im Vollbild, **F11** schaltet um. **Esc** öffnet ein Menü mi
|
|||||||
wirklich an, im Mehrspieler läuft die Runde weiter – sonst würde ein geöffnetes
|
wirklich an, im Mehrspieler läuft die Runde weiter – sonst würde ein geöffnetes
|
||||||
Menü alle anderen am Tisch blockieren.
|
Menü alle anderen am Tisch blockieren.
|
||||||
|
|
||||||
Die Bildaufteilung: Tisch mittig, aktuelles Gebot oben mittig, Würfel aller
|
Die Bildaufteilung: Tisch mittig, darauf die letzte Wette als so viele Würfel
|
||||||
Spieler oben rechts, Verlauf unten links, Ansage unten rechts.
|
wie angesagt; aktive Booster oben links, Würfel aller Spieler oben rechts
|
||||||
|
(Name mit den Würfeln darunter) – beide Panels nur so breit wie ihr Inhalt –,
|
||||||
|
und die eigene Ansage als schmale Leiste unten in der Mitte.
|
||||||
|
|
||||||
Augenzahlen werden überall als gezeichnete Würfel dargestellt (`DieIcon` in
|
Augenzahlen werden überall als gezeichnete Würfel dargestellt (`DieIcon` in
|
||||||
`hud.gd`), unbekannte Werte als Würfel mit Fragezeichen. Im Panel oben rechts
|
`hud.gd`), unbekannte Werte als Würfel mit Fragezeichen. Beim Aufdecken färbt
|
||||||
|
sich die Wette in der Tischmitte ein: grün die Würfel, die es wirklich gab,
|
||||||
|
rot die fehlenden. Am Tisch trägt jeder Becher ein Schild mit Namen und der letzten Ansage dieses
|
||||||
|
Spielers; Unterlage und Würfelbilder dafür werden beim Start einmal als Textur
|
||||||
|
gezeichnet. Im Panel oben rechts
|
||||||
stehen deshalb deine eigenen Würfel offen, gegnerische verdeckt – bis auf die
|
stehen deshalb deine eigenen Würfel offen, gegnerische verdeckt – bis auf die
|
||||||
per *Spähen* aufgedeckten und beim Aufdecken am Rundenende.
|
per *Spähen* aufgedeckten und beim Aufdecken am Rundenende.
|
||||||
|
|
||||||
|
Jeder Platz hat eine runde Würfelzone mit umlaufender Bande. Die runde Form
|
||||||
|
ist nicht nur Optik: in den Ecken der früheren rechteckigen Zone verkanteten
|
||||||
|
sich Würfel, im Kreis liegt kein Wurf mehr schief (Messung: 2,0 % schiefe
|
||||||
|
Würfel und 1,3 % Nachhelfer vorher, 0,4 % und 0 % nachher). Beim Aufdecken
|
||||||
|
legen sich die Würfel als Bogen auf einen Kreis um die Zonenmitte, damit sie
|
||||||
|
sich nicht überdecken und das Namensschild frei bleibt.
|
||||||
|
|
||||||
Der Tisch wächst mit der Spielerzahl, damit die Würfelzonen auch bei acht
|
Der Tisch wächst mit der Spielerzahl, damit die Würfelzonen auch bei acht
|
||||||
Plätzen nicht aneinanderstoßen. Die Kamera schaut mit 15° Neigung fast senkrecht
|
Plätzen nicht aneinanderstoßen. Die Kamera schaut mit 15° Neigung fast senkrecht
|
||||||
von oben darauf; der eigene Platz liegt unten im Bild. `_frame_camera` in
|
von oben darauf; der eigene Platz liegt unten im Bild. `_frame_camera` in
|
||||||
@@ -52,6 +65,13 @@ Zwei Engine-Vorgaben sind bewusst gesetzt, beide in `project.godot`:
|
|||||||
* **`renderer/rendering_method="mobile"`** – auf integrierten GPUs kostet
|
* **`renderer/rendering_method="mobile"`** – auf integrierten GPUs kostet
|
||||||
Forward+ hier mehr als das Doppelte: 22 statt 47 fps bei 30 Würfeln.
|
Forward+ hier mehr als das Doppelte: 22 statt 47 fps bei 30 Würfeln.
|
||||||
|
|
||||||
|
Ein liegender Würfel wird mit `FREEZE_MODE_STATIC` eingefroren, nicht
|
||||||
|
kinematisch. Ein kinematischer Körper meldet seine Lage weiter an den Knoten
|
||||||
|
zurück: setzt ein Booster wie *Glücksgriff* die Augenzahl über
|
||||||
|
`snap_to_value()`, überschreibt der Physikserver die neue Lage im nächsten
|
||||||
|
Schritt wieder mit der alten, und der Würfel zeigt den falschen Wert.
|
||||||
|
`Die.place()` zieht die Lage zusätzlich sofort im Physikserver nach.
|
||||||
|
|
||||||
Ein Würfel ist ein einziges Renderobjekt: Körper und Augen stecken in einem
|
Ein Würfel ist ein einziges Renderobjekt: Körper und Augen stecken in einem
|
||||||
gemeinsamen Mesh mit Vertexfarben, statt 21 Augen als eigene Meshes zu hängen.
|
gemeinsamen Mesh mit Vertexfarben, statt 21 Augen als eigene Meshes zu hängen.
|
||||||
|
|
||||||
|
|||||||
@@ -57,10 +57,12 @@ static func choose_action(values: Array, total_dice: int, bid: Dictionary,
|
|||||||
if raise_.is_empty():
|
if raise_.is_empty():
|
||||||
if must_bid:
|
if must_bid:
|
||||||
# Notgebot: minimal erhöhen, auch wenn unwahrscheinlich
|
# Notgebot: minimal erhöhen, auch wenn unwahrscheinlich
|
||||||
var c: int = bid.count + maxi(1, min_raise)
|
var c: int = int(bid.count) + maxi(1, min_raise)
|
||||||
if c > total_dice:
|
if c <= total_dice:
|
||||||
return {"type": "challenge"} if not must_bid else {"type": "bid", "count": total_dice, "face": bid.face}
|
return {"type": "bid", "count": c, "face": bid.face}
|
||||||
return {"type": "bid", "count": c, "face": bid.face}
|
if min_raise <= 1 and int(bid.face) < 6:
|
||||||
|
return {"type": "bid", "count": int(bid.count), "face": int(bid.face) + 1}
|
||||||
|
# Es gibt kein gültiges Gebot mehr - dann bleibt nur die Anfechtung.
|
||||||
return {"type": "challenge"}
|
return {"type": "challenge"}
|
||||||
return {"type": "bid", "count": raise_.count, "face": raise_.face}
|
return {"type": "bid", "count": raise_.count, "face": raise_.face}
|
||||||
|
|
||||||
|
|||||||
@@ -194,13 +194,21 @@ func snap_to_value(value: int, rng: RandomNumberGenerator) -> void:
|
|||||||
|
|
||||||
var t := global_transform
|
var t := global_transform
|
||||||
t.basis = Basis(wb, wa, wc) * Basis(b, a, c).transposed()
|
t.basis = Basis(wb, wa, wc) * Basis(b, a, c).transposed()
|
||||||
global_transform = t
|
place(t)
|
||||||
if not freeze:
|
if not freeze:
|
||||||
sleeping = false
|
sleeping = false
|
||||||
linear_velocity = Vector3.ZERO
|
linear_velocity = Vector3.ZERO
|
||||||
angular_velocity = Vector3.ZERO
|
angular_velocity = Vector3.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
## Setzt die Lage und schiebt sie sofort in den Physikserver nach. Ohne das
|
||||||
|
## schreibt der Server im nächsten Schritt noch einmal die alte Lage zurück -
|
||||||
|
## der Würfel zeigt dann einen Frame lang die falsche Augenzahl.
|
||||||
|
func place(t: Transform3D) -> void:
|
||||||
|
global_transform = t
|
||||||
|
PhysicsServer3D.body_set_state(get_rid(), PhysicsServer3D.BODY_STATE_TRANSFORM, t)
|
||||||
|
|
||||||
|
|
||||||
func set_kinematic(on: bool) -> void:
|
func set_kinematic(on: bool) -> void:
|
||||||
freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
|
freeze_mode = RigidBody3D.FREEZE_MODE_STATIC
|
||||||
freeze = on
|
freeze = on
|
||||||
|
|||||||
200
scripts/game.gd
200
scripts/game.gd
@@ -4,6 +4,7 @@ extends Node3D
|
|||||||
const START_DICE := 5
|
const START_DICE := 5
|
||||||
const BOOST_TIMEOUT := 30.0
|
const BOOST_TIMEOUT := 30.0
|
||||||
const BID_TIMEOUT := 30.0
|
const BID_TIMEOUT := 30.0
|
||||||
|
const REVEAL_PAUSE := 5.0 ## Anzeigedauer des Rundenergebnisses
|
||||||
|
|
||||||
var table: GameTable
|
var table: GameTable
|
||||||
var hud: CanvasLayer
|
var hud: CanvasLayer
|
||||||
@@ -18,11 +19,13 @@ var my_values: Array = [] ## Client-Sicht (-1 = vernebelt)
|
|||||||
var bid: Dictionary = {}
|
var bid: Dictionary = {}
|
||||||
var turn := 0
|
var turn := 0
|
||||||
var starter := 0
|
var starter := 0
|
||||||
var fogged: Dictionary = {} ## seat -> int index
|
var fogged: Dictionary = {} ## seat -> Array[int] verdeckter Würfel
|
||||||
var peeked: Dictionary = {} ## viewer -> {target_seat: [idx, value]}
|
var peeked: Dictionary = {} ## viewer -> {target_seat: [idx, value]}
|
||||||
var min_raise: Dictionary = {} ## seat -> int
|
var min_raise: Dictionary = {} ## seat -> int
|
||||||
var must_bid: Dictionary = {} ## seat -> bool
|
var must_bid: Dictionary = {} ## seat -> bool
|
||||||
var chosen_boost: Dictionary = {}
|
var chosen_boost: Dictionary = {}
|
||||||
|
var offers: Dictionary = {} ## seat -> die drei angebotenen Booster
|
||||||
|
var last_bids: Dictionary = {} ## seat -> letzte Ansage dieser Runde
|
||||||
var log_lines: Array = []
|
var log_lines: Array = []
|
||||||
var phase := "lobby"
|
var phase := "lobby"
|
||||||
var banner := ""
|
var banner := ""
|
||||||
@@ -33,6 +36,8 @@ var _stream_acc := 0.0
|
|||||||
var _last_phase := ""
|
var _last_phase := ""
|
||||||
var deadline := 0.0 ## Sekunden-Zeitstempel, 0 = keine Frist
|
var deadline := 0.0 ## Sekunden-Zeitstempel, 0 = keine Frist
|
||||||
var _pause_left := 0.0
|
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:
|
func _ready() -> void:
|
||||||
@@ -108,17 +113,25 @@ func _refresh_labels() -> void:
|
|||||||
func _host_loop() -> void:
|
func _host_loop() -> void:
|
||||||
_log("Spiel gestartet - %d Spieler, je %d Würfel." % [seats.size(), START_DICE])
|
_log("Spiel gestartet - %d Spieler, je %d Würfel." % [seats.size(), START_DICE])
|
||||||
starter = 0
|
starter = 0
|
||||||
while _alive_count() > 1:
|
while _alive() and _alive_count() > 1:
|
||||||
await _phase_roll()
|
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()
|
await _phase_boost()
|
||||||
|
if not _alive():
|
||||||
|
return
|
||||||
var result: Dictionary = await _phase_bid()
|
var result: Dictionary = await _phase_bid()
|
||||||
|
if not _alive():
|
||||||
|
return
|
||||||
await _phase_reveal(result)
|
await _phase_reveal(result)
|
||||||
phase = "over"
|
phase = "over"
|
||||||
var winner := ""
|
var winner := ""
|
||||||
for s in seats:
|
for s in seats:
|
||||||
if s.alive:
|
if s.alive:
|
||||||
winner = s.name
|
winner = s.name
|
||||||
banner = "%s gewinnt!" % winner
|
banner = "%s gewinnt" % winner
|
||||||
_log(banner)
|
_log(banner)
|
||||||
_push_state()
|
_push_state()
|
||||||
|
|
||||||
@@ -156,6 +169,18 @@ func _total_dice() -> int:
|
|||||||
return n
|
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:
|
func _now() -> float:
|
||||||
return Time.get_ticks_msec() / 1000.0
|
return Time.get_ticks_msec() / 1000.0
|
||||||
|
|
||||||
@@ -163,8 +188,10 @@ func _now() -> float:
|
|||||||
func _phase_roll() -> void:
|
func _phase_roll() -> void:
|
||||||
phase = "roll"
|
phase = "roll"
|
||||||
deadline = 0.0
|
deadline = 0.0
|
||||||
|
reveal_total = 0
|
||||||
bid = {}
|
bid = {}
|
||||||
fogged.clear()
|
fogged.clear()
|
||||||
|
last_bids.clear()
|
||||||
peeked.clear()
|
peeked.clear()
|
||||||
min_raise.clear()
|
min_raise.clear()
|
||||||
must_bid.clear()
|
must_bid.clear()
|
||||||
@@ -183,6 +210,8 @@ func _phase_roll() -> void:
|
|||||||
table.throw_dice(i, rng)
|
table.throw_dice(i, rng)
|
||||||
await _await_settled(_alive_seats())
|
await _await_settled(_alive_seats())
|
||||||
_stream = false
|
_stream = false
|
||||||
|
if not _alive():
|
||||||
|
return
|
||||||
|
|
||||||
for i in _alive_seats():
|
for i in _alive_seats():
|
||||||
host_values[i] = []
|
host_values[i] = []
|
||||||
@@ -197,7 +226,13 @@ func _await_settled(list: Array, timeout := 7.0) -> void:
|
|||||||
var step := 1.0 / float(Engine.physics_ticks_per_second)
|
var step := 1.0 / float(Engine.physics_ticks_per_second)
|
||||||
var t := 0.0
|
var t := 0.0
|
||||||
while t < timeout:
|
while t < timeout:
|
||||||
|
if not _alive():
|
||||||
|
return
|
||||||
await get_tree().physics_frame
|
await get_tree().physics_frame
|
||||||
|
if not _alive():
|
||||||
|
return
|
||||||
|
if get_tree().paused:
|
||||||
|
continue
|
||||||
t += step
|
t += step
|
||||||
if t < 0.7:
|
if t < 0.7:
|
||||||
continue
|
continue
|
||||||
@@ -218,7 +253,7 @@ func _await_settled(list: Array, timeout := 7.0) -> void:
|
|||||||
func _phase_boost() -> void:
|
func _phase_boost() -> void:
|
||||||
phase = "boost"
|
phase = "boost"
|
||||||
_answered.clear()
|
_answered.clear()
|
||||||
var offers := {}
|
offers.clear()
|
||||||
for i in _alive_seats():
|
for i in _alive_seats():
|
||||||
offers[i] = BoosterDB.draw_offer(rng)
|
offers[i] = BoosterDB.draw_offer(rng)
|
||||||
if seats[i].ai:
|
if seats[i].ai:
|
||||||
@@ -231,12 +266,16 @@ func _phase_boost() -> void:
|
|||||||
_push_state()
|
_push_state()
|
||||||
|
|
||||||
await _wait_until(_all_boosts_in, BOOST_TIMEOUT)
|
await _wait_until(_all_boosts_in, BOOST_TIMEOUT)
|
||||||
|
if not _alive():
|
||||||
|
return
|
||||||
|
|
||||||
for i in _alive_seats():
|
for i in _alive_seats():
|
||||||
if not chosen_boost.has(i):
|
if not chosen_boost.has(i):
|
||||||
var fallback: String = offers[i][rng.randi_range(0, offers[i].size() - 1)]
|
var fallback: String = offers[i][rng.randi_range(0, offers[i].size() - 1)]
|
||||||
chosen_boost[i] = {"id": fallback, "target": 0}
|
chosen_boost[i] = {"id": fallback, "target": 0}
|
||||||
await _apply_boosters()
|
await _apply_boosters()
|
||||||
|
if not _alive():
|
||||||
|
return
|
||||||
_snapshot_all()
|
_snapshot_all()
|
||||||
_push_state()
|
_push_state()
|
||||||
|
|
||||||
@@ -290,6 +329,8 @@ func _apply_boosters() -> void:
|
|||||||
_throw_subset(seat, rerolls[seat])
|
_throw_subset(seat, rerolls[seat])
|
||||||
await _await_settled(rerolls.keys())
|
await _await_settled(rerolls.keys())
|
||||||
_stream = false
|
_stream = false
|
||||||
|
if not _alive():
|
||||||
|
return
|
||||||
for seat: int in rerolls:
|
for seat: int in rerolls:
|
||||||
var dl: Array = table.dice_of(seat)
|
var dl: Array = table.dice_of(seat)
|
||||||
for k: int in rerolls[seat]:
|
for k: int in rerolls[seat]:
|
||||||
@@ -327,7 +368,11 @@ func _apply_boosters() -> void:
|
|||||||
"fog":
|
"fog":
|
||||||
for o in order:
|
for o in order:
|
||||||
if o != seat and host_values[o].size() > 0:
|
if o != seat and host_values[o].size() > 0:
|
||||||
fogged[o] = rng.randi_range(0, host_values[o].size() - 1)
|
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:
|
func _merge(a: Array, b: Array) -> Array:
|
||||||
@@ -343,7 +388,7 @@ func _set_die(seat: int, idx: int, value: int) -> void:
|
|||||||
return
|
return
|
||||||
host_values[seat][idx] = value
|
host_values[seat][idx] = value
|
||||||
var dl: Array = table.dice_of(seat)
|
var dl: Array = table.dice_of(seat)
|
||||||
if idx < dl.size():
|
if idx < dl.size() and dl[idx].is_inside_tree():
|
||||||
dl[idx].snap_to_value(value, rng)
|
dl[idx].snap_to_value(value, rng)
|
||||||
|
|
||||||
|
|
||||||
@@ -353,14 +398,16 @@ func _throw_subset(seat: int, idxs: Array) -> void:
|
|||||||
for k: int in idxs:
|
for k: int in idxs:
|
||||||
if k >= dl.size():
|
if k >= dl.size():
|
||||||
continue
|
continue
|
||||||
var off := Vector3(rng.randf_range(-0.1, 0.1), 0.42, rng.randf_range(-0.22, 0.22))
|
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)
|
dl[k].throw_from(zone.global_transform * off, rng)
|
||||||
|
|
||||||
|
|
||||||
func _phase_bid() -> Dictionary:
|
func _phase_bid() -> Dictionary:
|
||||||
phase = "bid"
|
phase = "bid"
|
||||||
turn = starter if seats[starter].alive else _next_alive(starter)
|
turn = starter if seats[starter].alive else _next_alive(starter)
|
||||||
while true:
|
while _alive():
|
||||||
_answered.clear()
|
_answered.clear()
|
||||||
deadline = 0.0 if seats[turn].ai else _now() + BID_TIMEOUT
|
deadline = 0.0 if seats[turn].ai else _now() + BID_TIMEOUT
|
||||||
_push_state()
|
_push_state()
|
||||||
@@ -392,16 +439,38 @@ func _phase_bid() -> Dictionary:
|
|||||||
## Gibt {} zurück wenn das Spiel weitergeht, sonst das Anfechtungs-Ergebnis.
|
## Gibt {} zurück wenn das Spiel weitergeht, sonst das Anfechtungs-Ergebnis.
|
||||||
func _apply_action(seat: int, a: Dictionary) -> Dictionary:
|
func _apply_action(seat: int, a: Dictionary) -> Dictionary:
|
||||||
if a.type == "bid" and not _valid_bid(seat, int(a.count), int(a.face)):
|
if a.type == "bid" and not _valid_bid(seat, int(a.count), int(a.face)):
|
||||||
a = {"type": "challenge"} if not bid.is_empty() else {"type": "bid", "count": 1, "face": 1}
|
# 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":
|
if a.type == "challenge":
|
||||||
_log("%s fechtet an!" % seats[seat].name)
|
_log("%s fechtet an!" % seats[seat].name)
|
||||||
return {"challenger": seat, "bid": bid.duplicate()}
|
return {"challenger": seat, "bid": bid.duplicate()}
|
||||||
bid = {"seat": seat, "count": int(a.count), "face": int(a.face)}
|
bid = {"seat": seat, "count": int(a.count), "face": int(a.face)}
|
||||||
|
last_bids[seat] = {"count": bid.count, "face": bid.face}
|
||||||
must_bid.erase(seat)
|
must_bid.erase(seat)
|
||||||
_log("%s sagt %d x %d an." % [seats[seat].name, bid.count, bid.face])
|
_log("%s sagt %d x %d an." % [seats[seat].name, bid.count, bid.face])
|
||||||
return {}
|
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:
|
func _valid_bid(seat: int, count: int, face: int) -> bool:
|
||||||
if face < 1 or face > 6 or count < 1 or count > _total_dice():
|
if face < 1 or face > 6 or count < 1 or count > _total_dice():
|
||||||
return false
|
return false
|
||||||
@@ -426,18 +495,26 @@ func _phase_reveal(result: Dictionary) -> void:
|
|||||||
if v == b.face:
|
if v == b.face:
|
||||||
total += 1
|
total += 1
|
||||||
|
|
||||||
|
reveal_total = total
|
||||||
var loser := challenger if total >= b.count else int(b.seat)
|
var loser := challenger if total >= b.count else int(b.seat)
|
||||||
banner = "%d x %d angesagt - tatsächlich %d. %s verliert einen Würfel." % [
|
_log("%d x %d angesagt - tatsächlich %d. %s verliert einen Würfel." % [
|
||||||
b.count, b.face, total, seats[loser].name]
|
b.count, b.face, total, seats[loser].name])
|
||||||
_log(banner)
|
banner = "%s verliert" % seats[loser].name
|
||||||
|
|
||||||
for i in _alive_seats():
|
for i in _alive_seats():
|
||||||
table.arrange_and_show(i, host_values[i], rng)
|
table.arrange_and_show(i, host_values[i], rng)
|
||||||
table.set_cup_down(i, false)
|
table.set_cup_down(i, false)
|
||||||
if not multiplayer.get_peers().is_empty():
|
# Gezielt an die Mitspieler am Tisch: ein Peer, der nur in der Lobby sitzt,
|
||||||
_reveal.rpc(_all_values(), banner)
|
# 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()
|
_push_state()
|
||||||
await _sleep(4.5)
|
await _sleep(REVEAL_PAUSE)
|
||||||
|
if not _alive():
|
||||||
|
return
|
||||||
|
|
||||||
seats[loser].count -= 1
|
seats[loser].count -= 1
|
||||||
if seats[loser].count <= 0:
|
if seats[loser].count <= 0:
|
||||||
@@ -461,18 +538,28 @@ func _all_values() -> Array:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## process_always = false: eine offene Pause hält den Timer wirklich an.
|
||||||
func _sleep(sec: float) -> void:
|
func _sleep(sec: float) -> void:
|
||||||
await get_tree().create_timer(sec).timeout
|
if not _alive():
|
||||||
|
return
|
||||||
|
await get_tree().create_timer(sec, false).timeout
|
||||||
|
|
||||||
|
|
||||||
func _wait_until(cond: Callable, timeout: float) -> bool:
|
func _wait_until(cond: Callable, timeout: float) -> bool:
|
||||||
var t := 0.0
|
var t := 0.0
|
||||||
while t < timeout:
|
while t < timeout:
|
||||||
|
if not _alive():
|
||||||
|
return false
|
||||||
if cond.call():
|
if cond.call():
|
||||||
return true
|
return true
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
t += get_process_delta_time()
|
if not _alive():
|
||||||
return cond.call()
|
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:
|
func _log(line: String) -> void:
|
||||||
@@ -484,7 +571,7 @@ func _log(line: String) -> void:
|
|||||||
# ---------------------------------------------------------------- Sync
|
# ---------------------------------------------------------------- Sync
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
if not multiplayer.is_server() or not _stream:
|
if not _alive() or not _stream or not multiplayer.is_server():
|
||||||
return
|
return
|
||||||
_stream_acc += delta
|
_stream_acc += delta
|
||||||
if _stream_acc < 1.0 / 30.0:
|
if _stream_acc < 1.0 / 30.0:
|
||||||
@@ -492,7 +579,7 @@ func _physics_process(delta: float) -> void:
|
|||||||
_stream_acc = 0.0
|
_stream_acc = 0.0
|
||||||
for i in seats.size():
|
for i in seats.size():
|
||||||
var peer: int = seats[i].peer
|
var peer: int = seats[i].peer
|
||||||
if peer <= 1:
|
if not _peer_live(peer):
|
||||||
continue
|
continue
|
||||||
_dice_stream.rpc_id(peer, _pack(i))
|
_dice_stream.rpc_id(peer, _pack(i))
|
||||||
|
|
||||||
@@ -500,7 +587,7 @@ func _physics_process(delta: float) -> void:
|
|||||||
func _snapshot_all() -> void:
|
func _snapshot_all() -> void:
|
||||||
for i in seats.size():
|
for i in seats.size():
|
||||||
var peer: int = seats[i].peer
|
var peer: int = seats[i].peer
|
||||||
if peer > 1:
|
if _peer_live(peer):
|
||||||
_dice_snapshot.rpc_id(peer, _pack(i))
|
_dice_snapshot.rpc_id(peer, _pack(i))
|
||||||
|
|
||||||
|
|
||||||
@@ -521,7 +608,7 @@ func _unpack(data: PackedFloat32Array) -> void:
|
|||||||
var t := Transform3D(Basis(Quaternion(data[o + 3], data[o + 4], data[o + 5], data[o + 6])),
|
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]))
|
Vector3(data[o], data[o + 1], data[o + 2]))
|
||||||
dl[i].set_kinematic(true)
|
dl[i].set_kinematic(true)
|
||||||
dl[i].global_transform = t
|
dl[i].place(t)
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "unreliable_ordered")
|
@rpc("authority", "unreliable_ordered")
|
||||||
@@ -536,9 +623,11 @@ func _dice_snapshot(data: PackedFloat32Array) -> void:
|
|||||||
|
|
||||||
@rpc("authority", "reliable")
|
@rpc("authority", "reliable")
|
||||||
func _reveal(all_values: Array, text: String) -> void:
|
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():
|
for i in seats.size():
|
||||||
if i == local_seat:
|
|
||||||
continue
|
|
||||||
if i < all_values.size() and not all_values[i].is_empty():
|
if i < all_values.size() and not all_values[i].is_empty():
|
||||||
table.arrange_and_show(i, all_values[i], rng)
|
table.arrange_and_show(i, all_values[i], rng)
|
||||||
table.set_cup_down(i, false)
|
table.set_cup_down(i, false)
|
||||||
@@ -547,14 +636,34 @@ func _reveal(all_values: Array, text: String) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _push_state() -> void:
|
func _push_state() -> void:
|
||||||
|
if not _alive():
|
||||||
|
return
|
||||||
_refresh_labels()
|
_refresh_labels()
|
||||||
for i in seats.size():
|
for i in seats.size():
|
||||||
var peer: int = seats[i].peer
|
var peer: int = seats[i].peer
|
||||||
var st := _state_for(i)
|
|
||||||
if peer == multiplayer.get_unique_id():
|
if peer == multiplayer.get_unique_id():
|
||||||
_apply_state(st)
|
_apply_state(_state_for(i))
|
||||||
elif peer > 1:
|
elif _peer_live(peer):
|
||||||
_state.rpc_id(peer, st)
|
_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.
|
||||||
|
func _round_boosters() -> Array:
|
||||||
|
var out: Array = []
|
||||||
|
if phase == "boost" or phase == "roll":
|
||||||
|
return out
|
||||||
|
for i in seats.size():
|
||||||
|
out.append(String(chosen_boost.get(i, {}).get("id", "")))
|
||||||
|
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:
|
func _state_for(seat: int) -> Dictionary:
|
||||||
@@ -565,18 +674,22 @@ func _state_for(seat: int) -> Dictionary:
|
|||||||
elif phase == "reveal":
|
elif phase == "reveal":
|
||||||
vis = host_values[seat].duplicate() # aufgedeckt wird ohne Nebel
|
vis = host_values[seat].duplicate() # aufgedeckt wird ohne Nebel
|
||||||
else:
|
else:
|
||||||
|
var fg: Array = fogged.get(seat, [])
|
||||||
for k in host_values[seat].size():
|
for k in host_values[seat].size():
|
||||||
vis.append(-1 if fogged.get(seat, -1) == k else host_values[seat][k])
|
vis.append(-1 if fg.has(k) else host_values[seat][k])
|
||||||
var pub: Array = []
|
var pub: Array = []
|
||||||
for s in seats:
|
for s in seats:
|
||||||
pub.append({"name": s.name, "count": s.count, "alive": s.alive, "ai": s.ai})
|
pub.append({"name": s.name, "count": s.count, "alive": s.alive, "ai": s.ai})
|
||||||
return {
|
return {
|
||||||
"phase": phase, "turn": turn, "bid": bid, "seats": pub, "values": vis,
|
"phase": phase, "turn": turn, "bid": bid, "seats": pub, "values": vis,
|
||||||
"log": log_lines.duplicate(), "banner": banner,
|
"banner": banner,
|
||||||
"peek": peeked.get(seat, {}), "min_raise": min_raise.get(seat, 1),
|
"peek": peeked.get(seat, {}), "min_raise": min_raise.get(seat, 1),
|
||||||
"must_bid": must_bid.get(seat, false), "total": _total_dice(),
|
"must_bid": must_bid.get(seat, false), "total": _total_dice(),
|
||||||
"time_left": 0.0 if deadline <= 0.0 else maxf(0.0, deadline - _now()),
|
"time_left": 0.0 if deadline <= 0.0 else maxf(0.0, deadline - _now()),
|
||||||
"open": _all_values() if phase == "reveal" else [],
|
"open": _all_values() if phase == "reveal" else [],
|
||||||
|
"boosts": _round_boosters(),
|
||||||
|
"actual": reveal_total if phase == "reveal" else 0,
|
||||||
|
"bids": _seat_bids(),
|
||||||
"boost": chosen_boost.get(seat, {}).get("id", ""),
|
"boost": chosen_boost.get(seat, {}).get("id", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,6 +721,13 @@ func _apply_state(st: Dictionary) -> void:
|
|||||||
table.set_dice_visible(local_seat, true)
|
table.set_dice_visible(local_seat, true)
|
||||||
_refresh_labels()
|
_refresh_labels()
|
||||||
_apply_fog_to_table()
|
_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)
|
hud.update_state(st, local_seat)
|
||||||
|
|
||||||
|
|
||||||
@@ -659,7 +779,13 @@ func _on_pause_changed(on: bool) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _on_quit() -> void:
|
func _on_quit() -> void:
|
||||||
|
if _quitting:
|
||||||
|
return
|
||||||
|
_quitting = true
|
||||||
get_tree().paused = false
|
get_tree().paused = false
|
||||||
|
# Sofort ausblenden, damit das Verlassen unmittelbar wirkt.
|
||||||
|
table.visible = false
|
||||||
|
hud.visible = false
|
||||||
Net.leave()
|
Net.leave()
|
||||||
get_tree().change_scene_to_file("res://scenes/main.tscn")
|
get_tree().change_scene_to_file("res://scenes/main.tscn")
|
||||||
|
|
||||||
@@ -680,6 +806,8 @@ func _req_boost(id: String, target: int) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _seat_of(peer: int) -> int:
|
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():
|
for i in seats.size():
|
||||||
if seats[i].peer == peer:
|
if seats[i].peer == peer:
|
||||||
return i
|
return i
|
||||||
@@ -697,15 +825,21 @@ func _do_bid(seat: int, count: int, face: int) -> void:
|
|||||||
func _do_challenge(seat: int) -> void:
|
func _do_challenge(seat: int) -> void:
|
||||||
if seat < 0 or phase != "bid" or seat != turn or _answered.has(seat):
|
if seat < 0 or phase != "bid" or seat != turn or _answered.has(seat):
|
||||||
return
|
return
|
||||||
if bid.is_empty() or must_bid.get(seat, false):
|
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
|
return
|
||||||
_answered[seat] = _apply_action(seat, {"type": "challenge"})
|
_answered[seat] = _apply_action(seat, {"type": "challenge"})
|
||||||
|
|
||||||
|
|
||||||
func _do_boost(seat: int, id: String, target: int) -> void:
|
func _do_boost(seat: int, id: String, target: int) -> void:
|
||||||
if seat < 0 or phase != "boost" or chosen_boost.has(seat):
|
if seat < 0 or phase != "boost" or chosen_boost.has(seat) or not seats[seat].alive:
|
||||||
return
|
return
|
||||||
if BoosterDB.get_booster(id).is_empty():
|
# 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
|
return
|
||||||
chosen_boost[seat] = {"id": id, "target": target}
|
chosen_boost[seat] = {"id": id, "target": target}
|
||||||
_answered[seat] = true
|
_answered[seat] = true
|
||||||
|
|||||||
345
scripts/hud.gd
345
scripts/hud.gd
@@ -9,16 +9,31 @@ signal pause_changed(paused: bool)
|
|||||||
|
|
||||||
const ACCENT := Color(1.0, 0.82, 0.32)
|
const ACCENT := Color(1.0, 0.82, 0.32)
|
||||||
const DIM := Color(0.62, 0.64, 0.70)
|
const DIM := Color(0.62, 0.64, 0.70)
|
||||||
const PLAYERS_W := 300.0 ## feste Breite der Übersicht oben rechts
|
## 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(200.0, 150.0)
|
||||||
|
const TIMER_W := 52.0 ## Breite der Zeitanzeige in der Ansage-Leiste
|
||||||
|
const GRUEN := Color(0.55, 0.84, 0.55) ## vorhandener Würfel beim Aufdecken
|
||||||
|
const ROT := Color(0.94, 0.55, 0.52) ## fehlender Würfel beim Aufdecken
|
||||||
|
|
||||||
var root: Control
|
var root: Control
|
||||||
var players_box: VBoxContainer
|
var players_box: VBoxContainer
|
||||||
var bid_row: HBoxContainer
|
var bid_box: VBoxContainer
|
||||||
var hint_label: Label
|
var bid_name: Label
|
||||||
|
var bid_row: HFlowContainer
|
||||||
|
var boosts_box: VBoxContainer
|
||||||
var banner_label: Label
|
var banner_label: Label
|
||||||
var log_label: Label
|
|
||||||
|
|
||||||
var count_spin: SpinBox
|
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 face_buttons: Array[Button] = []
|
||||||
var bid_btn: Button
|
var bid_btn: Button
|
||||||
var challenge_btn: Button
|
var challenge_btn: Button
|
||||||
@@ -53,14 +68,17 @@ class DieIcon extends Control:
|
|||||||
var value := 0
|
var value := 0
|
||||||
var _box := StyleBoxFlat.new()
|
var _box := StyleBoxFlat.new()
|
||||||
|
|
||||||
func _init(v: int, px: float = 0.0) -> void:
|
func _init(v: int, px: float = 0.0, bg := Color.TRANSPARENT) -> void:
|
||||||
value = v
|
value = v
|
||||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
if px > 0.0:
|
if px > 0.0:
|
||||||
custom_minimum_size = Vector2(px, px)
|
custom_minimum_size = Vector2(px, px)
|
||||||
# sonst zieht der Container das Symbol in die Höhe und es wird unrund
|
# sonst zieht der Container das Symbol in die Höhe und es wird unrund
|
||||||
size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
_box.bg_color = Color(0.93, 0.92, 0.88) if v > 0 else Color(0.26, 0.28, 0.34)
|
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.set_corner_radius_all(4)
|
||||||
_box.border_color = Color(0, 0, 0, 0.35)
|
_box.border_color = Color(0, 0, 0, 0.35)
|
||||||
_box.set_border_width_all(1)
|
_box.set_border_width_all(1)
|
||||||
@@ -141,81 +159,95 @@ func _slot(al: float, at: float, ar: float, ab: float,
|
|||||||
func _build() -> void:
|
func _build() -> void:
|
||||||
# --- Spielerliste oben rechts
|
# --- Spielerliste oben rechts
|
||||||
var pl := _panel()
|
var pl := _panel()
|
||||||
_place(pl, 1, 0, 1, 0, -16 - PLAYERS_W, 16, -16, 16)
|
_place(pl, 1, 0, 1, 0, -16, 16, -16, 16)
|
||||||
pl.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
pl.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||||
pl.custom_minimum_size = Vector2(PLAYERS_W, 0)
|
|
||||||
root.add_child(pl)
|
root.add_child(pl)
|
||||||
players_box = VBoxContainer.new()
|
players_box = VBoxContainer.new()
|
||||||
players_box.add_theme_constant_override("separation", 4)
|
players_box.add_theme_constant_override("separation", 6)
|
||||||
pl.add_child(players_box)
|
pl.add_child(players_box)
|
||||||
|
|
||||||
# --- Aktuelles Gebot oben mittig
|
# --- Aktive Booster oben links, Breite richtet sich nach dem Inhalt
|
||||||
var bp := _panel()
|
var bo := _panel()
|
||||||
_slot(0, 0, 1, 0, 300, 12, -300, 120).add_child(bp)
|
_place(bo, 0, 0, 0, 0, 16, 16, 16, 16)
|
||||||
var bv := VBoxContainer.new()
|
root.add_child(bo)
|
||||||
bp.add_child(bv)
|
boosts_box = VBoxContainer.new()
|
||||||
bid_row = HBoxContainer.new()
|
boosts_box.add_theme_constant_override("separation", 3)
|
||||||
bid_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
bo.add_child(boosts_box)
|
||||||
bid_row.add_theme_constant_override("separation", 8)
|
|
||||||
bv.add_child(bid_row)
|
|
||||||
hint_label = _label("", 14, DIM)
|
|
||||||
hint_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
||||||
bv.add_child(hint_label)
|
|
||||||
|
|
||||||
# --- Banner in der Bildmitte
|
# --- Letzte Wette in der Tischmitte, als so viele Würfel wie angesagt
|
||||||
var banner_slot := _slot(0, 0, 1, 0, 140, 126, -140, 210)
|
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("", 30, 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 = _label("", 28, ACCENT)
|
||||||
banner_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
banner_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
banner_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
banner_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
banner_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
banner_label.custom_minimum_size = Vector2(1000, 0)
|
banner_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
banner_slot.add_child(banner_label)
|
var edge := 240.0
|
||||||
|
_place(banner_label, 0, 0, 1, 0, edge, 20, -edge, 150)
|
||||||
|
root.add_child(banner_label)
|
||||||
|
|
||||||
# --- Verlauf unten links
|
# --- Ansage: kompakte Leiste unten in der Mitte
|
||||||
var lp := _panel()
|
|
||||||
_place(lp, 0, 1, 0, 1, 16, -252, 376, -16)
|
|
||||||
root.add_child(lp)
|
|
||||||
log_label = _label("", 13, Color(0.82, 0.84, 0.9))
|
|
||||||
log_label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
|
||||||
log_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
||||||
lp.add_child(log_label)
|
|
||||||
|
|
||||||
# --- Aktionen unten rechts
|
|
||||||
action_panel = _panel()
|
action_panel = _panel()
|
||||||
_place(action_panel, 1, 1, 1, 1, -296, -282, -16, -16)
|
_slot(0, 1, 1, 1, 16, -110, -16, -16).add_child(action_panel)
|
||||||
action_panel.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
var av := HBoxContainer.new()
|
||||||
root.add_child(action_panel)
|
av.add_theme_constant_override("separation", 16)
|
||||||
var av := VBoxContainer.new()
|
|
||||||
av.add_theme_constant_override("separation", 6)
|
|
||||||
action_panel.add_child(av)
|
action_panel.add_child(av)
|
||||||
|
|
||||||
var head := HBoxContainer.new()
|
# Gegenstück zur Zeitanzeige am rechten Ende, damit die Leiste symmetrisch
|
||||||
av.add_child(head)
|
# bleibt, auch wenn dort gerade keine Zeit läuft.
|
||||||
var head_title := _label("Deine Ansage", 18, ACCENT)
|
var spacer := Control.new()
|
||||||
head_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
spacer.custom_minimum_size = Vector2(TIMER_W, 0)
|
||||||
head.add_child(head_title)
|
spacer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
timer_bid = _label("", 18, DIM)
|
av.add_child(spacer)
|
||||||
head.add_child(timer_bid)
|
|
||||||
|
|
||||||
var row := HBoxContainer.new()
|
av.add_child(_label("Anzahl", 15))
|
||||||
av.add_child(row)
|
minus_btn = Button.new()
|
||||||
var cl := _label("Anzahl", 15)
|
minus_btn.text = "−"
|
||||||
cl.custom_minimum_size = Vector2(80, 0)
|
minus_btn.custom_minimum_size = Vector2(42, 42)
|
||||||
row.add_child(cl)
|
minus_btn.pressed.connect(_step_count.bind(-1))
|
||||||
count_spin = SpinBox.new()
|
av.add_child(minus_btn)
|
||||||
count_spin.min_value = 1
|
count_label = _label("1", 22)
|
||||||
count_spin.max_value = 30
|
count_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
count_spin.value = 1
|
count_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
count_spin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
count_label.custom_minimum_size = Vector2(38, 0)
|
||||||
row.add_child(count_spin)
|
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)
|
||||||
|
|
||||||
av.add_child(_label("Augenzahl", 15))
|
|
||||||
var frow := HBoxContainer.new()
|
var frow := HBoxContainer.new()
|
||||||
frow.add_theme_constant_override("separation", 4)
|
frow.add_theme_constant_override("separation", 4)
|
||||||
av.add_child(frow)
|
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):
|
for f in range(1, 7):
|
||||||
var b := Button.new()
|
var b := Button.new()
|
||||||
b.toggle_mode = true
|
b.toggle_mode = true
|
||||||
b.custom_minimum_size = Vector2(40, 40)
|
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))
|
b.pressed.connect(_on_face.bind(f))
|
||||||
var ic := DieIcon.new(f)
|
var ic := DieIcon.new(f)
|
||||||
ic.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
ic.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
@@ -230,16 +262,22 @@ func _build() -> void:
|
|||||||
|
|
||||||
bid_btn = Button.new()
|
bid_btn = Button.new()
|
||||||
bid_btn.text = "Ansagen"
|
bid_btn.text = "Ansagen"
|
||||||
bid_btn.custom_minimum_size = Vector2(0, 36)
|
bid_btn.custom_minimum_size = Vector2(120, 42)
|
||||||
bid_btn.pressed.connect(_on_bid)
|
bid_btn.pressed.connect(_on_bid)
|
||||||
av.add_child(bid_btn)
|
av.add_child(bid_btn)
|
||||||
|
|
||||||
challenge_btn = Button.new()
|
challenge_btn = Button.new()
|
||||||
challenge_btn.text = "Anfechten"
|
challenge_btn.text = "Anfechten"
|
||||||
challenge_btn.custom_minimum_size = Vector2(0, 36)
|
challenge_btn.custom_minimum_size = Vector2(120, 42)
|
||||||
challenge_btn.pressed.connect(_on_challenge)
|
challenge_btn.pressed.connect(_on_challenge)
|
||||||
av.add_child(challenge_btn)
|
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)
|
||||||
|
|
||||||
# --- Booster-Overlay
|
# --- Booster-Overlay
|
||||||
boost_slot = _slot(0, 0, 1, 1, 0, 0, 0, 0)
|
boost_slot = _slot(0, 0, 1, 1, 0, 0, 0, 0)
|
||||||
boost_slot.visible = false
|
boost_slot.visible = false
|
||||||
@@ -303,8 +341,58 @@ func _on_quit() -> void:
|
|||||||
quit_pressed.emit()
|
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:
|
func _on_bid() -> void:
|
||||||
bid_pressed.emit(int(count_spin.value), _face)
|
bid_pressed.emit(_count, _face)
|
||||||
|
|
||||||
|
|
||||||
func _on_challenge() -> void:
|
func _on_challenge() -> void:
|
||||||
@@ -326,6 +414,14 @@ func _set_paused(on: bool) -> void:
|
|||||||
pause_changed.emit(on)
|
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:
|
func _on_face(f: int) -> void:
|
||||||
_face = f
|
_face = f
|
||||||
for i in face_buttons.size():
|
for i in face_buttons.size():
|
||||||
@@ -338,64 +434,96 @@ func update_state(st: Dictionary, my_seat: int) -> void:
|
|||||||
_my_seat = my_seat
|
_my_seat = my_seat
|
||||||
_my_values = st.values
|
_my_values = st.values
|
||||||
_phase = st.phase
|
_phase = st.phase
|
||||||
|
_actual = int(st.get("actual", 0))
|
||||||
var left := float(st.get("time_left", 0.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
|
_deadline = 0.0 if left <= 0.0 else Time.get_ticks_msec() / 1000.0 + left
|
||||||
|
|
||||||
for c in players_box.get_children():
|
_clear(players_box)
|
||||||
c.queue_free()
|
|
||||||
for i in st.seats.size():
|
for i in st.seats.size():
|
||||||
var s: Dictionary = st.seats[i]
|
var s: Dictionary = st.seats[i]
|
||||||
var turn: bool = st.phase == "bid" and i == st.turn
|
var turn: bool = st.phase == "bid" and i == st.turn
|
||||||
var col := ACCENT if turn else (Color.WHITE if s.alive else DIM)
|
var col := ACCENT if turn else (Color.WHITE if s.alive else DIM)
|
||||||
var row := HBoxContainer.new()
|
var entry := VBoxContainer.new()
|
||||||
row.add_theme_constant_override("separation", 4)
|
entry.add_theme_constant_override("separation", 2)
|
||||||
players_box.add_child(row)
|
players_box.add_child(entry)
|
||||||
var nm := _label(("▸ " if turn else " ") + s.name + (" (KI)" if s.ai else ""), 15, col)
|
var nm := _label(("▸ " if turn else "") + _short(String(s.name)) + (" (KI)" if s.ai else ""), 14, col)
|
||||||
# Der Name teilt sich den Rest der festen Panelbreite und wird
|
|
||||||
# notfalls mit Auslassungspunkten gekürzt.
|
|
||||||
nm.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
nm.clip_text = true
|
nm.clip_text = true
|
||||||
nm.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
nm.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
row.add_child(nm)
|
entry.add_child(nm)
|
||||||
if not s.alive:
|
if not s.alive:
|
||||||
row.add_child(_label("ausgeschieden", 14, DIM))
|
entry.add_child(_label(" ausgeschieden", 13, DIM))
|
||||||
continue
|
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):
|
for v: int in _seat_dice(st, i, my_seat):
|
||||||
row.add_child(DieIcon.new(v, 22.0))
|
row.add_child(DieIcon.new(v, 21.0))
|
||||||
|
|
||||||
for c in bid_row.get_children():
|
_clear(bid_row)
|
||||||
c.queue_free()
|
|
||||||
if st.bid.is_empty():
|
if st.bid.is_empty():
|
||||||
bid_row.add_child(_label("Noch kein Gebot", 26, ACCENT))
|
bid_name.text = "Kein Gebot"
|
||||||
|
bid_name.add_theme_color_override("font_color", Color(1, 1, 1, 0.5))
|
||||||
else:
|
else:
|
||||||
bid_row.add_child(_label("%s: %d ×" % [st.seats[st.bid.seat].name, st.bid.count], 26, ACCENT))
|
bid_name.text = String(st.seats[st.bid.seat].name)
|
||||||
bid_row.add_child(DieIcon.new(int(st.bid.face), 30.0))
|
bid_name.add_theme_color_override("font_color", ACCENT)
|
||||||
|
var px := _fit_dice(int(st.bid.count))
|
||||||
|
# Beim Aufdecken zeigen die Farben, wie viele der angesagten Würfel es
|
||||||
|
# wirklich gab: grün die vorhandenen, rot die fehlenden.
|
||||||
|
var faerben: bool = st.phase == "reveal"
|
||||||
|
for i in int(st.bid.count):
|
||||||
|
var bg := Color.TRANSPARENT
|
||||||
|
if faerben:
|
||||||
|
bg = GRUEN if i < _actual else ROT
|
||||||
|
bid_row.add_child(DieIcon.new(int(st.bid.face), px, bg))
|
||||||
|
|
||||||
var hints: Array = []
|
_clear(boosts_box)
|
||||||
hints.append("Würfel im Spiel: %d" % st.total)
|
var boosts: Array = st.get("boosts", [])
|
||||||
if st.boost != "":
|
var any := false
|
||||||
hints.append("Dein Booster: %s" % BoosterDB.get_booster(st.boost).get("name", ""))
|
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:
|
if int(st.min_raise) > 1:
|
||||||
hints.append("Störfeuer: du musst um mind. %d erhöhen" % st.min_raise)
|
boosts_box.add_child(_label("Störfeuer: mind. %d erhöhen" % st.min_raise, 14, ACCENT))
|
||||||
|
any = true
|
||||||
if st.must_bid:
|
if st.must_bid:
|
||||||
hints.append("Maulkorb: du musst bieten")
|
boosts_box.add_child(_label("Maulkorb: du musst bieten", 14, ACCENT))
|
||||||
hint_label.text = " | ".join(hints)
|
any = true
|
||||||
|
if not any:
|
||||||
|
boosts_box.add_child(_label("Keine Booster aktiv", 14, DIM))
|
||||||
|
|
||||||
log_label.text = "\n".join(st.log)
|
|
||||||
banner_label.text = st.banner
|
banner_label.text = st.banner
|
||||||
|
|
||||||
var my_turn: bool = st.phase == "bid" and st.turn == my_seat and st.seats[my_seat].alive
|
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
|
_my_turn = my_turn
|
||||||
action_panel.modulate = Color(1, 1, 1, 1.0 if my_turn else 0.45)
|
action_panel.modulate = Color(1, 1, 1, 1.0 if my_turn else 0.45)
|
||||||
bid_btn.disabled = not my_turn
|
bid_btn.disabled = not my_turn
|
||||||
challenge_btn.disabled = not my_turn or st.bid.is_empty() or st.must_bid
|
_count_max = maxi(1, int(st.total))
|
||||||
count_spin.max_value = 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
|
var low := 1
|
||||||
if not st.bid.is_empty():
|
if _bid_count > 0:
|
||||||
low = int(st.bid.count) + (int(st.min_raise) if int(st.min_raise) > 1 else 0)
|
low = _bid_count + (_min_raise if _min_raise > 1 else 0)
|
||||||
count_spin.min_value = clampi(low, 1, int(count_spin.max_value))
|
_count_min = clampi(low, 1, _count_max)
|
||||||
if count_spin.value < count_spin.min_value:
|
_set_count(_count)
|
||||||
count_spin.value = count_spin.min_value
|
if kam_dran:
|
||||||
|
_lowest_bid()
|
||||||
|
|
||||||
if st.phase != "boost":
|
if st.phase != "boost":
|
||||||
boost_slot.visible = false
|
boost_slot.visible = false
|
||||||
@@ -423,6 +551,22 @@ func _process(_delta: float) -> void:
|
|||||||
|
|
||||||
## Eigene Würfel sind bekannt, gegnerische nicht - ausser sie wurden gespäht
|
## Eigene Würfel sind bekannt, gegnerische nicht - ausser sie wurden gespäht
|
||||||
## oder es wird gerade aufgedeckt.
|
## 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:
|
func _seat_dice(st: Dictionary, seat: int, my_seat: int) -> Array:
|
||||||
if seat == my_seat:
|
if seat == my_seat:
|
||||||
return st.values
|
return st.values
|
||||||
@@ -451,8 +595,7 @@ func show_booster_offer(ids: Array, values: Array) -> void:
|
|||||||
_pending_boost = ""
|
_pending_boost = ""
|
||||||
target_row.visible = false
|
target_row.visible = false
|
||||||
boost_title.text = "Wähle einen Booster"
|
boost_title.text = "Wähle einen Booster"
|
||||||
for c in boost_cards.get_children():
|
_clear(boost_cards)
|
||||||
c.queue_free()
|
|
||||||
for id: String in ids:
|
for id: String in ids:
|
||||||
var b: Dictionary = BoosterDB.get_booster(id)
|
var b: Dictionary = BoosterDB.get_booster(id)
|
||||||
var card := _panel(Color(0.10, 0.12, 0.17, 0.98))
|
var card := _panel(Color(0.10, 0.12, 0.17, 0.98))
|
||||||
@@ -482,10 +625,8 @@ func _pick(id: String) -> void:
|
|||||||
return
|
return
|
||||||
_pending_boost = id
|
_pending_boost = id
|
||||||
boost_title.text = "%s - welchen Würfel?" % BoosterDB.get_booster(id).name
|
boost_title.text = "%s - welchen Würfel?" % BoosterDB.get_booster(id).name
|
||||||
for c in boost_cards.get_children():
|
_clear(boost_cards)
|
||||||
c.queue_free()
|
_clear(target_row)
|
||||||
for c in target_row.get_children():
|
|
||||||
c.queue_free()
|
|
||||||
for i in _my_values.size():
|
for i in _my_values.size():
|
||||||
var b := Button.new()
|
var b := Button.new()
|
||||||
b.custom_minimum_size = Vector2(56, 56)
|
b.custom_minimum_size = Vector2(56, 56)
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
|||||||
func _start_game(cfg: Dictionary) -> void:
|
func _start_game(cfg: Dictionary) -> void:
|
||||||
if game != null:
|
if game != null:
|
||||||
return
|
return
|
||||||
menu.queue_free()
|
if menu != null:
|
||||||
menu = null
|
menu.queue_free()
|
||||||
|
menu = null
|
||||||
game = Node3D.new()
|
game = Node3D.new()
|
||||||
game.set_script(preload("res://scripts/game.gd"))
|
game.set_script(preload("res://scripts/game.gd"))
|
||||||
game.name = "Game" # fester Pfad, sonst findet das RPC-Routing den Knoten nicht
|
game.name = "Game" # fester Pfad, sonst findet das RPC-Routing den Knoten nicht
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ const BOT_NAMES_MAX := 7
|
|||||||
var screens: Dictionary = {}
|
var screens: Dictionary = {}
|
||||||
|
|
||||||
# Einzelspieler
|
# Einzelspieler
|
||||||
var solo_bots: SpinBox
|
var solo_bots: NumberRow
|
||||||
|
|
||||||
# Mehrspieler
|
# Mehrspieler
|
||||||
var name_edit: LineEdit
|
var name_edit: LineEdit
|
||||||
var ip_edit: LineEdit
|
var ip_edit: LineEdit
|
||||||
var multi_bots: SpinBox
|
var multi_bots: NumberRow
|
||||||
var lobby_list: Label
|
var lobby_list: Label
|
||||||
var status: Label
|
var status: Label
|
||||||
var host_btn: Button
|
var host_btn: Button
|
||||||
@@ -94,9 +94,46 @@ func _button(text: String, parent: Control, action: Callable) -> Button:
|
|||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
## Eine Reihe anklickbarer Zahlen - bei so wenigen Werten schneller als eine
|
||||||
|
## Spinbox mit ihren winzigen Pfeilen.
|
||||||
|
class NumberRow extends HBoxContainer:
|
||||||
|
var value := 0
|
||||||
|
var _buttons: Array[Button] = []
|
||||||
|
var _lo := 0
|
||||||
|
|
||||||
|
func _init(lo: int, hi: int, start: int) -> void:
|
||||||
|
_lo = lo
|
||||||
|
alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
add_theme_constant_override("separation", 6)
|
||||||
|
for n in range(lo, hi + 1):
|
||||||
|
var b := Button.new()
|
||||||
|
b.text = str(n)
|
||||||
|
b.toggle_mode = true
|
||||||
|
b.custom_minimum_size = Vector2(40, 38)
|
||||||
|
b.pressed.connect(pick.bind(n))
|
||||||
|
add_child(b)
|
||||||
|
_buttons.append(b)
|
||||||
|
pick(start)
|
||||||
|
|
||||||
|
func pick(n: int) -> void:
|
||||||
|
value = n
|
||||||
|
for i in _buttons.size():
|
||||||
|
_buttons[i].button_pressed = _lo + i == n
|
||||||
|
|
||||||
|
## Werte oberhalb von `hi` sperren (im Mehrspieler sind die Plätze begrenzt).
|
||||||
|
func set_limit(hi: int) -> void:
|
||||||
|
for i in _buttons.size():
|
||||||
|
_buttons[i].disabled = _lo + i > hi
|
||||||
|
if value > hi:
|
||||||
|
pick(maxi(_lo, hi))
|
||||||
|
|
||||||
|
func set_enabled(on: bool) -> void:
|
||||||
|
for b in _buttons:
|
||||||
|
b.disabled = not on
|
||||||
|
|
||||||
|
|
||||||
func _title(box: VBoxContainer) -> void:
|
func _title(box: VBoxContainer) -> void:
|
||||||
box.add_child(_text("LIAR'S DICE 3D", 40))
|
box.add_child(_text("LIAR'S DICE 3D", 40))
|
||||||
box.add_child(_text("Würfeln - Bluffen - Boostern", 16, Color(0.6, 0.63, 0.7)))
|
|
||||||
box.add_child(HSeparator.new())
|
box.add_child(HSeparator.new())
|
||||||
|
|
||||||
|
|
||||||
@@ -113,12 +150,9 @@ func _build_single() -> Control:
|
|||||||
var box := _box(400)
|
var box := _box(400)
|
||||||
_title(box)
|
_title(box)
|
||||||
box.add_child(_text("Einzelspieler", 22, Color(1.0, 0.82, 0.32)))
|
box.add_child(_text("Einzelspieler", 22, Color(1.0, 0.82, 0.32)))
|
||||||
solo_bots = SpinBox.new()
|
box.add_child(_text("Bots", 15))
|
||||||
solo_bots.min_value = 1
|
solo_bots = NumberRow.new(1, BOT_NAMES_MAX, 3)
|
||||||
solo_bots.max_value = BOT_NAMES_MAX
|
box.add_child(solo_bots)
|
||||||
solo_bots.value = 3
|
|
||||||
box.add_child(_row("Bots", solo_bots))
|
|
||||||
box.add_child(_text("Ohne Netzwerk - du spielst allein gegen die Bots.", 13, Color(0.55, 0.58, 0.65)))
|
|
||||||
box.add_child(HSeparator.new())
|
box.add_child(HSeparator.new())
|
||||||
var row := HBoxContainer.new()
|
var row := HBoxContainer.new()
|
||||||
row.add_theme_constant_override("separation", 8)
|
row.add_theme_constant_override("separation", 8)
|
||||||
@@ -142,11 +176,9 @@ func _build_multi() -> Control:
|
|||||||
ip_edit.text = "127.0.0.1"
|
ip_edit.text = "127.0.0.1"
|
||||||
box.add_child(_row("Server-IP", ip_edit))
|
box.add_child(_row("Server-IP", ip_edit))
|
||||||
|
|
||||||
multi_bots = SpinBox.new()
|
box.add_child(_text("Bots (nur Host)", 15))
|
||||||
multi_bots.min_value = 0
|
multi_bots = NumberRow.new(0, BOT_NAMES_MAX, 1)
|
||||||
multi_bots.max_value = BOT_NAMES_MAX
|
box.add_child(multi_bots)
|
||||||
multi_bots.value = 1
|
|
||||||
box.add_child(_row("Bots (nur Host)", multi_bots))
|
|
||||||
|
|
||||||
var brow := HBoxContainer.new()
|
var brow := HBoxContainer.new()
|
||||||
brow.add_theme_constant_override("separation", 8)
|
brow.add_theme_constant_override("separation", 8)
|
||||||
@@ -174,18 +206,26 @@ func _build_multi() -> Control:
|
|||||||
# ------------------------------------------------------------ Aktionen
|
# ------------------------------------------------------------ Aktionen
|
||||||
|
|
||||||
func _on_solo_start() -> void:
|
func _on_solo_start() -> void:
|
||||||
Net.start_singleplayer("Spieler", int(solo_bots.value))
|
Net.start_singleplayer("Spieler", solo_bots.value)
|
||||||
|
|
||||||
|
|
||||||
|
## Ein leeres Namensfeld ergäbe einen namenlosen Platz am Tisch.
|
||||||
|
func _spielername() -> String:
|
||||||
|
var n := name_edit.text.strip_edges()
|
||||||
|
return n if n != "" else "Spieler"
|
||||||
|
|
||||||
|
|
||||||
func _on_host() -> void:
|
func _on_host() -> void:
|
||||||
status.text = ""
|
status.text = ""
|
||||||
if Net.host_game(name_edit.text.strip_edges()):
|
if Net.host_game(_spielername()):
|
||||||
status.text = "Server läuft auf Port %d. Warte auf Mitspieler." % Net.PORT
|
status.text = "Server läuft auf Port %d. Warte auf Mitspieler." % Net.PORT
|
||||||
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
func _on_join() -> void:
|
func _on_join() -> void:
|
||||||
status.text = "Verbinde..."
|
status.text = "Verbinde..."
|
||||||
Net.join_game(ip_edit.text.strip_edges(), name_edit.text.strip_edges())
|
Net.join_game(ip_edit.text.strip_edges(), _spielername())
|
||||||
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
func _on_leave() -> void:
|
func _on_leave() -> void:
|
||||||
@@ -202,7 +242,7 @@ func _on_back() -> void:
|
|||||||
|
|
||||||
func _on_start() -> void:
|
func _on_start() -> void:
|
||||||
var humans: int = Net.lobby.size()
|
var humans: int = Net.lobby.size()
|
||||||
var ai: int = mini(int(multi_bots.value), Net.MAX_HUMANS - humans)
|
var ai: int = mini(multi_bots.value, Net.MAX_HUMANS - humans)
|
||||||
if humans + ai < 2:
|
if humans + ai < 2:
|
||||||
status.text = "Mindestens 2 Spieler nötig (Mitspieler oder Bots)."
|
status.text = "Mindestens 2 Spieler nötig (Mitspieler oder Bots)."
|
||||||
return
|
return
|
||||||
@@ -227,7 +267,11 @@ func _refresh() -> void:
|
|||||||
join_btn.disabled = connected
|
join_btn.disabled = connected
|
||||||
leave_btn.disabled = not connected
|
leave_btn.disabled = not connected
|
||||||
start_btn.disabled = not (connected and Net.is_host)
|
start_btn.disabled = not (connected and Net.is_host)
|
||||||
multi_bots.editable = not connected or Net.is_host
|
if not connected or not Net.is_host:
|
||||||
|
multi_bots.set_enabled(not connected)
|
||||||
|
else:
|
||||||
|
multi_bots.set_enabled(true)
|
||||||
|
multi_bots.set_limit(Net.MAX_HUMANS - Net.lobby.size())
|
||||||
if Net.lobby.is_empty():
|
if Net.lobby.is_empty():
|
||||||
lobby_list.text = "- nicht verbunden -"
|
lobby_list.text = "- nicht verbunden -"
|
||||||
else:
|
else:
|
||||||
|
|||||||
225
scripts/table.gd
225
scripts/table.gd
@@ -3,14 +3,14 @@ class_name GameTable
|
|||||||
## Baut den 3D-Tisch mit Würfelzonen, Bechern, Licht und Kamera.
|
## Baut den 3D-Tisch mit Würfelzonen, Bechern, Licht und Kamera.
|
||||||
|
|
||||||
const TABLE_H := 0.95
|
const TABLE_H := 0.95
|
||||||
const ZONE_W := 1.5 ## Breite der Würfelzone (tangential)
|
const ZONE_RAD := 0.68 ## Radius der runden Würfelzone
|
||||||
const ZONE_D := 0.95 ## Tiefe der Würfelzone (radial)
|
const WALL_SEGS := 20 ## Bandensegmente - Godot kennt keinen hohlen Zylinder
|
||||||
const WALL_H := 0.55 ## unsichtbare Begrenzung, damit nichts herausspringt
|
const WALL_H := 0.55 ## unsichtbare Begrenzung, damit nichts herausspringt
|
||||||
const SEAT_ARC := ZONE_W + 0.85 ## Bogenlänge, die ein Platz am Tischrand braucht
|
const SEAT_ARC := ZONE_RAD * 2.0 + 0.85 ## Bogenlänge, die ein Platz am Tischrand braucht
|
||||||
const MIN_ZONE_R := 1.15
|
const MIN_ZONE_R := 1.15
|
||||||
const TILT_DEG := 15.0 ## Neigung der Kamera gegen die Senkrechte
|
const TILT_DEG := 15.0 ## Neigung der Kamera gegen die Senkrechte
|
||||||
const HUD_TOP := 0.13 ## Bildanteil oben für die Gebotsanzeige
|
const HUD_TOP := 0.13 ## Bildanteil oben für die Gebotsanzeige
|
||||||
const HUD_BOTTOM := 0.07 ## Bildanteil unten als Rand
|
const HUD_BOTTOM := 0.17 ## Bildanteil unten für die Ansage-Leiste
|
||||||
|
|
||||||
## Tisch und Zonenradius wachsen mit der Spielerzahl.
|
## Tisch und Zonenradius wachsen mit der Spielerzahl.
|
||||||
var table_r := 2.0
|
var table_r := 2.0
|
||||||
@@ -23,6 +23,10 @@ var zones: Array[Node3D] = []
|
|||||||
var cups: Array[Node3D] = []
|
var cups: Array[Node3D] = []
|
||||||
var labels: Array[Label3D] = []
|
var labels: Array[Label3D] = []
|
||||||
var cup_tweens: Array = []
|
var cup_tweens: Array = []
|
||||||
|
var bid_counts: Array[Label3D] = []
|
||||||
|
var bid_icons: Array[Sprite3D] = []
|
||||||
|
var _bid_anchor: Array[Vector3] = []
|
||||||
|
var plates: Array[Sprite3D] = []
|
||||||
var dice: Array = [] ## seat -> Array[Die]
|
var dice: Array = [] ## seat -> Array[Die]
|
||||||
|
|
||||||
var camera: Camera3D
|
var camera: Camera3D
|
||||||
@@ -32,7 +36,7 @@ func build(count: int, local_index: int) -> void:
|
|||||||
seat_count = count
|
seat_count = count
|
||||||
local_seat = local_index
|
local_seat = local_index
|
||||||
zone_r = maxf(MIN_ZONE_R, count * SEAT_ARC / TAU)
|
zone_r = maxf(MIN_ZONE_R, count * SEAT_ARC / TAU)
|
||||||
table_r = zone_r + ZONE_D * 0.5 + 0.45
|
table_r = zone_r + ZONE_RAD + 0.45
|
||||||
_build_room()
|
_build_room()
|
||||||
seat_dir.clear()
|
seat_dir.clear()
|
||||||
for i in count:
|
for i in count:
|
||||||
@@ -131,40 +135,46 @@ func _build_seat(i: int) -> void:
|
|||||||
add_child(zone)
|
add_child(zone)
|
||||||
zones.append(zone)
|
zones.append(zone)
|
||||||
|
|
||||||
|
# Runde Bande aus Segmenten - ein hohler Zylinder ist als Kollisionsform
|
||||||
|
# nicht vorgesehen. Nur Kollision, kein Mesh: der sichtbare Rand ist ein Torus.
|
||||||
var walls := StaticBody3D.new()
|
var walls := StaticBody3D.new()
|
||||||
zone.add_child(walls)
|
zone.add_child(walls)
|
||||||
# lokale x = radial (Tiefe), lokale z = tangential (Breite)
|
var seg_w := TAU * ZONE_RAD / WALL_SEGS * 1.25 # Überlappung, damit keine Lücke bleibt
|
||||||
var specs := [
|
for k in WALL_SEGS:
|
||||||
[Vector3(ZONE_D * 0.5, WALL_H * 0.5, 0), Vector3(0.12, WALL_H, ZONE_W)],
|
var ang := TAU * k / WALL_SEGS
|
||||||
[Vector3(-ZONE_D * 0.5, WALL_H * 0.5, 0), Vector3(0.12, WALL_H, ZONE_W)],
|
|
||||||
[Vector3(0, WALL_H * 0.5, ZONE_W * 0.5), Vector3(ZONE_D, WALL_H, 0.12)],
|
|
||||||
[Vector3(0, WALL_H * 0.5, -ZONE_W * 0.5), Vector3(ZONE_D, WALL_H, 0.12)],
|
|
||||||
]
|
|
||||||
var rim_mat := _mat(Color(0.22, 0.15, 0.09), 0.6)
|
|
||||||
for s in specs:
|
|
||||||
var c := CollisionShape3D.new()
|
var c := CollisionShape3D.new()
|
||||||
var b := BoxShape3D.new()
|
var b := BoxShape3D.new()
|
||||||
b.size = s[1]
|
b.size = Vector3(0.12, WALL_H, seg_w)
|
||||||
c.shape = b
|
c.shape = b
|
||||||
c.position = s[0]
|
c.position = Vector3(cos(ang), 0.0, sin(ang)) * (ZONE_RAD + 0.06) \
|
||||||
|
+ Vector3(0, WALL_H * 0.5, 0)
|
||||||
|
c.rotation.y = -ang
|
||||||
walls.add_child(c)
|
walls.add_child(c)
|
||||||
var m := MeshInstance3D.new()
|
|
||||||
var bm := BoxMesh.new()
|
|
||||||
bm.size = Vector3(s[1].x, 0.05, s[1].z)
|
|
||||||
m.mesh = bm
|
|
||||||
m.position = Vector3(s[0].x, 0.025, s[0].z)
|
|
||||||
m.material_override = rim_mat
|
|
||||||
walls.add_child(m)
|
|
||||||
|
|
||||||
# Filzfläche
|
# Filzfläche
|
||||||
var felt := MeshInstance3D.new()
|
var felt := MeshInstance3D.new()
|
||||||
var fm := BoxMesh.new()
|
var fm := CylinderMesh.new()
|
||||||
fm.size = Vector3(ZONE_D, 0.01, ZONE_W)
|
fm.top_radius = ZONE_RAD
|
||||||
|
fm.bottom_radius = ZONE_RAD
|
||||||
|
fm.height = 0.012
|
||||||
|
fm.radial_segments = 32
|
||||||
felt.mesh = fm
|
felt.mesh = fm
|
||||||
felt.position = Vector3(0, 0.006, 0)
|
felt.position = Vector3(0, 0.006, 0)
|
||||||
felt.material_override = _mat(Color(0.07, 0.20, 0.13), 0.95)
|
felt.material_override = _mat(Color(0.07, 0.20, 0.13), 0.95)
|
||||||
zone.add_child(felt)
|
zone.add_child(felt)
|
||||||
|
|
||||||
|
# Sichtbarer Rand
|
||||||
|
var rim := MeshInstance3D.new()
|
||||||
|
var tm := TorusMesh.new()
|
||||||
|
tm.inner_radius = ZONE_RAD
|
||||||
|
tm.outer_radius = ZONE_RAD + 0.07
|
||||||
|
tm.rings = 32
|
||||||
|
tm.ring_segments = 8
|
||||||
|
rim.mesh = tm
|
||||||
|
rim.position = Vector3(0, 0.02, 0)
|
||||||
|
rim.material_override = _mat(Color(0.22, 0.15, 0.09), 0.6)
|
||||||
|
zone.add_child(rim)
|
||||||
|
|
||||||
# Becher
|
# Becher
|
||||||
var cup := Node3D.new()
|
var cup := Node3D.new()
|
||||||
cup.position = Vector3(0, 0.02, 0)
|
cup.position = Vector3(0, 0.02, 0)
|
||||||
@@ -196,6 +206,42 @@ func _build_seat(i: int) -> void:
|
|||||||
add_child(lbl)
|
add_child(lbl)
|
||||||
labels.append(lbl)
|
labels.append(lbl)
|
||||||
|
|
||||||
|
# Unterlage hinter Name und Ansage
|
||||||
|
_build_plate_texture()
|
||||||
|
var pl := Sprite3D.new()
|
||||||
|
pl.texture = _plate_tex
|
||||||
|
pl.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||||
|
pl.no_depth_test = true
|
||||||
|
pl.pixel_size = 1.0 / 128.0 # Grundgrösse 1 x 1 Welteinheit
|
||||||
|
pl.render_priority = -1 # immer hinter Schrift und Würfelbild
|
||||||
|
pl.visible = i != local_seat
|
||||||
|
add_child(pl)
|
||||||
|
plates.append(pl)
|
||||||
|
|
||||||
|
# Ansagezeile darunter: Anzahl als Text, Augenzahl als Würfelbild
|
||||||
|
_build_face_textures()
|
||||||
|
var unten := lbl.position + Vector3(0, -0.001, 0)
|
||||||
|
var cnt := Label3D.new()
|
||||||
|
cnt.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||||
|
cnt.font_size = 64
|
||||||
|
cnt.pixel_size = 0.0019
|
||||||
|
cnt.outline_size = 12
|
||||||
|
cnt.no_depth_test = true
|
||||||
|
cnt.modulate = Color(1.0, 0.88, 0.52)
|
||||||
|
cnt.visible = false
|
||||||
|
add_child(cnt)
|
||||||
|
bid_counts.append(cnt)
|
||||||
|
|
||||||
|
var ic := Sprite3D.new()
|
||||||
|
ic.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||||
|
ic.no_depth_test = true
|
||||||
|
ic.pixel_size = 0.0016
|
||||||
|
ic.texture = _face_tex[0]
|
||||||
|
ic.visible = false
|
||||||
|
add_child(ic)
|
||||||
|
bid_icons.append(ic)
|
||||||
|
_bid_anchor.append(unten)
|
||||||
|
|
||||||
|
|
||||||
## Fast senkrechte Aufsicht mit leichter Neigung, damit die Würfel noch
|
## Fast senkrechte Aufsicht mit leichter Neigung, damit die Würfel noch
|
||||||
## plastisch wirken. Der eigene Platz liegt unten im Bild.
|
## plastisch wirken. Der eigene Platz liegt unten im Bild.
|
||||||
@@ -229,6 +275,113 @@ func _frame_camera() -> void:
|
|||||||
var target := dir * shift + Vector3(0, TABLE_H, 0)
|
var target := dir * shift + Vector3(0, TABLE_H, 0)
|
||||||
var pos := target + (Vector3.UP * cos(tilt) + dir * sin(tilt)) * dist
|
var pos := target + (Vector3.UP * cos(tilt) + dir * sin(tilt)) * dist
|
||||||
camera.look_at_from_position(pos, target, Vector3.UP)
|
camera.look_at_from_position(pos, target, Vector3.UP)
|
||||||
|
_place_bid_lines()
|
||||||
|
|
||||||
|
|
||||||
|
## Setzt Name, Ansage und Unterlage eines Platzes. Alles wird entlang der
|
||||||
|
## Bildachsen ausgerichtet, sonst stünde bei der Aufsicht eines hinter dem
|
||||||
|
## anderen statt darunter.
|
||||||
|
func _layout_seat(i: int) -> void:
|
||||||
|
if camera == null or i >= plates.size():
|
||||||
|
return
|
||||||
|
var rechts: Vector3 = camera.global_transform.basis.x
|
||||||
|
var hoch: Vector3 = camera.global_transform.basis.y
|
||||||
|
var p: Vector3 = _bid_anchor[i]
|
||||||
|
var zeigt_ansage: bool = bid_icons[i].visible
|
||||||
|
|
||||||
|
var name_w := _label_width(labels[i])
|
||||||
|
labels[i].position = p + hoch * (0.10 if zeigt_ansage else 0.0)
|
||||||
|
|
||||||
|
var icon_w: float = 96.0 * bid_icons[i].pixel_size
|
||||||
|
var cnt_w := _label_width(bid_counts[i])
|
||||||
|
var zeile_w := cnt_w + 0.03 + icon_w
|
||||||
|
var mitte: Vector3 = p - hoch * 0.10
|
||||||
|
bid_counts[i].position = mitte - rechts * (zeile_w - cnt_w) * 0.5
|
||||||
|
bid_icons[i].position = mitte + rechts * (zeile_w - icon_w) * 0.5
|
||||||
|
|
||||||
|
var breite := maxf(name_w, zeile_w if zeigt_ansage else 0.0) + 0.14
|
||||||
|
var hoehe := 0.40 if zeigt_ansage else 0.22
|
||||||
|
plates[i].scale = Vector3(breite, hoehe, 1.0)
|
||||||
|
plates[i].position = p
|
||||||
|
plates[i].visible = labels[i].visible or zeigt_ansage
|
||||||
|
|
||||||
|
|
||||||
|
func _label_width(l: Label3D) -> float:
|
||||||
|
if l.text == "":
|
||||||
|
return 0.0
|
||||||
|
var f := ThemeDB.fallback_font
|
||||||
|
return f.get_string_size(l.text, HORIZONTAL_ALIGNMENT_LEFT, -1, l.font_size).x * l.pixel_size
|
||||||
|
|
||||||
|
|
||||||
|
func _place_bid_lines() -> void:
|
||||||
|
for i in plates.size():
|
||||||
|
_layout_seat(i)
|
||||||
|
|
||||||
|
|
||||||
|
## Würfelbilder für die Ansagen am Tisch. Einmal gezeichnet, von allen genutzt.
|
||||||
|
static var _face_tex: Array[ImageTexture] = []
|
||||||
|
|
||||||
|
static var _plate_tex: ImageTexture
|
||||||
|
|
||||||
|
## Fast deckende, abgerundete Unterlage. Halbtransparent ginge sie auf dem
|
||||||
|
## dunkelroten Becher unter.
|
||||||
|
static func _build_plate_texture() -> void:
|
||||||
|
if _plate_tex != null:
|
||||||
|
return
|
||||||
|
const S := 128
|
||||||
|
var img := Image.create(S, S, false, Image.FORMAT_RGBA8)
|
||||||
|
img.fill(Color(0, 0, 0, 0))
|
||||||
|
var r := S * 0.22
|
||||||
|
for y in S:
|
||||||
|
for x in S:
|
||||||
|
var dx := maxf(maxf(r - x, x - (S - r)), 0.0)
|
||||||
|
var dy := maxf(maxf(r - y, y - (S - r)), 0.0)
|
||||||
|
if Vector2(dx, dy).length() <= r:
|
||||||
|
img.set_pixel(x, y, Color(0.04, 0.05, 0.07, 0.90))
|
||||||
|
_plate_tex = ImageTexture.create_from_image(img)
|
||||||
|
|
||||||
|
|
||||||
|
static func _build_face_textures() -> void:
|
||||||
|
if not _face_tex.is_empty():
|
||||||
|
return
|
||||||
|
const S := 192 # doppelt gezeichnet und verkleinert = weiche Kanten
|
||||||
|
const OUT := 96
|
||||||
|
for v in range(1, 7):
|
||||||
|
var img := Image.create(S, S, false, Image.FORMAT_RGBA8)
|
||||||
|
img.fill(Color(0, 0, 0, 0))
|
||||||
|
var r := S * 0.18
|
||||||
|
for y in S:
|
||||||
|
for x in S:
|
||||||
|
var dx := maxf(maxf(r - x, x - (S - r)), 0.0)
|
||||||
|
var dy := maxf(maxf(r - y, y - (S - r)), 0.0)
|
||||||
|
if Vector2(dx, dy).length() <= r:
|
||||||
|
img.set_pixel(x, y, Color(0.95, 0.94, 0.90))
|
||||||
|
var mid := S * 0.5
|
||||||
|
var spread := S * 0.24
|
||||||
|
var pr := S * 0.105
|
||||||
|
for p: Vector2 in Die.PIPS[v]:
|
||||||
|
var cx := mid + p.x * spread
|
||||||
|
var cy := mid + p.y * spread
|
||||||
|
for y in S:
|
||||||
|
for x in S:
|
||||||
|
if Vector2(x + 0.5 - cx, y + 0.5 - cy).length() <= pr:
|
||||||
|
img.set_pixel(x, y, Color(0.09, 0.08, 0.1))
|
||||||
|
img.resize(OUT, OUT, Image.INTERPOLATE_LANCZOS)
|
||||||
|
_face_tex.append(ImageTexture.create_from_image(img))
|
||||||
|
|
||||||
|
|
||||||
|
## Zeigt unter dem Namen, was der Platz zuletzt angesagt hat (0 = nichts).
|
||||||
|
func set_bid(seat: int, count: int, face: int) -> void:
|
||||||
|
if seat >= bid_counts.size():
|
||||||
|
return
|
||||||
|
# Am eigenen Platz steht kein Name, dort läge die Zeile in den Würfeln.
|
||||||
|
var zeige := seat != local_seat and count > 0 and face >= 1 and face <= 6
|
||||||
|
bid_counts[seat].visible = zeige
|
||||||
|
bid_icons[seat].visible = zeige
|
||||||
|
if zeige:
|
||||||
|
bid_counts[seat].text = "%d ×" % count
|
||||||
|
bid_icons[seat].texture = _face_tex[face - 1]
|
||||||
|
_layout_seat(seat)
|
||||||
|
|
||||||
|
|
||||||
func _mat(col: Color, rough: float) -> StandardMaterial3D:
|
func _mat(col: Color, rough: float) -> StandardMaterial3D:
|
||||||
@@ -262,12 +415,13 @@ func throw_dice(seat: int, rng: RandomNumberGenerator) -> void:
|
|||||||
var arr: Array = dice[seat]
|
var arr: Array = dice[seat]
|
||||||
var n := arr.size()
|
var n := arr.size()
|
||||||
for i in n:
|
for i in n:
|
||||||
# eigene Spur je Würfel, damit sie nicht übereinander fallen
|
# gleichmässig auf einem Kreis verteilt, damit nichts übereinander fällt
|
||||||
var lane := ((float(i) + 0.5) / n * 2.0 - 1.0) * ZONE_W * 0.34
|
var ang := TAU * i / maxi(n, 1) + rng.randf_range(-0.15, 0.15)
|
||||||
|
var rad := ZONE_RAD * rng.randf_range(0.30, 0.46)
|
||||||
var off := Vector3(
|
var off := Vector3(
|
||||||
rng.randf_range(-ZONE_D * 0.14, ZONE_D * 0.14),
|
cos(ang) * rad,
|
||||||
0.48 + rng.randf_range(0.0, 0.10),
|
0.48 + rng.randf_range(0.0, 0.10),
|
||||||
lane + rng.randf_range(-0.03, 0.03)
|
sin(ang) * rad
|
||||||
)
|
)
|
||||||
arr[i].throw_from(zone.global_transform * off, rng)
|
arr[i].throw_from(zone.global_transform * off, rng)
|
||||||
|
|
||||||
@@ -276,12 +430,18 @@ func throw_dice(seat: int, rng: RandomNumberGenerator) -> void:
|
|||||||
func _layout_idle(seat: int) -> void:
|
func _layout_idle(seat: int) -> void:
|
||||||
var zone: Node3D = zones[seat]
|
var zone: Node3D = zones[seat]
|
||||||
var arr: Array = dice[seat]
|
var arr: Array = dice[seat]
|
||||||
|
# Bogen auf einem Kreis um die Zonenmitte - passt zur runden Bande.
|
||||||
|
# Der Schrittwinkel folgt aus dem gewünschten Abstand: Diagonale eines
|
||||||
|
# Würfels plus Luft, sonst überlappen die Bilder beim Aufdecken.
|
||||||
|
const BOGEN_R := 0.48 # etwas weiter aussen, damit das Namensschild frei bleibt
|
||||||
|
var schritt: float = (Die.SIZE * 1.7) / BOGEN_R
|
||||||
for i in arr.size():
|
for i in arr.size():
|
||||||
var x := (float(i) - (arr.size() - 1) * 0.5) * 0.24
|
var ang := PI + (float(i) - (arr.size() - 1) * 0.5) * schritt
|
||||||
var t := zone.global_transform
|
var t := zone.global_transform
|
||||||
t.origin = zone.global_transform * Vector3(0.0, Die.HALF + 0.01, x)
|
t.origin = zone.global_transform * Vector3(
|
||||||
|
cos(ang) * BOGEN_R, Die.HALF + 0.01, sin(ang) * BOGEN_R)
|
||||||
arr[i].set_kinematic(true)
|
arr[i].set_kinematic(true)
|
||||||
arr[i].global_transform = t
|
arr[i].place(t)
|
||||||
|
|
||||||
|
|
||||||
func arrange_and_show(seat: int, values: Array, rng: RandomNumberGenerator) -> void:
|
func arrange_and_show(seat: int, values: Array, rng: RandomNumberGenerator) -> void:
|
||||||
@@ -321,3 +481,4 @@ func set_cup_down(seat: int, down: bool) -> void:
|
|||||||
func set_label(seat: int, text: String, col: Color) -> void:
|
func set_label(seat: int, text: String, col: Color) -> void:
|
||||||
labels[seat].text = text
|
labels[seat].text = text
|
||||||
labels[seat].modulate = col
|
labels[seat].modulate = col
|
||||||
|
_layout_seat(seat)
|
||||||
|
|||||||
Reference in New Issue
Block a user