diff --git a/README.md b/README.md index dff6280..45c145e 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,107 @@ im `Dockerfile`), importiert das Projekt einmal vor und startet den Server. Es muss kein Port freigegeben werden: alles läuft als WebSocket über Traefik auf 443. +Das Spiel startet im Vollbild, **F11** schaltet um. **Esc** öffnet ein Menü mit +*Fortsetzen* und *Verlassen*; im Einzelspieler hält es das Spiel samt Zeitlimit +wirklich an, im Mehrspieler läuft die Runde weiter – sonst würde ein geöffnetes +Menü alle anderen am Tisch blockieren. + +Die Bildaufteilung: Tisch mittig, darauf die letzte Wette als so viele Würfel +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 +`hud.gd`), unbekannte Werte als Würfel mit Fragezeichen. Beim Aufdecken bleibt die Wette in der +Tischmitte stehen, wie sie angesagt war: fehlende Würfel werden rot. Gab es +mehr als angesagt, kommen die überzähligen grün dahinter. 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 +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 +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 +`table.gd` rechnet den Abstand so aus, dass der Tisch genau das freie Band +zwischen den HUD-Feldern ausfüllt (`HUD_TOP`/`HUD_BOTTOM`) – bei jeder +Spielerzahl und jedem Fensterformat, auch nach dem Ändern der Fenstergröße. + +## Physik und Bildrate + +Zwei Engine-Vorgaben sind bewusst gesetzt, beide in `project.godot`: + +* **`3d/physics_engine="Jolt Physics"`** – GodotPhysics3D hat Kontakt-Toleranzen, + die auf meterhohe Objekte ausgelegt sind (`contact_max_separation` 0.05 bei + 0.075 Würfel-Halbkante). Würfel dieser Größe sanken dadurch in die Tischplatte + ein und rollten endlos weiter, sodass jeder Wurf in den Settle-Timeout lief. + Mit Jolt liegt die Wurfphase bei rund 0,9 s statt 7 s. +* **`renderer/rendering_method="mobile"`** – auf integrierten GPUs kostet + 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 +gemeinsamen Mesh mit Vertexfarben, statt 21 Augen als eigene Meshes zu hängen. + +Weil Kamera und Deckenlicht beide fast senkrecht stehen, liegt der Glanzpunkt +genau auf der Würfeloberseite. Mit glänzendem Material und starkem Spot klippte +die weiße Fläche bei 255 und riss die schwarzen Augen mit hoch – gemessen lag +kein einziger Punkt-Pixel unter Luminanz 150. Deshalb ist das Würfelmaterial +matt (`roughness` 0.9, `metallic_specular` 0.0) und der Spot schwächer +zugunsten von mehr Umgebungslicht; der Kontrast Auge zu Körper liegt damit bei +rund 11:1. + +## Regeln + +Klassisches Liar's Dice ohne Joker. Jeder startet mit 5 Würfeln und sieht nur +die eigenen. + +1. **Würfeln** – alle Würfel fallen physikalisch in den eigenen Becher. +2. **Boost wählen** – jeder bekommt 3 zufällige Booster angeboten und wählt + einen; er wirkt nur für die laufende Runde. +3. **Ansagen** – reihum wird ein Gebot "Anzahl x Augenzahl" über alle Würfel + am Tisch gemacht. Jedes Gebot muss höher sein: mehr Würfel, oder gleich + viele bei höherer Augenzahl. +4. **Anfechten** – statt zu erhöhen kann man das Vorgebot anzweifeln. Alle + Becher werden gehoben und gezählt: Stimmt das Gebot, verliert der + Anfechter einen Würfel, sonst der Bieter. + +Für Boosterwahl und Ansage hat man jeweils **30 Sekunden**; die Restzeit steht +im Booster-Fenster beziehungsweise neben „Deine Ansage" und wird unter zehn +Sekunden rot. Läuft die Zeit ab, entscheidet der Host automatisch – beim Booster +zufällig, beim Gebot mit der Bot-Logik. Ohne dieses Limit würde ein abwesender +oder getrennter Mitspieler den ganzen Tisch dauerhaft blockieren. + +Wer keine Würfel mehr hat, scheidet aus. Der Letzte gewinnt. + +## Booster + +| Booster | Art | Wirkung | +|---|---|---| +| Neuwurf | Buff | Alle eigenen Würfel neu werfen | +| Feinschliff | Buff | Einen gewählten eigenen Würfel neu werfen | +| Glücksgriff | Buff | Ein gewählter Würfel wird zur 6 | +| Zwilling | Buff | Ein anderer eigener Würfel nimmt den Wert des gewählten an | +| Spähen | Buff | Von jedem Gegner wird ein zufälliger Würfel aufgedeckt | +| Rütteln | Debuff | Jeder Gegner wirft einen zufälligen Würfel neu | +| Absenken | Debuff | Bei jedem Gegner sinkt ein zufälliger Würfel um 1 | +| Nebel | Debuff | Jeder Gegner sieht einen eigenen Würfel nicht mehr | +| Störfeuer | Debuff | Gegner müssen diese Runde um mindestens 2 erhöhen | +| Maulkorb | Debuff | Der nächste Spieler muss bieten und darf nicht anfechten | + ## Aufbau | Datei | Inhalt | diff --git a/scripts/hud.gd b/scripts/hud.gd index 329507f..368f248 100644 --- a/scripts/hud.gd +++ b/scripts/hud.gd @@ -13,7 +13,7 @@ const DIM := Color(0.62, 0.64, 0.70) ## 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 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 var root: Control @@ -466,14 +466,20 @@ func update_state(st: Dictionary, my_seat: int) -> void: else: bid_name.text = String(st.seats[st.bid.seat].name) 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. + # Beim Aufdecken bleibt die Ansage stehen, wie sie war: die angesagten + # Würfel normal, die fehlenden davon rot. Gab es mehr als angesagt, + # kommen die überzähligen grün dahinter. var faerben: bool = st.phase == "reveal" - for i in int(st.bid.count): + var angesagt := int(st.bid.count) + var gezeigt := maxi(angesagt, _actual) if faerben else angesagt + var px := _fit_dice(gezeigt) + for i in gezeigt: var bg := Color.TRANSPARENT if faerben: - bg = GRUEN if i < _actual else ROT + if i >= angesagt: + bg = GRUEN + elif i >= _actual: + bg = ROT bid_row.add_child(DieIcon.new(int(st.bid.face), px, bg)) _clear(boosts_box)