update
This commit is contained in:
67
README.md
67
README.md
@@ -1,7 +1,7 @@
|
|||||||
# Liar's Dice 3D
|
# Liar's Dice 3D
|
||||||
|
|
||||||
Würfel-Bluffspiel in Godot 4.7 mit echter 3D-Physik, Mehrspieler über Raum-Codes,
|
Würfel-Bluffspiel in Godot 4.7 mit echter 3D-Physik, Mehrspieler über Raum-Codes,
|
||||||
KI-Bots und einem Booster-System aus Buffs und Debuffs.
|
KI-Bots und Münzen als Buffs und Debuffs.
|
||||||
|
|
||||||
## Starten
|
## Starten
|
||||||
|
|
||||||
@@ -150,38 +150,61 @@ Mitspieler den ganzen Tisch dauerhaft blockieren.
|
|||||||
|
|
||||||
Wer keine Würfel mehr hat, scheidet aus. Der Letzte gewinnt.
|
Wer keine Würfel mehr hat, scheidet aus. Der Letzte gewinnt.
|
||||||
|
|
||||||
## Karten
|
## Münzen
|
||||||
|
|
||||||
Karten ersetzen die frühere Booster-Phase. Die kostete jede Runde eine eigene
|
Münzen ersetzen die frühere Booster-Phase. Die kostete jede Runde eine eigene
|
||||||
Wartezeit und riss den Ablauf auseinander.
|
Wartezeit und riss den Ablauf auseinander.
|
||||||
|
|
||||||
* Wer einen Würfel abgibt, **zieht eine Karte**. Das ist der einzige Nachschub,
|
* Jeder startet mit **einer** Münze.
|
||||||
also bekommt der etwas zurück, der gerade hinten liegt.
|
* Wer danach einen Würfel abgibt, **bekommt eine Münze**. Das ist der einzige
|
||||||
* Gespielt wird **jederzeit während der Ansagen**, nicht nur am eigenen Zug.
|
Nachschub, also bekommt der etwas zurück, der gerade hinten liegt.
|
||||||
* Mehr als **drei** Karten passen nicht auf die Hand.
|
* Geworfen wird **jederzeit während der Ansagen**, nicht nur am eigenen Zug.
|
||||||
* Die Hand liegt offen unten links; ein Klick spielt die Karte sofort. Nur
|
* Mehr als **drei** Münzen passen nicht in den Vorrat.
|
||||||
|
* Der Vorrat liegt offen unten links; ein Klick wirft die Münze sofort. Nur
|
||||||
*Feinschliff* und *Glücksgriff* fragen noch, welcher Würfel gemeint ist.
|
*Feinschliff* und *Glücksgriff* fragen noch, welcher Würfel gemeint ist.
|
||||||
|
|
||||||
| Karte | Art | Wirkung |
|
### Der Wurf
|
||||||
|---|---|---|
|
|
||||||
| Neuwurf | Buff | Alle eigenen Würfel neu werfen |
|
Beim Ausspielen wird die Münze geworfen und landet in der Mitte.
|
||||||
| Feinschliff | Buff | Einen gewählten eigenen Würfel neu werfen |
|
|
||||||
| Glücksgriff | Buff | Ein gewählter Würfel wird zur 6 |
|
* **Zahl** – die Grundwirkung tritt ein.
|
||||||
| Nachschub | Buff | Du bekommst einen Würfel dazu |
|
* **Wappen** – die Wirkung fällt stärker aus. Die Chance ist die Hälfte.
|
||||||
| Widerruf | Buff | Hebt Nebel, Störfeuer und Maulkorb auf, die auf dir liegen |
|
|
||||||
| Spähen | Buff | Von jedem Gegner wird ein zufälliger Würfel aufgedeckt |
|
Die Grundwirkung kommt immer. Kein Zug ist verschenkt, und die Entscheidung,
|
||||||
| Erhöhen | Debuff | Die laufende Ansage steigt um 1 |
|
wann geworfen wird, bleibt beim Spieler. Der Wurf selbst ist reine Zugabe.
|
||||||
| Absenken | Debuff | Bei jedem Gegner sinkt ein zufälliger Würfel um 1 |
|
|
||||||
| Nebel | Debuff | Jeder Gegner sieht einen eigenen Würfel nicht mehr |
|
Der Ausgang wird beim Host ausgewürfelt und mit dem Zustand verschickt. Die
|
||||||
| Störfeuer | Debuff | Gegner müssen diese Runde um mindestens 2 erhöhen |
|
Drehung im HUD ist nur die Ansicht dazu - sie endet auf der Seite, die schon
|
||||||
| Maulkorb | Debuff | Wer gerade dran ist, muss bieten und darf nicht anfechten |
|
feststeht. Würfelphysik wäre hier nicht synchron zu bekommen.
|
||||||
|
|
||||||
|
**Der Wurf verstärkt die Würfelzahl nie.** Sie ist die Kernressource; ein
|
||||||
|
Würfel mehr aus Glück würde die Partie kippen. *Nachschub* gibt einen Würfel -
|
||||||
|
das ist seine Grundwirkung, und Wappen macht daraus nur eine 6. Verstärkt
|
||||||
|
werden sonst nur Beträge und Reichweiten.
|
||||||
|
|
||||||
|
| Münze | Art | Zahl | Wappen |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Neuwurf | Buff | Alle eigenen Würfel neu werfen | Danach wird einer davon zur 6 |
|
||||||
|
| Feinschliff | Buff | Einen gewählten eigenen Würfel neu werfen | Ein zweiter, zufälliger kommt mit |
|
||||||
|
| Glücksgriff | Buff | Ein gewählter Würfel wird zur 6 | Ein zweiter, zufälliger wird auch zur 6 |
|
||||||
|
| Nachschub | Buff | Du bekommst einen Würfel dazu | Der neue Würfel zeigt sofort eine 6 |
|
||||||
|
| Widerruf | Buff | Hebt Nebel, Störfeuer und Maulkorb auf, die auf dir liegen | Die Münze bleibt dir erhalten |
|
||||||
|
| Spähen | Buff | Von jedem Gegner wird ein zufälliger Würfel aufgedeckt | Es sind zwei je Gegner |
|
||||||
|
| Erhöhen | Debuff | Die laufende Ansage steigt um 1 | Sie steigt um 2 |
|
||||||
|
| Absenken | Debuff | Bei jedem Gegner sinkt ein zufälliger Würfel um 1 | Er sinkt um 2 |
|
||||||
|
| Nebel | Debuff | Jeder Gegner sieht einen eigenen Würfel nicht mehr | Es sind zwei Würfel |
|
||||||
|
| Störfeuer | Debuff | Gegner müssen diese Runde um mindestens 2 erhöhen | Mindestens 3 |
|
||||||
|
| Maulkorb | Debuff | Wer gerade dran ist, muss bieten und darf nicht anfechten | Alle Gegner müssen bieten |
|
||||||
|
|
||||||
|
Welche Wirkungen es am Ende gibt, steht noch nicht fest. Der Wurf und die
|
||||||
|
Verstärkung sind der Kern; die Tabelle ist die aktuelle Füllung.
|
||||||
|
|
||||||
## Aufbau
|
## Aufbau
|
||||||
|
|
||||||
| Datei | Inhalt |
|
| Datei | Inhalt |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `scripts/net.gd` | Autoload: WebSocket-Verbindung, Räume, Lobby, Spielstart |
|
| `scripts/net.gd` | Autoload: WebSocket-Verbindung, Räume, Lobby, Spielstart |
|
||||||
| `scripts/boosters.gd` | Autoload: Kartenkatalog |
|
| `scripts/boosters.gd` | Autoload: Münzkatalog mit Grund- und Wappenwirkung |
|
||||||
| `scripts/game.gd` | Spiellogik (host-autoritativ), RPCs, Zustandsabgleich |
|
| `scripts/game.gd` | Spiellogik (host-autoritativ), RPCs, Zustandsabgleich |
|
||||||
| `scripts/table.gd` | Aufbau der 3D-Szene: Tisch, Würfelzonen, Becher, Kamera |
|
| `scripts/table.gd` | Aufbau der 3D-Szene: Tisch, Würfelzonen, Becher, Kamera |
|
||||||
| `scripts/die.gd` | Physik-Würfel, Ablesen der Augenzahl aus der Ruhelage |
|
| `scripts/die.gd` | Physik-Würfel, Ablesen der Augenzahl aus der Ruhelage |
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ static func _is_higher(count: int, face: int, bid: Dictionary, min_raise: int) -
|
|||||||
return count == bid.count and face > bid.face
|
return count == bid.count and face > bid.face
|
||||||
|
|
||||||
|
|
||||||
## Welcher eigene Würfel soll dran glauben? Für Karten mit Zielwahl.
|
## Welcher eigene Würfel soll dran glauben? Für Münzen mit Zielwahl.
|
||||||
static func target_for(id: String, values: Array) -> int:
|
static func target_for(id: String, values: Array) -> int:
|
||||||
if values.is_empty():
|
if values.is_empty():
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -1,70 +1,91 @@
|
|||||||
extends Node
|
extends Node
|
||||||
## Katalog aller Karten.
|
## Katalog aller Münzen.
|
||||||
##
|
##
|
||||||
## Karten gibt es nicht mehr rundenweise zur Auswahl. Wer einen Würfel
|
## Münzen gibt es nicht rundenweise zur Auswahl. Wer einen Würfel verliert,
|
||||||
## verliert, zieht eine. Gespielt wird jederzeit während der Ansagen - deshalb
|
## bekommt eine. Gespielt wird jederzeit während der Ansagen - deshalb wirkt
|
||||||
## wirkt jede Karte sofort und braucht keine eigene Phase mehr.
|
## jede Münze sofort und braucht keine eigene Phase mehr.
|
||||||
|
##
|
||||||
|
## Beim Ausspielen wird die Münze geworfen. Wappen oben heißt verstärkt. Die
|
||||||
|
## Grundwirkung kommt immer, der Wurf legt nur eine Schippe drauf. Deshalb ist
|
||||||
|
## kein Zug verschenkt, und die Entscheidung bleibt beim Spieler.
|
||||||
|
|
||||||
enum Kind { BUFF, DEBUFF }
|
enum Kind { BUFF, DEBUFF }
|
||||||
|
|
||||||
## Mehr als das passt nicht auf die Hand; sonst hortet man bis zum Schluss.
|
## Mehr als das passt nicht auf die Hand; sonst hortet man bis zum Schluss.
|
||||||
const HANDLIMIT := 3
|
const HANDLIMIT := 3
|
||||||
|
|
||||||
|
## Eine Münze hat zwei Seiten, also die Hälfte.
|
||||||
|
const WAPPEN_CHANCE := 0.5
|
||||||
|
|
||||||
|
## Die Würfelzahl wird bewusst nie verstärkt. Sie ist die Kernressource -
|
||||||
|
## ein Würfel mehr aus Glück würde die Partie kippen.
|
||||||
const ALL := [
|
const ALL := [
|
||||||
{
|
{
|
||||||
"id": "reroll_all", "kind": Kind.BUFF, "target": false,
|
"id": "reroll_all", "kind": Kind.BUFF, "target": false,
|
||||||
"name": "Neuwurf",
|
"name": "Neuwurf",
|
||||||
"desc": "Wirf alle deine Würfel noch einmal neu.",
|
"desc": "Wirf alle deine Würfel noch einmal neu.",
|
||||||
|
"stark": "Danach wird einer davon zur 6.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "reroll_one", "kind": Kind.BUFF, "target": true,
|
"id": "reroll_one", "kind": Kind.BUFF, "target": true,
|
||||||
"name": "Feinschliff",
|
"name": "Feinschliff",
|
||||||
"desc": "Wirf einen selbst gewählten Würfel neu.",
|
"desc": "Wirf einen selbst gewählten Würfel neu.",
|
||||||
|
"stark": "Ein zweiter, zufälliger Würfel kommt mit.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "set_six", "kind": Kind.BUFF, "target": true,
|
"id": "set_six", "kind": Kind.BUFF, "target": true,
|
||||||
"name": "Glücksgriff",
|
"name": "Glücksgriff",
|
||||||
"desc": "Ein gewählter Würfel wird zur 6.",
|
"desc": "Ein gewählter Würfel wird zur 6.",
|
||||||
|
"stark": "Ein zweiter, zufälliger Würfel wird auch zur 6.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gain_die", "kind": Kind.BUFF, "target": false,
|
"id": "gain_die", "kind": Kind.BUFF, "target": false,
|
||||||
"name": "Nachschub",
|
"name": "Nachschub",
|
||||||
"desc": "Du bekommst einen Würfel dazu.",
|
"desc": "Du bekommst einen Würfel dazu.",
|
||||||
|
"stark": "Der neue Würfel zeigt sofort eine 6.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cancel", "kind": Kind.BUFF, "target": false,
|
"id": "cancel", "kind": Kind.BUFF, "target": false,
|
||||||
"name": "Widerruf",
|
"name": "Widerruf",
|
||||||
"desc": "Hebt Nebel, Störfeuer und Maulkorb auf, die auf dir liegen.",
|
"desc": "Hebt Nebel, Störfeuer und Maulkorb auf, die auf dir liegen.",
|
||||||
|
"stark": "Die Münze bleibt dir erhalten.",
|
||||||
|
"behalten": true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "peek", "kind": Kind.BUFF, "target": false,
|
"id": "peek", "kind": Kind.BUFF, "target": false,
|
||||||
"name": "Spähen",
|
"name": "Spähen",
|
||||||
"desc": "Von jedem Gegner wird dir ein zufälliger Würfel aufgedeckt.",
|
"desc": "Von jedem Gegner wird dir ein zufälliger Würfel aufgedeckt.",
|
||||||
|
"stark": "Es sind zwei Würfel je Gegner.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "bid_up", "kind": Kind.DEBUFF, "target": false,
|
"id": "bid_up", "kind": Kind.DEBUFF, "target": false,
|
||||||
"name": "Erhöhen",
|
"name": "Erhöhen",
|
||||||
"desc": "Die laufende Ansage steigt um 1.",
|
"desc": "Die laufende Ansage steigt um 1.",
|
||||||
|
"stark": "Sie steigt um 2.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "enemy_lower", "kind": Kind.DEBUFF, "target": false,
|
"id": "enemy_lower", "kind": Kind.DEBUFF, "target": false,
|
||||||
"name": "Absenken",
|
"name": "Absenken",
|
||||||
"desc": "Bei jedem Gegner sinkt ein zufälliger Würfel um 1 (1 bleibt 1).",
|
"desc": "Bei jedem Gegner sinkt ein zufälliger Würfel um 1 (1 bleibt 1).",
|
||||||
|
"stark": "Er sinkt um 2.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "fog", "kind": Kind.DEBUFF, "target": false,
|
"id": "fog", "kind": Kind.DEBUFF, "target": false,
|
||||||
"name": "Nebel",
|
"name": "Nebel",
|
||||||
"desc": "Jeder Gegner verliert die Sicht auf einen seiner eigenen Würfel.",
|
"desc": "Jeder Gegner verliert die Sicht auf einen seiner eigenen Würfel.",
|
||||||
|
"stark": "Es sind zwei Würfel.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "raise_min", "kind": Kind.DEBUFF, "target": false,
|
"id": "raise_min", "kind": Kind.DEBUFF, "target": false,
|
||||||
"name": "Störfeuer",
|
"name": "Störfeuer",
|
||||||
"desc": "Alle Gegner müssen diese Runde um mindestens 2 erhöhen.",
|
"desc": "Alle Gegner müssen diese Runde um mindestens 2 erhöhen.",
|
||||||
|
"stark": "Es müssen mindestens 3 sein.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gag", "kind": Kind.DEBUFF, "target": false,
|
"id": "gag", "kind": Kind.DEBUFF, "target": false,
|
||||||
"name": "Maulkorb",
|
"name": "Maulkorb",
|
||||||
"desc": "Wer gerade dran ist, muss bieten und darf nicht anfechten.",
|
"desc": "Wer gerade dran ist, muss bieten und darf nicht anfechten.",
|
||||||
|
"stark": "Alle Gegner müssen bieten.",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -80,6 +101,11 @@ func needs_target(id: String) -> bool:
|
|||||||
return get_booster(id).get("target", false)
|
return get_booster(id).get("target", false)
|
||||||
|
|
||||||
|
|
||||||
## Eine zufällige Karte, für den, der gerade einen Würfel verloren hat.
|
## Münzen, die bei Wappen auf der Hand bleiben, statt in die Mitte zu wandern.
|
||||||
|
func keeps_on_wappen(id: String) -> bool:
|
||||||
|
return get_booster(id).get("behalten", false)
|
||||||
|
|
||||||
|
|
||||||
|
## Eine zufällige Münze, für den, der gerade einen Würfel verloren hat.
|
||||||
func zieh(rng: RandomNumberGenerator) -> String:
|
func zieh(rng: RandomNumberGenerator) -> String:
|
||||||
return String(ALL[rng.randi_range(0, ALL.size() - 1)].id)
|
return String(ALL[rng.randi_range(0, ALL.size() - 1)].id)
|
||||||
|
|||||||
187
scripts/game.gd
187
scripts/game.gd
@@ -4,6 +4,8 @@ extends Node3D
|
|||||||
const START_DICE := 5
|
const START_DICE := 5
|
||||||
const BID_TIMEOUT := 30.0
|
const BID_TIMEOUT := 30.0
|
||||||
const REVEAL_PAUSE := 5.0 ## Anzeigedauer des Rundenergebnisses
|
const REVEAL_PAUSE := 5.0 ## Anzeigedauer des Rundenergebnisses
|
||||||
|
const WURF_ZEIT := 1.1 ## wie lange die geworfene Münze zu sehen ist,
|
||||||
|
## bevor ihre Wirkung einsetzt
|
||||||
|
|
||||||
var table: GameTable
|
var table: GameTable
|
||||||
var hud: CanvasLayer
|
var hud: CanvasLayer
|
||||||
@@ -19,12 +21,13 @@ var bid: Dictionary = {}
|
|||||||
var turn := 0
|
var turn := 0
|
||||||
var starter := 0
|
var starter := 0
|
||||||
var fogged: Dictionary = {} ## seat -> Array[int] verdeckter Würfel
|
var fogged: Dictionary = {} ## seat -> Array[int] verdeckter Würfel
|
||||||
var peeked: Dictionary = {} ## viewer -> {target_seat: [idx, value]}
|
var peeked: Dictionary = {} ## viewer -> {target_seat: Array[[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 hand: Dictionary = {} ## seat -> Array[String], Karten auf der Hand
|
var hand: Dictionary = {} ## seat -> Array[String], Münzen beim Spieler
|
||||||
var letzte_karte: Dictionary = {} ## seat -> zuletzt gespielte Karte, nur Anzeige
|
var mitte: Dictionary = {} ## seat -> zuletzt in die Mitte gelegte Münze
|
||||||
var _karte_laeuft := false ## eine Karte wird gerade aufgelöst
|
var _muenze_laeuft := false ## eine Münze wird gerade aufgelöst
|
||||||
|
var _wurf_nr := 0 ## zählt die Würfe, damit das HUD neue erkennt
|
||||||
var last_bids: Dictionary = {} ## seat -> letzte Ansage dieser Runde
|
var last_bids: Dictionary = {} ## seat -> letzte Ansage dieser Runde
|
||||||
var log_lines: Array = []
|
var log_lines: Array = []
|
||||||
var phase := "lobby"
|
var phase := "lobby"
|
||||||
@@ -78,7 +81,9 @@ func setup(cfg: Dictionary) -> void:
|
|||||||
hud.pause_changed.connect(_on_pause_changed)
|
hud.pause_changed.connect(_on_pause_changed)
|
||||||
|
|
||||||
for i in seats.size():
|
for i in seats.size():
|
||||||
hand[i] = []
|
# Die Startmünze zieht nur der Host. Beim Client kommt sie mit dem
|
||||||
|
# ersten Zustand, sonst läge dort kurz eine andere.
|
||||||
|
hand[i] = [BoosterDB.zieh(rng)] if multiplayer.is_server() else []
|
||||||
table.set_dice_count(i, START_DICE)
|
table.set_dice_count(i, START_DICE)
|
||||||
table.set_dice_visible(i, false)
|
table.set_dice_visible(i, false)
|
||||||
table.set_cup_down(i, i != local_seat)
|
table.set_cup_down(i, i != local_seat)
|
||||||
@@ -113,6 +118,7 @@ func _refresh_labels() -> void:
|
|||||||
elif phase == "bid" and i == turn:
|
elif phase == "bid" and i == turn:
|
||||||
col = Color(1.0, 0.85, 0.3)
|
col = Color(1.0, 0.85, 0.3)
|
||||||
table.set_label(i, txt, col)
|
table.set_label(i, txt, col)
|
||||||
|
table.set_active_seat(turn if phase == "bid" else -1)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- Host-Ablauf
|
# ---------------------------------------------------------------- Host-Ablauf
|
||||||
@@ -199,7 +205,8 @@ func _phase_roll() -> void:
|
|||||||
peeked.clear()
|
peeked.clear()
|
||||||
min_raise.clear()
|
min_raise.clear()
|
||||||
must_bid.clear()
|
must_bid.clear()
|
||||||
letzte_karte.clear()
|
mitte.clear()
|
||||||
|
_wurf_nr = 0
|
||||||
banner = ""
|
banner = ""
|
||||||
_log("--- Neue Runde ---")
|
_log("--- Neue Runde ---")
|
||||||
|
|
||||||
@@ -254,57 +261,81 @@ func _await_settled(list: Array, timeout := 7.0) -> void:
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- Karten
|
# ---------------------------------------------------------------- Münzen
|
||||||
|
|
||||||
## Wer einen Würfel abgibt, zieht eine Karte. Das ist der einzige Nachschub.
|
## Wer einen Würfel abgibt, bekommt eine Münze. Das ist der einzige Nachschub.
|
||||||
func _zieh_karte(seat: int) -> void:
|
func _zieh_muenze(seat: int) -> void:
|
||||||
var karten: Array = hand.get(seat, [])
|
var muenzen: Array = hand.get(seat, [])
|
||||||
if karten.size() >= BoosterDB.HANDLIMIT:
|
if muenzen.size() >= BoosterDB.HANDLIMIT:
|
||||||
_log("%s hat die Hand voll und zieht nicht nach." % seats[seat].name)
|
_log("%s hat schon drei Münzen und bekommt keine." % seats[seat].name)
|
||||||
return
|
return
|
||||||
var id := BoosterDB.zieh(rng)
|
var id := BoosterDB.zieh(rng)
|
||||||
karten.append(id)
|
muenzen.append(id)
|
||||||
hand[seat] = karten
|
hand[seat] = muenzen
|
||||||
_log("%s zieht %s." % [seats[seat].name, BoosterDB.get_booster(id).name])
|
_log("%s bekommt %s." % [seats[seat].name, BoosterDB.get_booster(id).name])
|
||||||
|
|
||||||
|
|
||||||
|
## Ausspielen heißt: Münze in die Mitte legen und werfen. Wappen verstärkt,
|
||||||
|
## Zahl lässt es bei der Grundwirkung. Beides hilft - verschenkt ist nie ein Zug.
|
||||||
func _do_play(seat: int, id: String, target: int) -> void:
|
func _do_play(seat: int, id: String, target: int) -> void:
|
||||||
if seat < 0 or phase != "bid" or not seats[seat].alive:
|
if seat < 0 or phase != "bid" or not seats[seat].alive:
|
||||||
return
|
return
|
||||||
# Zwei Karten gleichzeitig würden sich beim Würfeln in die Quere kommen.
|
# Zwei Münzen gleichzeitig würden sich beim Würfeln in die Quere kommen.
|
||||||
if _karte_laeuft:
|
if _muenze_laeuft:
|
||||||
return
|
return
|
||||||
var karten: Array = hand.get(seat, [])
|
var muenzen: Array = hand.get(seat, [])
|
||||||
if not karten.has(id):
|
if not muenzen.has(id):
|
||||||
return
|
return
|
||||||
karten.erase(id)
|
muenzen.erase(id)
|
||||||
hand[seat] = karten
|
hand[seat] = muenzen
|
||||||
letzte_karte[seat] = id
|
_muenze_laeuft = true
|
||||||
_log("%s spielt %s." % [seats[seat].name, BoosterDB.get_booster(id).name])
|
|
||||||
_karte_laeuft = true
|
var stark := rng.randf() < BoosterDB.WAPPEN_CHANCE
|
||||||
await _wirkung(seat, id, target)
|
_wurf_nr += 1
|
||||||
_karte_laeuft = false
|
mitte[seat] = {"id": id, "stark": stark, "nr": _wurf_nr}
|
||||||
|
_log("%s wirft %s - %s." % [seats[seat].name,
|
||||||
|
BoosterDB.get_booster(id).name, "Wappen" if stark else "Zahl"])
|
||||||
|
|
||||||
|
# Erst die Münze zeigen, dann wirken lassen. Andersherum sähe man die
|
||||||
|
# Folge vor der Ursache.
|
||||||
|
_push_state()
|
||||||
|
await _sleep(WURF_ZEIT)
|
||||||
|
if _alive():
|
||||||
|
await _wirkung(seat, id, target, stark)
|
||||||
|
_muenze_laeuft = false
|
||||||
if not _alive():
|
if not _alive():
|
||||||
return
|
return
|
||||||
|
# Der Widerruf hat keine eigene Verstärkung; bei Wappen bleibt er liegen.
|
||||||
|
if stark and BoosterDB.keeps_on_wappen(id):
|
||||||
|
var zurueck: Array = hand.get(seat, [])
|
||||||
|
if zurueck.size() < BoosterDB.HANDLIMIT:
|
||||||
|
zurueck.append(id)
|
||||||
|
hand[seat] = zurueck
|
||||||
_snapshot_all()
|
_snapshot_all()
|
||||||
_push_state()
|
_push_state()
|
||||||
|
|
||||||
|
|
||||||
func _wirkung(seat: int, id: String, target: int) -> void:
|
func _wirkung(seat: int, id: String, target: int, stark: bool) -> void:
|
||||||
var vals: Array = host_values[seat]
|
var vals: Array = host_values[seat]
|
||||||
var ziel: int = clampi(target, 0, maxi(0, vals.size() - 1))
|
var ziel: int = clampi(target, 0, maxi(0, vals.size() - 1))
|
||||||
match id:
|
match id:
|
||||||
"reroll_all":
|
"reroll_all":
|
||||||
await _neu_werfen(seat, range(vals.size()))
|
await _neu_werfen(seat, range(vals.size()))
|
||||||
|
if stark and not host_values[seat].is_empty():
|
||||||
|
_set_die(seat, rng.randi_range(0, host_values[seat].size() - 1), 6)
|
||||||
"reroll_one":
|
"reroll_one":
|
||||||
await _neu_werfen(seat, [ziel])
|
await _neu_werfen(seat, _ziel_und_zufall(seat, ziel, stark))
|
||||||
"set_six":
|
"set_six":
|
||||||
_set_die(seat, ziel, 6)
|
for k: int in _ziel_und_zufall(seat, ziel, stark):
|
||||||
|
_set_die(seat, k, 6)
|
||||||
"gain_die":
|
"gain_die":
|
||||||
seats[seat].count += 1
|
seats[seat].count += 1
|
||||||
table.set_dice_count(seat, seats[seat].count)
|
table.set_dice_count(seat, seats[seat].count)
|
||||||
host_values[seat].append(1)
|
host_values[seat].append(1)
|
||||||
await _neu_werfen(seat, [host_values[seat].size() - 1])
|
var neu: int = host_values[seat].size() - 1
|
||||||
|
await _neu_werfen(seat, [neu])
|
||||||
|
if stark:
|
||||||
|
_set_die(seat, neu, 6)
|
||||||
"cancel":
|
"cancel":
|
||||||
fogged.erase(seat)
|
fogged.erase(seat)
|
||||||
min_raise.erase(seat)
|
min_raise.erase(seat)
|
||||||
@@ -312,33 +343,69 @@ func _wirkung(seat: int, id: String, target: int) -> void:
|
|||||||
"peek":
|
"peek":
|
||||||
var info := {}
|
var info := {}
|
||||||
for o in _alive_seats():
|
for o in _alive_seats():
|
||||||
if o != seat and host_values[o].size() > 0:
|
if o != seat:
|
||||||
var k := rng.randi_range(0, host_values[o].size() - 1)
|
info[o] = _zufalls_paare(o, 2 if stark else 1)
|
||||||
info[o] = [k, host_values[o][k]]
|
|
||||||
peeked[seat] = info
|
peeked[seat] = info
|
||||||
"bid_up":
|
"bid_up":
|
||||||
if not bid.is_empty():
|
if not bid.is_empty():
|
||||||
bid.count = int(bid.count) + 1
|
bid.count = int(bid.count) + (2 if stark else 1)
|
||||||
last_bids[int(bid.seat)] = {"count": bid.count, "face": bid.face}
|
last_bids[int(bid.seat)] = {"count": bid.count, "face": bid.face}
|
||||||
"enemy_lower":
|
"enemy_lower":
|
||||||
|
var minus := 2 if stark else 1
|
||||||
for o in _alive_seats():
|
for o in _alive_seats():
|
||||||
if o != seat and host_values[o].size() > 0:
|
if o != seat:
|
||||||
var k := rng.randi_range(0, host_values[o].size() - 1)
|
for k: int in _zufalls_idx(o, 1):
|
||||||
_set_die(o, k, maxi(1, host_values[o][k] - 1))
|
_set_die(o, k, maxi(1, int(host_values[o][k]) - minus))
|
||||||
"fog":
|
"fog":
|
||||||
for o in _alive_seats():
|
for o in _alive_seats():
|
||||||
if o != seat and host_values[o].size() > 0:
|
if o == seat:
|
||||||
var k := rng.randi_range(0, host_values[o].size() - 1)
|
continue
|
||||||
var liste: Array = fogged.get(o, [])
|
var liste: Array = fogged.get(o, [])
|
||||||
|
for k: int in _zufalls_idx(o, 2 if stark else 1):
|
||||||
if not liste.has(k):
|
if not liste.has(k):
|
||||||
liste.append(k)
|
liste.append(k)
|
||||||
fogged[o] = liste
|
fogged[o] = liste
|
||||||
"raise_min":
|
"raise_min":
|
||||||
for o in _alive_seats():
|
for o in _alive_seats():
|
||||||
if o != seat:
|
if o != seat:
|
||||||
min_raise[o] = 2
|
min_raise[o] = 3 if stark else 2
|
||||||
"gag":
|
"gag":
|
||||||
must_bid[turn] = true
|
if stark:
|
||||||
|
for o in _alive_seats():
|
||||||
|
if o != seat:
|
||||||
|
must_bid[o] = true
|
||||||
|
else:
|
||||||
|
must_bid[turn] = true
|
||||||
|
|
||||||
|
|
||||||
|
## Bis zu `n` verschiedene Würfelplätze eines Sitzes, zufällig gezogen.
|
||||||
|
func _zufalls_idx(seat: int, n: int) -> Array:
|
||||||
|
var frei: Array = range(host_values[seat].size())
|
||||||
|
var out: Array = []
|
||||||
|
while out.size() < n and not frei.is_empty():
|
||||||
|
out.append(frei.pop_at(rng.randi_range(0, frei.size() - 1)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
func _zufalls_paare(seat: int, n: int) -> Array:
|
||||||
|
var out: Array = []
|
||||||
|
for k: int in _zufalls_idx(seat, n):
|
||||||
|
out.append([k, host_values[seat][k]])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Der selbst gewählte Würfel, bei Wappen zusätzlich ein anderer zufälliger.
|
||||||
|
func _ziel_und_zufall(seat: int, ziel: int, stark: bool) -> Array:
|
||||||
|
var out: Array = [ziel]
|
||||||
|
if not stark:
|
||||||
|
return out
|
||||||
|
var frei: Array = []
|
||||||
|
for k in host_values[seat].size():
|
||||||
|
if k != ziel:
|
||||||
|
frei.append(k)
|
||||||
|
if not frei.is_empty():
|
||||||
|
out.append(frei[rng.randi_range(0, frei.size() - 1)])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
## Würfel neu werfen und die Augenzahl danach übernehmen.
|
## Würfel neu werfen und die Augenzahl danach übernehmen.
|
||||||
@@ -386,8 +453,8 @@ func _phase_bid() -> Dictionary:
|
|||||||
_push_state()
|
_push_state()
|
||||||
if seats[turn].ai:
|
if seats[turn].ai:
|
||||||
await _sleep(1.1)
|
await _sleep(1.1)
|
||||||
# Bots spielen ihre Karten, bevor sie ansagen.
|
# Bots werfen ihre Münzen, bevor sie ansagen.
|
||||||
await _bot_karte(turn)
|
await _bot_muenze(turn)
|
||||||
if not _alive():
|
if not _alive():
|
||||||
return {}
|
return {}
|
||||||
var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid,
|
var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid,
|
||||||
@@ -413,16 +480,16 @@ func _phase_bid() -> Dictionary:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
## Ein Bot legt manchmal eine Karte, statt sie bis zum Ausscheiden zu horten.
|
## Ein Bot wirft manchmal eine Münze, statt sie bis zum Ausscheiden zu horten.
|
||||||
func _bot_karte(seat: int) -> void:
|
func _bot_muenze(seat: int) -> void:
|
||||||
var karten: Array = hand.get(seat, [])
|
var muenzen: Array = hand.get(seat, [])
|
||||||
if karten.is_empty():
|
if muenzen.is_empty():
|
||||||
return
|
return
|
||||||
# Volle Hand: raus damit. Sonst nur ab und zu, sonst ist sofort alles weg.
|
# Voller Vorrat: raus damit. Sonst nur ab und zu, sonst ist alles sofort weg.
|
||||||
var lust := 1.0 if karten.size() >= BoosterDB.HANDLIMIT else 0.35
|
var lust := 1.0 if muenzen.size() >= BoosterDB.HANDLIMIT else 0.35
|
||||||
if rng.randf() > lust:
|
if rng.randf() > lust:
|
||||||
return
|
return
|
||||||
var id: String = karten[rng.randi_range(0, karten.size() - 1)]
|
var id: String = muenzen[rng.randi_range(0, muenzen.size() - 1)]
|
||||||
if id == "bid_up" and bid.is_empty():
|
if id == "bid_up" and bid.is_empty():
|
||||||
return
|
return
|
||||||
await _do_play(seat, id, DiceAI.target_for(id, host_values[seat]))
|
await _do_play(seat, id, DiceAI.target_for(id, host_values[seat]))
|
||||||
@@ -510,17 +577,20 @@ func _phase_reveal(result: Dictionary) -> void:
|
|||||||
return
|
return
|
||||||
|
|
||||||
seats[loser].count -= 1
|
seats[loser].count -= 1
|
||||||
_zieh_karte(loser)
|
|
||||||
if seats[loser].count <= 0:
|
if seats[loser].count <= 0:
|
||||||
seats[loser].alive = false
|
seats[loser].alive = false
|
||||||
seats[loser].count = 0
|
seats[loser].count = 0
|
||||||
host_values[loser] = []
|
host_values[loser] = []
|
||||||
table.set_dice_count(loser, 0)
|
table.set_dice_count(loser, 0)
|
||||||
_log("%s scheidet aus." % seats[loser].name)
|
_log("%s scheidet aus." % seats[loser].name)
|
||||||
|
else:
|
||||||
|
_zieh_muenze(loser)
|
||||||
starter = loser if seats[loser].alive else _next_alive(loser)
|
starter = loser if seats[loser].alive else _next_alive(loser)
|
||||||
|
# Nur wer noch spielt, bekommt den Becher wieder. Auf einer leeren Zone
|
||||||
|
# sähe er aus, als lägen dort noch Würfel.
|
||||||
for i in seats.size():
|
for i in seats.size():
|
||||||
table.set_dice_visible(i, false)
|
table.set_dice_visible(i, false)
|
||||||
table.set_cup_down(i, true)
|
table.set_cup_down(i, seats[i].alive)
|
||||||
banner = ""
|
banner = ""
|
||||||
_push_state()
|
_push_state()
|
||||||
|
|
||||||
@@ -645,14 +715,14 @@ func _push_state() -> void:
|
|||||||
_state.rpc_id(peer, _state_for(i))
|
_state.rpc_id(peer, _state_for(i))
|
||||||
|
|
||||||
|
|
||||||
## Wer hat diese Runde schon eine Karte gespielt? Beim Würfeln steht noch
|
## Welche Münze liegt von wem in der Mitte? Beim Würfeln steht noch nichts
|
||||||
## nichts an, danach ist es offen.
|
## an, danach ist es offen.
|
||||||
func _round_boosters() -> Array:
|
func _round_boosters() -> Array:
|
||||||
var out: Array = []
|
var out: Array = []
|
||||||
if phase == "roll":
|
if phase == "roll":
|
||||||
return out
|
return out
|
||||||
for i in seats.size():
|
for i in seats.size():
|
||||||
out.append(String(letzte_karte.get(i, "")))
|
out.append(mitte.get(i, {}))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -688,7 +758,6 @@ func _state_for(seat: int) -> Dictionary:
|
|||||||
"boosts": _round_boosters(),
|
"boosts": _round_boosters(),
|
||||||
"actual": reveal_total if phase == "reveal" else 0,
|
"actual": reveal_total if phase == "reveal" else 0,
|
||||||
"bids": _seat_bids(),
|
"bids": _seat_bids(),
|
||||||
"boost": String(letzte_karte.get(seat, "")),
|
|
||||||
"hand": (hand.get(seat, []) as Array).duplicate(),
|
"hand": (hand.get(seat, []) as Array).duplicate(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -719,7 +788,7 @@ func _apply_state(st: Dictionary) -> void:
|
|||||||
for i in seats.size():
|
for i in seats.size():
|
||||||
if i != local_seat:
|
if i != local_seat:
|
||||||
table.set_dice_visible(i, false)
|
table.set_dice_visible(i, false)
|
||||||
table.set_cup_down(i, true)
|
table.set_cup_down(i, seats[i].alive)
|
||||||
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()
|
||||||
|
|||||||
380
scripts/hud.gd
380
scripts/hud.gd
@@ -13,6 +13,7 @@ const DIM := Color(0.62, 0.64, 0.70)
|
|||||||
## Wette werden so gross gewählt, dass sie dort hineinpassen.
|
## Wette werden so gross gewählt, dass sie dort hineinpassen.
|
||||||
const BID_BOX := Vector2(150.0, 108.0)
|
const BID_BOX := Vector2(150.0, 108.0)
|
||||||
const TIMER_W := 52.0 ## Breite der Zeitanzeige in der Ansage-Leiste
|
const TIMER_W := 52.0 ## Breite der Zeitanzeige in der Ansage-Leiste
|
||||||
|
const INFO_W := 280.0 ## Textbreite im Erklärfeld beim Überfahren
|
||||||
const GRUEN := Color(0.55, 0.84, 0.55) ## überzähliger Würfel beim Aufdecken
|
const GRUEN := Color(0.55, 0.84, 0.55) ## überzähliger Würfel beim Aufdecken
|
||||||
const ROT := Color(0.94, 0.55, 0.52) ## fehlender Würfel beim Aufdecken
|
const ROT := Color(0.94, 0.55, 0.52) ## fehlender Würfel beim Aufdecken
|
||||||
|
|
||||||
@@ -47,6 +48,14 @@ var pause_hint: Label
|
|||||||
var boost_panel: PanelContainer
|
var boost_panel: PanelContainer
|
||||||
var boost_cards: HBoxContainer
|
var boost_cards: HBoxContainer
|
||||||
var boost_title: Label
|
var boost_title: Label
|
||||||
|
var turn_label: Label
|
||||||
|
var info_panel: PanelContainer
|
||||||
|
var info_name: Label
|
||||||
|
var info_desc: Label
|
||||||
|
var info_stark: Label
|
||||||
|
var wurf_slot: CenterContainer
|
||||||
|
var wurf_icon: MuenzIcon
|
||||||
|
var wurf_label: Label
|
||||||
var timer_bid: Label
|
var timer_bid: Label
|
||||||
var timer_boost: Label
|
var timer_boost: Label
|
||||||
var target_row: HBoxContainer
|
var target_row: HBoxContainer
|
||||||
@@ -54,24 +63,44 @@ var target_row: HBoxContainer
|
|||||||
var _face := 1
|
var _face := 1
|
||||||
|
|
||||||
|
|
||||||
## Ein gezeichnetes Würfelbild: Augen 1-6 oder ein Fragezeichen bei unbekanntem
|
## Gezeichnete Münze. Bilder als Dateien gibt es im Projekt nicht; alles wird
|
||||||
## Wert. Wird überall dort benutzt, wo früher Ziffern oder Sternchen standen.
|
## zur Laufzeit gemalt, so wie die Würfel auch. Vorn das Sinnbild der Wirkung
|
||||||
## Gezeichnetes Sinnbild je Karte. Bilder als Dateien gibt es im Projekt nicht;
|
## (Wappen), hinten die glatte Zahlseite.
|
||||||
## alles wird zur Laufzeit gemalt, so wie die Würfel auch.
|
class MuenzIcon extends Control:
|
||||||
class KartenIcon extends Control:
|
const RAND := Color(0.40, 0.29, 0.11) ## Prägekante
|
||||||
var id := ""
|
const FLAECHE := Color(0.78, 0.60, 0.26) ## Messing
|
||||||
var farbe := Color.WHITE
|
const GLANZ := Color(0.91, 0.75, 0.39) ## Lichtkante
|
||||||
|
const GRAVUR := Color(0.19, 0.13, 0.04) ## eingeschlagenes Zeichen
|
||||||
|
|
||||||
func _init(karte: String, col: Color, px: float) -> void:
|
var id := ""
|
||||||
id = karte
|
var farbe := GRAVUR ## Zeichenfarbe, von den Zeichenhilfen benutzt
|
||||||
farbe = col
|
var kind_col := Color.WHITE
|
||||||
|
var seite := 0 ## 0 = Wappen (Sinnbild), 1 = Zahl
|
||||||
|
|
||||||
|
func _init(muenze: String, col: Color, px: float) -> void:
|
||||||
|
id = muenze
|
||||||
|
kind_col = col
|
||||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
custom_minimum_size = Vector2(px, px)
|
custom_minimum_size = Vector2(px, px)
|
||||||
size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
|
pivot_offset = Vector2(px, px) * 0.5
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
var s := minf(size.x, size.y)
|
var s := minf(size.x, size.y)
|
||||||
var m := size * 0.5
|
var m := size * 0.5
|
||||||
|
var r := s * 0.5
|
||||||
|
draw_circle(m, r, RAND)
|
||||||
|
draw_circle(m - Vector2(r, r) * 0.05, r * 0.95, GLANZ)
|
||||||
|
draw_circle(m, r * 0.89, FLAECHE)
|
||||||
|
# Der Farbring sagt, ob die Münze dir hilft oder den anderen schadet.
|
||||||
|
draw_arc(m, r * 0.79, 0, TAU, 40, kind_col, maxf(1.0, s * 0.05))
|
||||||
|
if seite == 1:
|
||||||
|
draw_arc(m, r * 0.52, 0, TAU, 32, GRAVUR, maxf(1.0, s * 0.04))
|
||||||
|
draw_circle(m, s * 0.09, GRAVUR)
|
||||||
|
return
|
||||||
|
_zeichen(m, s * 0.74)
|
||||||
|
|
||||||
|
func _zeichen(m: Vector2, s: float) -> void:
|
||||||
match id:
|
match id:
|
||||||
"reroll_all":
|
"reroll_all":
|
||||||
_wuerfel(m + Vector2(-s * 0.16, -s * 0.06), s * 0.42, 5)
|
_wuerfel(m + Vector2(-s * 0.16, -s * 0.06), s * 0.42, 5)
|
||||||
@@ -115,7 +144,7 @@ class KartenIcon extends Control:
|
|||||||
var punkt := kante * 0.10
|
var punkt := kante * 0.10
|
||||||
var weg := kante * 0.26
|
var weg := kante * 0.26
|
||||||
for p: Vector2 in DieIcon.PIPS[augen]:
|
for p: Vector2 in DieIcon.PIPS[augen]:
|
||||||
draw_circle(mitte + p * weg, punkt, Color(0.08, 0.09, 0.12))
|
draw_circle(mitte + p * weg, punkt, FLAECHE)
|
||||||
|
|
||||||
## Offener Kreisbogen mit Spitze - das Zeichen fürs Neuwerfen.
|
## Offener Kreisbogen mit Spitze - das Zeichen fürs Neuwerfen.
|
||||||
func _bogen(mitte: Vector2, r: float) -> void:
|
func _bogen(mitte: Vector2, r: float) -> void:
|
||||||
@@ -152,6 +181,8 @@ class KartenIcon extends Control:
|
|||||||
draw_circle(mitte, s * 0.11, farbe)
|
draw_circle(mitte, s * 0.11, farbe)
|
||||||
|
|
||||||
|
|
||||||
|
## Ein gezeichnetes Würfelbild: Augen 1-6 oder ein Fragezeichen bei unbekanntem
|
||||||
|
## Wert. Wird überall dort benutzt, wo früher Ziffern oder Sternchen standen.
|
||||||
class DieIcon extends Control:
|
class DieIcon extends Control:
|
||||||
const PIPS := {
|
const PIPS := {
|
||||||
1: [Vector2(0, 0)],
|
1: [Vector2(0, 0)],
|
||||||
@@ -199,11 +230,24 @@ var _deadline := 0.0 ## lokaler Zeitstempel, 0 = keine Frist
|
|||||||
var _phase := ""
|
var _phase := ""
|
||||||
var _my_turn := false
|
var _my_turn := false
|
||||||
var _pending_boost := ""
|
var _pending_boost := ""
|
||||||
|
var _letzter_wurf := 0 ## Nummer des zuletzt gezeigten Münzwurfs
|
||||||
|
var _info_ctrl: Control ## worauf die Maus gerade liegt
|
||||||
|
var _zug_box := StyleBoxFlat.new() ## Hinterlegung der Zeile, die dran ist
|
||||||
|
var _leer_box := StyleBoxEmpty.new()
|
||||||
|
var _hand_sig := "" ## Abbild des Vorrats, gegen unnötigen Neuaufbau
|
||||||
|
var _mitte_sig := "" ## dasselbe für die Liste der gelegten Münzen
|
||||||
|
var _wurf_tween: Tween
|
||||||
var _my_values: Array = []
|
var _my_values: Array = []
|
||||||
var _my_seat := 0
|
var _my_seat := 0
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
_zug_box.bg_color = Color(0.30, 0.24, 0.07, 0.85)
|
||||||
|
_zug_box.set_corner_radius_all(6)
|
||||||
|
_zug_box.border_color = ACCENT
|
||||||
|
_zug_box.set_border_width_all(1)
|
||||||
|
_zug_box.set_content_margin_all(5)
|
||||||
|
_leer_box.set_content_margin_all(5)
|
||||||
layer = 1
|
layer = 1
|
||||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
root = Control.new()
|
root = Control.new()
|
||||||
@@ -300,6 +344,13 @@ func _build() -> void:
|
|||||||
_place(banner_label, 0, 0, 1, 0, edge, 20, -edge, 150)
|
_place(banner_label, 0, 0, 1, 0, edge, 20, -edge, 150)
|
||||||
root.add_child(banner_label)
|
root.add_child(banner_label)
|
||||||
|
|
||||||
|
# --- Wer ist dran: direkt über der Ansageleiste, wo der Blick ohnehin liegt
|
||||||
|
turn_label = _label("", 19, ACCENT)
|
||||||
|
turn_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
turn_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_place(turn_label, 0, 1, 1, 1, 16, -146, -16, -114)
|
||||||
|
root.add_child(turn_label)
|
||||||
|
|
||||||
# --- Ansage: kompakte Leiste unten in der Mitte
|
# --- Ansage: kompakte Leiste unten in der Mitte
|
||||||
action_panel = _panel()
|
action_panel = _panel()
|
||||||
_slot(0, 1, 1, 1, 16, -110, -16, -16).add_child(action_panel)
|
_slot(0, 1, 1, 1, 16, -110, -16, -16).add_child(action_panel)
|
||||||
@@ -376,20 +427,56 @@ func _build() -> void:
|
|||||||
timer_bid.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
timer_bid.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
av.add_child(timer_bid)
|
av.add_child(timer_bid)
|
||||||
|
|
||||||
# --- Handkarten unten links, über der Ansageleiste
|
# --- Münzvorrat unten links, über der Ansageleiste
|
||||||
hand_panel = _panel()
|
hand_panel = _panel()
|
||||||
_place(hand_panel, 0, 1, 0, 1, 16, -300, 16, -124)
|
_place(hand_panel, 0, 1, 0, 1, 16, -300, 16, -124)
|
||||||
root.add_child(hand_panel)
|
root.add_child(hand_panel)
|
||||||
var hv := VBoxContainer.new()
|
var hv := VBoxContainer.new()
|
||||||
hv.add_theme_constant_override("separation", 4)
|
hv.add_theme_constant_override("separation", 4)
|
||||||
hand_panel.add_child(hv)
|
hand_panel.add_child(hv)
|
||||||
hv.add_child(_label("Karten", 13, DIM))
|
hv.add_child(_label("Münzen", 13, DIM))
|
||||||
hand_box = HBoxContainer.new()
|
hand_box = HBoxContainer.new()
|
||||||
hand_box.add_theme_constant_override("separation", 6)
|
hand_box.add_theme_constant_override("separation", 6)
|
||||||
hv.add_child(hand_box)
|
hv.add_child(hand_box)
|
||||||
hand_panel.visible = false
|
hand_panel.visible = false
|
||||||
|
|
||||||
# --- Booster-Overlay
|
# --- Booster-Overlay
|
||||||
|
# --- Geworfene Münze, links unter der Liste der gelegten Münzen. Die
|
||||||
|
# Tischmitte ist besetzt: dort stehen die Becher und das Gebot.
|
||||||
|
wurf_slot = _slot(0, 0, 0, 0, 16, 100, 336, 300)
|
||||||
|
wurf_slot.visible = false
|
||||||
|
var wv := VBoxContainer.new()
|
||||||
|
wv.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
wv.add_theme_constant_override("separation", 6)
|
||||||
|
wurf_slot.add_child(wv)
|
||||||
|
wurf_icon = MuenzIcon.new("", ACCENT, 104.0)
|
||||||
|
wurf_icon.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
|
wv.add_child(wurf_icon)
|
||||||
|
wurf_label = _label("", 16, ACCENT)
|
||||||
|
wurf_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
wurf_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
wv.add_child(wurf_label)
|
||||||
|
|
||||||
|
# --- Erklärung beim Überfahren. Ein eigenes Feld statt Tooltip: das kommt
|
||||||
|
# sofort und verschwindet nicht, wenn die Runde weiterläuft.
|
||||||
|
info_panel = _panel(Color(0.05, 0.06, 0.09, 0.97))
|
||||||
|
info_panel.visible = false
|
||||||
|
info_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
root.add_child(info_panel)
|
||||||
|
var iv := VBoxContainer.new()
|
||||||
|
iv.add_theme_constant_override("separation", 4)
|
||||||
|
info_panel.add_child(iv)
|
||||||
|
info_name = _label("", 16, ACCENT)
|
||||||
|
iv.add_child(info_name)
|
||||||
|
info_desc = _label("", 14, Color(0.82, 0.84, 0.90))
|
||||||
|
info_desc.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
info_desc.custom_minimum_size = Vector2(INFO_W, 0)
|
||||||
|
iv.add_child(info_desc)
|
||||||
|
info_stark = _label("", 14, ACCENT)
|
||||||
|
info_stark.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
info_stark.custom_minimum_size = Vector2(INFO_W, 0)
|
||||||
|
iv.add_child(info_stark)
|
||||||
|
|
||||||
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
|
||||||
boost_panel = _panel(Color(0.05, 0.06, 0.09, 0.97))
|
boost_panel = _panel(Color(0.05, 0.06, 0.09, 0.97))
|
||||||
@@ -397,7 +484,7 @@ func _build() -> void:
|
|||||||
var bvv := VBoxContainer.new()
|
var bvv := VBoxContainer.new()
|
||||||
bvv.add_theme_constant_override("separation", 12)
|
bvv.add_theme_constant_override("separation", 12)
|
||||||
boost_panel.add_child(bvv)
|
boost_panel.add_child(bvv)
|
||||||
boost_title = _label("Wähle einen Booster", 24, ACCENT)
|
boost_title = _label("Wähle eine Münze", 24, ACCENT)
|
||||||
boost_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
boost_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
bvv.add_child(boost_title)
|
bvv.add_child(boost_title)
|
||||||
timer_boost = _label("", 16, DIM)
|
timer_boost = _label("", 16, DIM)
|
||||||
@@ -554,15 +641,20 @@ func update_state(st: Dictionary, my_seat: int) -> void:
|
|||||||
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)
|
||||||
|
# Wer dran ist, bekommt eine hinterlegte Zeile. Ein Pfeil und etwas
|
||||||
|
# Farbe gehen in der Liste unter.
|
||||||
|
var zeile := PanelContainer.new()
|
||||||
|
zeile.add_theme_stylebox_override("panel", _zug_box if turn else _leer_box)
|
||||||
|
players_box.add_child(zeile)
|
||||||
var entry := VBoxContainer.new()
|
var entry := VBoxContainer.new()
|
||||||
entry.add_theme_constant_override("separation", 2)
|
entry.add_theme_constant_override("separation", 2)
|
||||||
players_box.add_child(entry)
|
zeile.add_child(entry)
|
||||||
var nm := _label(("▸ " if turn else "") + _short(String(s.name)) + (" (KI)" if s.ai else ""), 14, col)
|
var nm := _label(("▸ " if turn else "") + _short(String(s.name)) + (" (KI)" if s.ai else ""), 14, col)
|
||||||
nm.clip_text = true
|
nm.clip_text = true
|
||||||
nm.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
nm.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
entry.add_child(nm)
|
entry.add_child(nm)
|
||||||
if not s.alive:
|
if not s.alive:
|
||||||
entry.add_child(_label(" ausgeschieden", 13, DIM))
|
entry.add_child(_label("ausgeschieden", 13, DIM))
|
||||||
continue
|
continue
|
||||||
var row := HBoxContainer.new()
|
var row := HBoxContainer.new()
|
||||||
row.add_theme_constant_override("separation", 4)
|
row.add_theme_constant_override("separation", 4)
|
||||||
@@ -595,35 +687,24 @@ func update_state(st: Dictionary, my_seat: int) -> void:
|
|||||||
|
|
||||||
_zeichne_hand(st)
|
_zeichne_hand(st)
|
||||||
|
|
||||||
_clear(boosts_box)
|
_zeichne_mitte(st, my_seat)
|
||||||
var boosts: Array = st.get("boosts", [])
|
|
||||||
var any := false
|
|
||||||
for i in mini(boosts.size(), st.seats.size()):
|
|
||||||
if String(boosts[i]) == "":
|
|
||||||
continue
|
|
||||||
any = true
|
|
||||||
var b: Dictionary = BoosterDB.get_booster(String(boosts[i]))
|
|
||||||
var line := HBoxContainer.new()
|
|
||||||
line.add_theme_constant_override("separation", 5)
|
|
||||||
boosts_box.add_child(line)
|
|
||||||
line.add_child(_label(_short(String(st.seats[i].name)) + ":", 14,
|
|
||||||
ACCENT if i == my_seat else Color(0.78, 0.80, 0.86)))
|
|
||||||
var kind := Color(0.5, 0.9, 0.6) if b.kind == BoosterDB.Kind.BUFF else Color(0.95, 0.5, 0.5)
|
|
||||||
line.add_child(_label(String(b.get("name", "")), 14, kind))
|
|
||||||
if int(st.min_raise) > 1:
|
|
||||||
boosts_box.add_child(_label("Störfeuer: mind. %d erhöhen" % st.min_raise, 14, ACCENT))
|
|
||||||
any = true
|
|
||||||
if st.must_bid:
|
|
||||||
boosts_box.add_child(_label("Maulkorb: du musst bieten", 14, ACCENT))
|
|
||||||
any = true
|
|
||||||
if not any:
|
|
||||||
boosts_box.add_child(_label("Keine Karte gespielt", 14, DIM))
|
|
||||||
|
|
||||||
banner_label.text = st.banner
|
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
|
var kam_dran := my_turn and not _my_turn
|
||||||
_my_turn = my_turn
|
_my_turn = my_turn
|
||||||
|
if st.phase == "bid" and int(st.turn) < st.seats.size():
|
||||||
|
var wer := "%s ist dran" % _short(String(st.seats[st.turn].name))
|
||||||
|
if my_turn:
|
||||||
|
turn_label.text = "Du bist dran"
|
||||||
|
elif not st.seats[my_seat].alive:
|
||||||
|
turn_label.text = "Du bist raus · " + wer
|
||||||
|
else:
|
||||||
|
turn_label.text = wer
|
||||||
|
turn_label.add_theme_color_override("font_color", ACCENT if my_turn else DIM)
|
||||||
|
else:
|
||||||
|
turn_label.text = ""
|
||||||
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
|
||||||
_count_max = maxi(1, int(st.total))
|
_count_max = maxi(1, int(st.total))
|
||||||
@@ -665,7 +746,7 @@ func _process(_delta: float) -> void:
|
|||||||
timer_bid.text = text if wants_bid else ""
|
timer_bid.text = text if wants_bid else ""
|
||||||
timer_bid.add_theme_color_override("font_color", col)
|
timer_bid.add_theme_color_override("font_color", col)
|
||||||
# Läuft die Ansagezeit ab, während die Würfelauswahl offen steht, wird
|
# Läuft die Ansagezeit ab, während die Würfelauswahl offen steht, wird
|
||||||
# automatisch angesagt - die Karte bleibt dann ungespielt auf der Hand.
|
# automatisch angesagt - die Münze bleibt dann ungeworfen liegen.
|
||||||
timer_boost.text = "Noch %s bis zur Ansage" % text if wants_boost else ""
|
timer_boost.text = "Noch %s bis zur Ansage" % text if wants_boost else ""
|
||||||
timer_boost.add_theme_color_override("font_color", col)
|
timer_boost.add_theme_color_override("font_color", col)
|
||||||
|
|
||||||
@@ -698,10 +779,10 @@ func _seat_dice(st: Dictionary, seat: int, my_seat: int) -> Array:
|
|||||||
for _k in int(st.seats[seat].count):
|
for _k in int(st.seats[seat].count):
|
||||||
out.append(-1)
|
out.append(-1)
|
||||||
var peek: Dictionary = st.peek
|
var peek: Dictionary = st.peek
|
||||||
if peek.has(seat):
|
for paar: Array in peek.get(seat, []):
|
||||||
var idx := int(peek[seat][0])
|
var idx := int(paar[0])
|
||||||
if idx >= 0 and idx < out.size():
|
if idx >= 0 and idx < out.size():
|
||||||
out[idx] = int(peek[seat][1])
|
out[idx] = int(paar[1])
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -711,52 +792,215 @@ func set_banner(text: String) -> void:
|
|||||||
|
|
||||||
# ---------------------------------------------------------------- Booster
|
# ---------------------------------------------------------------- Booster
|
||||||
|
|
||||||
## Die Hand liegt offen unten links. Ein Klick spielt die Karte sofort;
|
## Der Vorrat liegt offen unten links. Ein Klick wirft die Münze sofort; nur
|
||||||
## nur Karten mit eigenem Ziel fragen noch nach dem Würfel.
|
## Münzen mit eigenem Ziel fragen noch nach dem Würfel. Die Wirkung steht im
|
||||||
|
## Tooltip - auf eine Münze passt nur das Sinnbild.
|
||||||
func _zeichne_hand(st: Dictionary) -> void:
|
func _zeichne_hand(st: Dictionary) -> void:
|
||||||
if hand_box == null:
|
if hand_box == null:
|
||||||
return
|
return
|
||||||
var karten: Array = st.get("hand", [])
|
var muenzen: Array = st.get("hand", [])
|
||||||
hand_panel.visible = not karten.is_empty()
|
# Wer raus ist, schaut nur noch zu. Münzen, die er nie werfen kann,
|
||||||
_clear(hand_box)
|
# stünden nur klickbar im Weg.
|
||||||
|
var dabei: bool = _my_seat < st.seats.size() and st.seats[_my_seat].alive
|
||||||
|
hand_panel.visible = dabei and not muenzen.is_empty()
|
||||||
var spielbar: bool = st.phase == "bid"
|
var spielbar: bool = st.phase == "bid"
|
||||||
for id: String in karten:
|
# Jeder Zustand kommt mehrmals an. Ein Neuaufbau würde die Münze unter dem
|
||||||
|
# Zeiger wegreissen und die Erklärung mitnehmen.
|
||||||
|
var sig := "%s|%s" % [muenzen, spielbar]
|
||||||
|
if sig == _hand_sig:
|
||||||
|
return
|
||||||
|
_hand_sig = sig
|
||||||
|
_clear(hand_box)
|
||||||
|
for id: String in muenzen:
|
||||||
var b: Dictionary = BoosterDB.get_booster(id)
|
var b: Dictionary = BoosterDB.get_booster(id)
|
||||||
if b.is_empty():
|
if b.is_empty():
|
||||||
continue
|
continue
|
||||||
var buff: bool = b.kind == BoosterDB.Kind.BUFF
|
var col := _kind_col(b)
|
||||||
var col := Color(0.5, 0.9, 0.6) if buff else Color(0.95, 0.5, 0.5)
|
|
||||||
|
|
||||||
var btn := Button.new()
|
var btn := Button.new()
|
||||||
btn.custom_minimum_size = Vector2(168, 132)
|
btn.custom_minimum_size = Vector2(104, 120)
|
||||||
btn.tooltip_text = String(b.desc)
|
|
||||||
btn.disabled = not spielbar
|
btn.disabled = not spielbar
|
||||||
|
# Ein gesperrter Knopf meldet keine Maus mehr; die Erklärung soll aber
|
||||||
|
# auch dann kommen, wenn gerade nicht geworfen werden darf.
|
||||||
|
btn.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
btn.pressed.connect(_pick.bind(id))
|
btn.pressed.connect(_pick.bind(id))
|
||||||
hand_box.add_child(btn)
|
hand_box.add_child(btn)
|
||||||
|
_info_an(btn, id, false, hand_panel)
|
||||||
|
|
||||||
var v := VBoxContainer.new()
|
var v := VBoxContainer.new()
|
||||||
v.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
v.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
v.offset_left = 9
|
v.offset_top = 8
|
||||||
v.offset_top = 7
|
v.offset_bottom = -6
|
||||||
v.offset_right = -9
|
|
||||||
v.offset_bottom = -7
|
|
||||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
v.add_theme_constant_override("separation", 2)
|
v.add_theme_constant_override("separation", 4)
|
||||||
btn.add_child(v)
|
btn.add_child(v)
|
||||||
|
|
||||||
var kopf := HBoxContainer.new()
|
var ic := MuenzIcon.new(id, col, 72.0)
|
||||||
kopf.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
ic.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
v.add_child(kopf)
|
v.add_child(ic)
|
||||||
kopf.add_child(KartenIcon.new(id, col, 34.0))
|
|
||||||
|
|
||||||
var name_l := _label(String(b.name), 15, col)
|
var name_l := _label(String(b.name), 14, col)
|
||||||
|
name_l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
name_l.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
name_l.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
v.add_child(name_l)
|
v.add_child(name_l)
|
||||||
var d := _label(String(b.desc), 11, Color(0.78, 0.80, 0.86))
|
|
||||||
d.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
||||||
d.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
## Was liegt in der Mitte, plus die Wirkungen, die gerade auf einem liegen.
|
||||||
d.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
## Auch hier gilt: nur neu bauen, wenn sich etwas geändert hat.
|
||||||
v.add_child(d)
|
func _zeichne_mitte(st: Dictionary, my_seat: int) -> void:
|
||||||
|
var boosts: Array = st.get("boosts", [])
|
||||||
|
# Der neueste Wurf muss bei jedem Zustand geprüft werden, auch ohne Neubau.
|
||||||
|
var neu := {}
|
||||||
|
for i in mini(boosts.size(), st.seats.size()):
|
||||||
|
var m: Dictionary = boosts[i]
|
||||||
|
if not m.is_empty() and int(m.get("nr", 0)) > int(neu.get("nr", 0)):
|
||||||
|
neu = {"nr": m.get("nr", 0), "id": m.get("id", ""),
|
||||||
|
"stark": m.get("stark", false), "seat": i}
|
||||||
|
_pruefe_wurf(neu, st)
|
||||||
|
|
||||||
|
var sig := "%s|%s|%s" % [boosts, st.min_raise, st.must_bid]
|
||||||
|
if sig == _mitte_sig:
|
||||||
|
return
|
||||||
|
_mitte_sig = sig
|
||||||
|
_clear(boosts_box)
|
||||||
|
|
||||||
|
var any := false
|
||||||
|
for i in mini(boosts.size(), st.seats.size()):
|
||||||
|
var m: Dictionary = boosts[i]
|
||||||
|
if m.is_empty():
|
||||||
|
continue
|
||||||
|
var id := String(m.get("id", ""))
|
||||||
|
var b: Dictionary = BoosterDB.get_booster(id)
|
||||||
|
if b.is_empty():
|
||||||
|
continue
|
||||||
|
any = true
|
||||||
|
var line := HBoxContainer.new()
|
||||||
|
line.add_theme_constant_override("separation", 5)
|
||||||
|
# Der Container muss die Maus annehmen, sonst gibt es kein Überfahren.
|
||||||
|
line.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
boosts_box.add_child(line)
|
||||||
|
_info_an(line, id, true)
|
||||||
|
line.add_child(MuenzIcon.new(id, _kind_col(b), 22.0))
|
||||||
|
line.add_child(_label(_short(String(st.seats[i].name)) + ":", 14,
|
||||||
|
ACCENT if i == my_seat else Color(0.78, 0.80, 0.86)))
|
||||||
|
line.add_child(_label(String(b.get("name", "")), 14, _kind_col(b)))
|
||||||
|
if bool(m.get("stark", false)):
|
||||||
|
line.add_child(_label("· Wappen", 14, ACCENT))
|
||||||
|
if int(st.min_raise) > 1:
|
||||||
|
boosts_box.add_child(_label("Störfeuer: mind. %d erhöhen" % st.min_raise, 14, ACCENT))
|
||||||
|
any = true
|
||||||
|
if st.must_bid:
|
||||||
|
boosts_box.add_child(_label("Maulkorb: du musst bieten", 14, ACCENT))
|
||||||
|
any = true
|
||||||
|
if not any:
|
||||||
|
boosts_box.add_child(_label("Keine Münze in der Mitte", 14, DIM))
|
||||||
|
|
||||||
|
|
||||||
|
## Erklärung zu einer Münze, sobald die Maus daraufliegt. `unter` legt sie
|
||||||
|
## unter das Feld statt darüber - oben links wäre sonst kein Platz. `anker`
|
||||||
|
## ist, woran sie sich ausrichtet: beim Vorrat das ganze Feld, sonst läge sie
|
||||||
|
## halb über dessen Überschrift.
|
||||||
|
func _info_an(ctrl: Control, id: String, unter: bool, anker: Control = null) -> void:
|
||||||
|
var an: Control = anker if anker != null else ctrl
|
||||||
|
ctrl.mouse_entered.connect(_info_zeigen.bind(ctrl, id, unter, an))
|
||||||
|
ctrl.mouse_exited.connect(_info_weg.bind(ctrl))
|
||||||
|
|
||||||
|
|
||||||
|
func _info_zeigen(ctrl: Control, id: String, unter: bool, anker: Control) -> void:
|
||||||
|
var b: Dictionary = BoosterDB.get_booster(id)
|
||||||
|
if b.is_empty():
|
||||||
|
return
|
||||||
|
_info_ctrl = ctrl
|
||||||
|
info_panel.visible = false
|
||||||
|
info_name.text = String(b.name)
|
||||||
|
info_desc.text = String(b.desc)
|
||||||
|
info_stark.text = "Wappen: %s" % b.get("stark", "")
|
||||||
|
# Die Höhe steht erst nach dem Umbruch fest, deshalb ein Bild später
|
||||||
|
# setzen. Bis dahin bleibt das Feld unsichtbar, sonst springt es.
|
||||||
|
await get_tree().process_frame
|
||||||
|
if _info_ctrl != ctrl or not is_instance_valid(ctrl) or not is_instance_valid(anker):
|
||||||
|
return
|
||||||
|
info_panel.reset_size()
|
||||||
|
var gr := info_panel.size
|
||||||
|
var r := anker.get_global_rect()
|
||||||
|
var y: float = r.position.y + r.size.y + 8.0 if unter else r.position.y - gr.y - 8.0
|
||||||
|
info_panel.position = Vector2(
|
||||||
|
clampf(r.position.x, 8.0, maxf(8.0, root.size.x - gr.x - 8.0)),
|
||||||
|
clampf(y, 8.0, maxf(8.0, root.size.y - gr.y - 8.0)))
|
||||||
|
info_panel.visible = true
|
||||||
|
|
||||||
|
|
||||||
|
func _info_weg(ctrl: Control) -> void:
|
||||||
|
if _info_ctrl != ctrl:
|
||||||
|
return
|
||||||
|
_info_ctrl = null
|
||||||
|
info_panel.visible = false
|
||||||
|
|
||||||
|
|
||||||
|
func _kind_col(b: Dictionary) -> Color:
|
||||||
|
return Color(0.5, 0.9, 0.6) if b.kind == BoosterDB.Kind.BUFF else Color(0.95, 0.5, 0.5)
|
||||||
|
|
||||||
|
|
||||||
|
## Jeder Wurf hat eine laufende Nummer. Steigt sie, ist eine neue Münze
|
||||||
|
## gefallen und wird gezeigt. Zu Rundenbeginn ist die Mitte leer, dann faengt
|
||||||
|
## die Zählung wieder bei null an.
|
||||||
|
func _pruefe_wurf(neu: Dictionary, st: Dictionary) -> void:
|
||||||
|
var nr := int(neu.get("nr", 0))
|
||||||
|
if nr == 0:
|
||||||
|
_letzter_wurf = 0
|
||||||
|
return
|
||||||
|
if nr <= _letzter_wurf:
|
||||||
|
return
|
||||||
|
_letzter_wurf = nr
|
||||||
|
var seat := int(neu.get("seat", 0))
|
||||||
|
_zeig_wurf(String(neu.get("id", "")), bool(neu.get("stark", false)),
|
||||||
|
_short(String(st.seats[seat].name)))
|
||||||
|
|
||||||
|
|
||||||
|
## Der Ausgang steht schon fest - der Host hat ihn ausgewürfelt. Die Drehung
|
||||||
|
## ist nur die Ansicht dazu und endet auf der richtigen Seite.
|
||||||
|
func _zeig_wurf(id: String, stark: bool, wer: String) -> void:
|
||||||
|
var b: Dictionary = BoosterDB.get_booster(id)
|
||||||
|
if b.is_empty():
|
||||||
|
return
|
||||||
|
wurf_icon.id = id
|
||||||
|
wurf_icon.kind_col = _kind_col(b)
|
||||||
|
wurf_icon.seite = 0
|
||||||
|
wurf_icon.scale = Vector2.ONE
|
||||||
|
wurf_icon.queue_redraw()
|
||||||
|
wurf_label.text = "%s wirft\n%s" % [wer, b.name]
|
||||||
|
wurf_label.add_theme_color_override("font_color", DIM)
|
||||||
|
wurf_slot.visible = true
|
||||||
|
if _wurf_tween != null and _wurf_tween.is_valid():
|
||||||
|
_wurf_tween.kill()
|
||||||
|
_wurf_tween = create_tween()
|
||||||
|
_wurf_tween.tween_method(_dreh, 0.0, TAU * 2.5, 0.8)
|
||||||
|
_wurf_tween.tween_callback(_wurf_ende.bind(stark, b))
|
||||||
|
_wurf_tween.tween_interval(1.6)
|
||||||
|
_wurf_tween.tween_callback(func() -> void: wurf_slot.visible = false)
|
||||||
|
|
||||||
|
|
||||||
|
## Die Münze wird waagerecht gestaucht; bei negativer Breite sieht man die
|
||||||
|
## Rückseite. Das ist eine Drehung, ohne dass etwas in 3D stehen muss.
|
||||||
|
func _dreh(winkel: float) -> void:
|
||||||
|
var c := cos(winkel)
|
||||||
|
wurf_icon.scale.x = maxf(0.06, absf(c))
|
||||||
|
var seite := 0 if c >= 0.0 else 1
|
||||||
|
if wurf_icon.seite != seite:
|
||||||
|
wurf_icon.seite = seite
|
||||||
|
wurf_icon.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _wurf_ende(stark: bool, b: Dictionary) -> void:
|
||||||
|
wurf_icon.scale.x = 1.0
|
||||||
|
wurf_icon.seite = 0 if stark else 1
|
||||||
|
wurf_icon.queue_redraw()
|
||||||
|
if stark:
|
||||||
|
wurf_label.text = "%s\nWappen - verstärkt" % b.name
|
||||||
|
wurf_label.add_theme_color_override("font_color", ACCENT)
|
||||||
|
else:
|
||||||
|
wurf_label.text = "%s\nZahl - Grundwirkung" % b.name
|
||||||
|
wurf_label.add_theme_color_override("font_color", DIM)
|
||||||
|
|
||||||
|
|
||||||
func _pick(id: String) -> void:
|
func _pick(id: String) -> void:
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ const WALL_SEGS := 20 ## Bandensegmente - Godot kennt keinen hohlen Zylin
|
|||||||
const WALL_H := 0.55 ## unsichtbare Begrenzung, damit nichts herausspringt
|
const WALL_H := 0.55 ## unsichtbare Begrenzung, damit nichts herausspringt
|
||||||
const SEAT_ARC := ZONE_RAD * 2.0 + 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 RIM_RUHE := Color(0.22, 0.15, 0.09) ## Zonenrand, wenn der Platz wartet
|
||||||
|
const RIM_ZUG := Color(0.85, 0.66, 0.22) ## Zonenrand des Platzes, der dran ist
|
||||||
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.17 ## Bildanteil unten für die Ansage-Leiste
|
const HUD_BOTTOM := 0.17 ## Bildanteil unten für die Ansage-Leiste
|
||||||
@@ -27,6 +29,7 @@ var bid_counts: Array[Label3D] = []
|
|||||||
var bid_icons: Array[Sprite3D] = []
|
var bid_icons: Array[Sprite3D] = []
|
||||||
var _bid_anchor: Array[Vector3] = []
|
var _bid_anchor: Array[Vector3] = []
|
||||||
var plates: Array[Sprite3D] = []
|
var plates: Array[Sprite3D] = []
|
||||||
|
var rims: Array[MeshInstance3D] = [] ## Rand der Zone, leuchtet beim Zug
|
||||||
var dice: Array = [] ## seat -> Array[Die]
|
var dice: Array = [] ## seat -> Array[Die]
|
||||||
|
|
||||||
var camera: Camera3D
|
var camera: Camera3D
|
||||||
@@ -172,8 +175,9 @@ func _build_seat(i: int) -> void:
|
|||||||
tm.ring_segments = 8
|
tm.ring_segments = 8
|
||||||
rim.mesh = tm
|
rim.mesh = tm
|
||||||
rim.position = Vector3(0, 0.02, 0)
|
rim.position = Vector3(0, 0.02, 0)
|
||||||
rim.material_override = _mat(Color(0.22, 0.15, 0.09), 0.6)
|
rim.material_override = _mat(RIM_RUHE, 0.6)
|
||||||
zone.add_child(rim)
|
zone.add_child(rim)
|
||||||
|
rims.append(rim)
|
||||||
|
|
||||||
# Becher
|
# Becher
|
||||||
var cup := Node3D.new()
|
var cup := Node3D.new()
|
||||||
@@ -393,17 +397,26 @@ func _mat(col: Color, rough: float) -> StandardMaterial3D:
|
|||||||
|
|
||||||
## --- Würfel ---
|
## --- Würfel ---
|
||||||
|
|
||||||
|
## Nur die neu dazugekommenen Würfel werden hingelegt. Die schon liegenden
|
||||||
|
## bleiben, wie sie sind - ein Nachschub mitten in der Runde würde ihnen sonst
|
||||||
|
## die Drehung nehmen und damit auf dem Tisch die Augenzahl.
|
||||||
func set_dice_count(seat: int, count: int) -> void:
|
func set_dice_count(seat: int, count: int) -> void:
|
||||||
var arr: Array = dice[seat]
|
var arr: Array = dice[seat]
|
||||||
while arr.size() > count:
|
while arr.size() > count:
|
||||||
var d = arr.pop_back()
|
var d = arr.pop_back()
|
||||||
d.queue_free()
|
d.queue_free()
|
||||||
|
# Ein neuer Würfel darf nicht sichtbar sein, wenn die anderen unterm
|
||||||
|
# Becher liegen - sonst verrät er die Hand des Gegners.
|
||||||
|
var sichtbar: bool = arr.is_empty() or arr[0].visible
|
||||||
|
var ab := arr.size()
|
||||||
while arr.size() < count:
|
while arr.size() < count:
|
||||||
var d := Die.new()
|
var d := Die.new()
|
||||||
add_child(d)
|
add_child(d)
|
||||||
d.global_position = zones[seat].global_position + Vector3(0, 0.4, 0)
|
d.global_position = zones[seat].global_position + Vector3(0, 0.4, 0)
|
||||||
|
d.visible = sichtbar
|
||||||
arr.append(d)
|
arr.append(d)
|
||||||
_layout_idle(seat)
|
if arr.size() > ab:
|
||||||
|
_layout_idle(seat, ab)
|
||||||
|
|
||||||
|
|
||||||
func dice_of(seat: int) -> Array:
|
func dice_of(seat: int) -> Array:
|
||||||
@@ -426,8 +439,9 @@ func throw_dice(seat: int, rng: RandomNumberGenerator) -> void:
|
|||||||
arr[i].throw_from(zone.global_transform * off, rng)
|
arr[i].throw_from(zone.global_transform * off, rng)
|
||||||
|
|
||||||
|
|
||||||
## Legt die Würfel ordentlich nebeneinander (für Clients/Reveal).
|
## Legt die Würfel ordentlich nebeneinander (für Clients/Reveal). Ab `von`
|
||||||
func _layout_idle(seat: int) -> void:
|
## bleiben die davorliegenden unangetastet.
|
||||||
|
func _layout_idle(seat: int, von := 0) -> 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.
|
# Bogen auf einem Kreis um die Zonenmitte - passt zur runden Bande.
|
||||||
@@ -435,7 +449,7 @@ func _layout_idle(seat: int) -> void:
|
|||||||
# Würfels plus Luft, sonst überlappen die Bilder beim Aufdecken.
|
# Würfels plus Luft, sonst überlappen die Bilder beim Aufdecken.
|
||||||
const BOGEN_R := 0.48 # etwas weiter aussen, damit das Namensschild frei bleibt
|
const BOGEN_R := 0.48 # etwas weiter aussen, damit das Namensschild frei bleibt
|
||||||
var schritt: float = (Die.SIZE * 1.7) / BOGEN_R
|
var schritt: float = (Die.SIZE * 1.7) / BOGEN_R
|
||||||
for i in arr.size():
|
for i in range(von, arr.size()):
|
||||||
var ang := PI + (float(i) - (arr.size() - 1) * 0.5) * schritt
|
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(
|
t.origin = zone.global_transform * Vector3(
|
||||||
@@ -458,6 +472,19 @@ func freeze_dice(seat: int) -> void:
|
|||||||
d.set_kinematic(true)
|
d.set_kinematic(true)
|
||||||
|
|
||||||
|
|
||||||
|
## Der Platz, der dran ist, bekommt einen leuchtenden Rand. In der dunklen
|
||||||
|
## Szene ist das von jeder Ecke aus zu sehen - ein farbiger Name allein nicht.
|
||||||
|
func set_active_seat(seat: int) -> void:
|
||||||
|
for i in rims.size():
|
||||||
|
var m: StandardMaterial3D = rims[i].material_override
|
||||||
|
var dran: bool = i == seat
|
||||||
|
m.albedo_color = RIM_ZUG if dran else RIM_RUHE
|
||||||
|
m.emission_enabled = dran
|
||||||
|
if dran:
|
||||||
|
m.emission = Color(1.0, 0.78, 0.30)
|
||||||
|
m.emission_energy_multiplier = 1.1
|
||||||
|
|
||||||
|
|
||||||
func set_dice_visible(seat: int, on: bool) -> void:
|
func set_dice_visible(seat: int, on: bool) -> void:
|
||||||
for d in dice[seat]:
|
for d in dice[seat]:
|
||||||
d.visible = on
|
d.visible = on
|
||||||
|
|||||||
Reference in New Issue
Block a user