update
This commit is contained in:
26
README.md
26
README.md
@@ -20,23 +20,22 @@ Der Spielstand liegt unter `user://spielstand.json`
|
|||||||
|
|
||||||
## Spielsysteme
|
## Spielsysteme
|
||||||
|
|
||||||
- 81 Karten in `data/karten.json`, alle Wesen, jede mit genau einem der vier Elemente; Kombos sind die Namen verschmolzener Wesen
|
- 120 Karten in `data/karten.json`: je Element alle Werte von 0/1 bis 5/5, die ID verrät sie (`f_3_2` = Feuer 3/2), Namen aus Eigenschaft und Wesen; Kombos sind die Namen verschmolzener Wesen
|
||||||
- Karten haben nur Element, Angriff und Leben; die Kosten sind zwei Drittel Angriff plus ein Drittel Leben, abgerundet; je Element gibt es ein 0/2-Wesen für 0 Mana
|
- Karten haben nur Element, Angriff und Leben; die Kosten sind zwei Drittel Angriff plus ein Drittel Leben, abgerundet, höchstens 10 (0/2 kostet nichts)
|
||||||
- Mana: 1 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen, Legen und Verschmelzen
|
- Mana: 1 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen, Legen und Verschmelzen
|
||||||
- Kein Deckbau: beide ziehen vom gemeinsamen Stapel (jede Karte dreimal). Zu Beginn fünf Karten, alle im ersten Zug spielbar. Auf dem Stapel liegen drei Karten offen, nie teurer als das Mana des Zugs; beim Ziehen wählt man eine, eine neue rückt nach. Das erste Ziehen im Zug ist frei, jedes weitere kostet 2 Mana; eine Handkarte abwerfen bringt 1 Mana (Rechtsklick)
|
- Jeder spielt mit seinem eigenen Stapel aus 40 bis 60 Karten (Menü „Deck“: eigene Karten rein oder raus, „Auffüllen“ nimmt die teuersten des gewählten Filters). Zu Beginn fünf im ersten Zug spielbare Karten. Ziehen bringt eine Karte, die erste im Zug ist frei, dann 1, 2, 3 … Mana; ist der Stapel leer, kostet jeder Zug 1, 2, 3 … Leben (Erschöpfung); eine Handkarte abwerfen bringt 1 Mana (Rechtsklick)
|
||||||
- Eigenschaften gibt es nur über Elementpaare (siehe Elementboni): Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug). Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink; verletzte Wesen heilen nicht von selbst
|
- Eigenschaften gibt es nur über Elementpaare (siehe Elementboni): Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug). Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink; verletzte Wesen heilen nicht von selbst
|
||||||
- Verdeckt legen (mittlere Maustaste auf die Handkarte oder im Kartenmenü): gleich teuer, das Wesen greift nicht an, zählt nicht fürs Elementpaar und bekommt den Elementbonus erst offen; aufdecken ist kostenlos, frühestens im nächsten Zug, danach greift es sofort an; ein Angriff deckt es auf
|
- Verdeckt legen (mittlere Maustaste auf die Handkarte oder im Kartenmenü): gleich teuer, das Wesen greift nicht an, zählt nicht fürs Elementpaar und bekommt den Elementbonus erst offen; aufdecken ist kostenlos, frühestens im nächsten Zug, danach greift es sofort an; ein Angriff deckt es auf
|
||||||
- Jede Karte gehört jedem von Anfang an. Gesammelt wird die Seltenheitsstufe, und die ist reine Optik wie bei Yu-Gi-Oh: Common matt, Rare silberner Name und Rahmen mit Glanzstreifen, Super Rare Holo-Bild, Ultra Rare goldener Name und Rahmen mit Holo-Bild, Ultimate Rare Relief, Holo auf Bild und Rahmen, Funkeln und schillernde Farben; auch auf dem Feld für alle sichtbar, Werte bleiben gleich. Verschmolzene Wesen tragen die höhere Stufe ihrer Eltern
|
- Beim ersten Start wählt man eines von vier Startdecks (40 Karten: 25 eines Elements bis Angriff 4, je fünf billige der anderen), der Rest kommt aus Boostern oder der Werkstatt. Jede eigene Karte hat eine Seltenheitsstufe (Common, Rare, Super Rare, Ultra Rare, Ultimate Rare) und ist je Stufe +1/+1 stärker, bei gleichen Kosten; die Stufe zeigt sich auch in der Optik (Glanz, Holo, Relief, Funkeln). Verschmolzene Wesen tragen die höhere Stufe ihrer Eltern
|
||||||
- Booster (100 Münzen): fünf Karten mit gewürfelter Stufe (50/25/15/8/2 %, mindestens eine Rare), höhere werten die Karte auf, der Rest wird zu Staub (50/100/200/500/1000). Aufwerten kostet 100/200/500/1000 Staub, eine Stufe abgeben bringt den Zerlegewert zurück
|
- Booster (100 Münzen): fünf Karten mit gewürfelter Stufe (45/25/15/10/5 %, mindestens eine Rare); neue kommen in Sammlung und Stapel, eine höhere Stufe ersetzt die alte Version (die wird zu Staub), der Rest wird zu Staub (50/100/200/500/1000). Werkstatt in der Sammlung: Aufwerten kostet 100/200/500/1000 Staub, Zerlegen bringt den Wert zurück (Common verlässt die Sammlung), Herstellen einer fehlenden Karte kostet 200 Staub
|
||||||
- Elementboni, jeder Spieler für sich: zwei eigene offene Wesen eines Elements geben allen eigenen Wesen dieses Elements seinen Effekt (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag); je 10 Mana an ausgespielten Karten eines Elements geben den eigenen Wesen dieses Elements +1 Angriff (verdeckt gelegte zählen mit, bekommen den Bonus aber erst offen). Verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Boni. Zwei Leisten in der Mitte, oben die des Gegners, unten die eigene, zeigen je Element zwei Symbole, gefüllt je offenem eigenen Wesen, und das Mana der ausgespielten Karten
|
- Elementboni, jeder Spieler für sich: zwei eigene offene Wesen eines Elements geben allen eigenen Wesen dieses Elements seinen Effekt (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag); je fünf ausgespielte Karten eines Elements geben den eigenen Wesen dieses Elements +1/+1 (verdeckt gelegte zählen mit, bekommen den Bonus aber erst offen). Verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Boni. Die Zähler stehen links neben dem jeweiligen Brett und zeigen nur Elemente, bei denen etwas passiert ist: zwei Symbole, gefüllt je offenem eigenen Wesen, und die Zahl der ausgespielten Karten
|
||||||
- Elementvorteil beim Kampf (Wasser > Feuer > Luft > Erde > Wasser): wer im Vorteil ist, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter, gegen Helden nichts; so greift und kontert auch ein Wesen mit 0 Angriff, nur nicht ohne Vorteil; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht. Ist ein Angreifer gewählt, zeigen die Gegnerkarten ihren Konter gegen ihn (grün mehr, rot weniger); über einem Ziel zeigt der Angreifer seinen Angriff mit Vorteil, und wer stirbt, trägt die Marke „stirbt“
|
- Elementvorteil beim Kampf (Wasser > Feuer > Luft > Erde > Wasser): wer im Vorteil ist, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter, gegen Helden nichts; so greift und kontert auch ein Wesen mit 0 Angriff, nur nicht ohne Vorteil; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht. Ist ein Angreifer gewählt, zeigen die Gegnerkarten ihren Konter gegen ihn (grün mehr, rot weniger); über einem Ziel zeigt der Angreifer seinen Angriff mit Vorteil, und wer stirbt, trägt die Marke „stirbt“
|
||||||
- Angriffe treffen erst Wesen (Wesen mit Spott zuerst); das Leben ist nur ohne Wesen angreifbar, per Doppelklick auf das eigene Wesen direkt. Nichts schlägt durch
|
- Angriffe treffen erst Wesen (Wesen mit Spott zuerst); das Leben ist nur ohne Wesen angreifbar, per Doppelklick auf das eigene Wesen direkt. Nichts schlägt durch. Den Zug beendet die Leertaste oder der Rechtsklick ins Leere; Esc öffnet die Optionen (Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden)
|
||||||
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde halten, auf dem Brett wie auf der Hand (Gamepad: Y, „Verschmelzen mit …“). Das neue Wesen hat je Wert den besseren plus ein Drittel des schwächeren (aufgerundet, mindestens 1), Kosten passend dazu, Elemente und Eigenschaften beider; kostet 1 Mana, verdoppelt je weiterer Verschmelzung (1, 2, 4, 8); drei oder mehr Elemente ergeben eine Chimäre
|
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde halten, auf dem Brett wie auf der Hand (Gamepad: Y, „Verschmelzen mit …“). Das neue Wesen hat je Wert den besseren plus ein Drittel des schwächeren (aufgerundet, mindestens 1), Kosten aus den neuen Werten, Elemente und Eigenschaften beider; es kostet die halbe Manasumme beider Karten (aufgerundet); drei oder mehr Elemente ergeben eine Chimäre
|
||||||
- Duell mit ausgeloster Zugreihenfolge, beide beginnen mit 1 Mana; die Hand fasst 5 Karten, das Feld 5 Wesen
|
- Duell mit ausgeloster Zugreihenfolge, beide beginnen mit 1 Mana; die Hand fasst 5 Karten, das Feld 5 Wesen
|
||||||
- Schwierigkeit Leicht/Normal/Schwer: Gegner mit 25/30/35 Leben, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
|
- Schwierigkeit Leicht/Normal/Schwer: Gegner mit 25/30/35 Leben, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
|
||||||
- KI mit Vorausschau in zwei Stufen und Zeitbudget (150 ms), sie rechnet im Hintergrund-Thread
|
- KI mit Vorausschau in zwei Stufen und Zeitbudget (150 ms), sie rechnet im Hintergrund-Thread
|
||||||
- Kampagne: drei Kapitel mit je zehn Magiern, Bosse mit ein bis drei Merkmalen, eigenes Merkmal nach Kapitel 1
|
- Kampagne: drei Kapitel mit je zehn Magiern, Bosse mit ein bis drei Merkmalen und eigenen Stapeln (Kapitel 1 bis Kosten 3, Kapitel 2 bis 5 als Rare, Kapitel 3 alles als Super Rare), eigenes Merkmal nach Kapitel 1
|
||||||
- Netzwerkspiel im lokalen Netz: Host führt das Spiel, Mitspieler sehen nur ihre Hand, Zuschauer keine. Fällt der Host aus, übernimmt der Mitspieler mit der kleinsten Kennung, die anderen verbinden neu (Platz bleibt 60 s frei)
|
|
||||||
- Spielstand versioniert mit Sicherungskopie (`spielstand.json.bak`)
|
- Spielstand versioniert mit Sicherungskopie (`spielstand.json.bak`)
|
||||||
- Oberfläche und Kartentexte wahlweise auf Englisch (`translations/ui.csv`, `name_en` in `data/karten.json`)
|
- Oberfläche und Kartentexte wahlweise auf Englisch (`translations/ui.csv`, `name_en` in `data/karten.json`)
|
||||||
- Gamepad in Menü, Sammlung, Booster und Kampf; große Schrift und Animationen in den Einstellungen; Ton und Musik sind standardmäßig aus
|
- Gamepad in Menü, Sammlung, Booster und Kampf; große Schrift und Animationen in den Einstellungen; Ton und Musik sind standardmäßig aus
|
||||||
@@ -53,7 +52,6 @@ godot --headless --path . -s tests/test_runner.gd # Regeln, KI, Booster, Spiel
|
|||||||
godot --headless --path . -s tests/simulate.gd # Balance: KI spielt gegen KI
|
godot --headless --path . -s tests/simulate.gd # Balance: KI spielt gegen KI
|
||||||
DINGER_SHOTS=/tmp/shots godot --path . tests/ui_test.tscn # Oberfläche mit Fenster und Screenshots
|
DINGER_SHOTS=/tmp/shots godot --path . tests/ui_test.tscn # Oberfläche mit Fenster und Screenshots
|
||||||
DINGER_ONLY=test_sammlung godot --path . tests/ui_test.tscn # nur einzelne Oberflächentests
|
DINGER_ONLY=test_sammlung godot --path . tests/ui_test.tscn # nur einzelne Oberflächentests
|
||||||
DINGER_NETZ=1 godot --path . tests/ui_test.tscn # zusätzlich die langsamen Netzwerktests (Bot-Prozesse)
|
|
||||||
godot --headless --path . -s tests/perf_probe.gd # Laufzeiten: KI-Entscheidung, Stapel, Musik
|
godot --headless --path . -s tests/perf_probe.gd # Laufzeiten: KI-Entscheidung, Stapel, Musik
|
||||||
godot --path . tests/frame_probe.tscn # Bildzeiten im Kampf
|
godot --path . tests/frame_probe.tscn # Bildzeiten im Kampf
|
||||||
```
|
```
|
||||||
@@ -71,13 +69,11 @@ godot --headless --path . --export-release Windows build/dinger-windows.exe
|
|||||||
|
|
||||||
- `scripts/cards.gd` – Elemente, Kombos, Kartenklassen; Pool aus `data/karten.json`
|
- `scripts/cards.gd` – Elemente, Kombos, Kartenklassen; Pool aus `data/karten.json`
|
||||||
- `scripts/game.gd` – Spielregeln: Züge, Mana, Ziehen, verdeckte Wesen, Elementboni, Kampf, Verschmelzen
|
- `scripts/game.gd` – Spielregeln: Züge, Mana, Ziehen, verdeckte Wesen, Elementboni, Kampf, Verschmelzen
|
||||||
- `scripts/deckgen.gd` – gemeinsamer Stapel und Gegner
|
- `scripts/deckgen.gd` – Stapel: zufällige für Gegner, Standardstapel aus der Sammlung
|
||||||
- `scripts/netstate.gd` – Zustandsbilder fürs Netzwerk
|
|
||||||
- `scripts/ai.gd` – KI mit Vorausschau (plus gierige Vergleichs-KI)
|
- `scripts/ai.gd` – KI mit Vorausschau (plus gierige Vergleichs-KI)
|
||||||
- `scripts/profile.gd` – Spielstand (versioniert), Sammlung, Booster, Einstellungen
|
- `scripts/profile.gd` – Spielstand (versioniert), Sammlung, Stapel, Booster, Werkstatt, Einstellungen
|
||||||
- `scripts/campaign.gd` – Kampagnenstufen
|
- `scripts/campaign.gd` – Kampagnenstufen
|
||||||
- `scenes/main.tscn` – Startszene
|
- `scenes/main.tscn` – Startszene
|
||||||
- `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden
|
|
||||||
- `scripts/ui/` – Bildschirme; `card_view.gd` und `creature_view.gd` zeichnen Karten und Wesen, `element_view.gd` den Elementzähler, `battle_layout.gd` die Aufteilung des Kampfes
|
- `scripts/ui/` – Bildschirme; `card_view.gd` und `creature_view.gd` zeichnen Karten und Wesen, `element_view.gd` den Elementzähler, `battle_layout.gd` die Aufteilung des Kampfes
|
||||||
- `translations/ui.csv` – englische Oberflächentexte
|
- `translations/ui.csv` – englische Oberflächentexte
|
||||||
- `design.md` – Gestaltung: Vorbilder, Karte, Kampfbildschirm, Bewegung, Menüs
|
- `design.md` – Gestaltung: Vorbilder, Karte, Kampfbildschirm, Bewegung, Menüs
|
||||||
|
|||||||
@@ -5,18 +5,17 @@
|
|||||||
## Grundidee
|
## Grundidee
|
||||||
- Kartenspiel wie zum Beispiel Magic
|
- Kartenspiel wie zum Beispiel Magic
|
||||||
- PC-Spiel
|
- PC-Spiel
|
||||||
- Duell: Spieler gegen einen KI-Gegner (oder einen Mitspieler im Netz), beide mit gleichen Regeln
|
- Duell: Spieler gegen einen KI-Gegner, beide mit gleichen Regeln
|
||||||
- Einzelspieler-Knopf führt zur Wahl der Schwierigkeit
|
- Einzelspieler-Knopf führt zur Wahl der Schwierigkeit
|
||||||
- Kampagne gegen benannte Magier als Fortschritt
|
- Kampagne gegen benannte Magier als Fortschritt
|
||||||
- Im lokalen Netz zu zweit spielbar, weitere als Zuschauer
|
|
||||||
|
|
||||||
## Sammeln
|
## Sammeln
|
||||||
- Jede Karte gehört jedem von Anfang an: wer zum ersten Mal spielt, kann so gut sein wie wer tausend Partien hat
|
- Man startet mit einem von vier Startdecks (40 Karten: 25 eines Elements bis Angriff 4, je fünf billige der anderen); alle Karten sammeln ist das lange Ziel
|
||||||
- Gesammelt wird die Seltenheitsstufe einer Karte (Common, Rare, Super Rare, Ultra Rare, Ultimate Rare), und die ist reine Optik wie bei Yu-Gi-Oh: silberner oder goldener Name, Holo-Bild, Relief, Funkeln, Schillern; auch im Netzwerkspiel sichtbar; Fusionen tragen die höhere Stufe der Eltern
|
- Jede eigene Karte hat eine Seltenheitsstufe (Common, Rare, Super Rare, Ultra Rare, Ultimate Rare); je Stufe +1/+1 bei gleichen Kosten, dazu Optik (Glanz, Holo, Relief, Funkeln); Fusionen tragen die höhere Stufe der Eltern
|
||||||
- Booster öffnen (Chancen 50/25/15/8/2 %, mindestens eine Rare): höhere Stufen werten Karten auf, der Rest wird zu Staub (50/100/200/500/1000)
|
- Booster öffnen (Chancen 45/25/15/10/5 %, mindestens eine Rare): neue Karten kommen in Sammlung und Stapel, von jeder Karte zählt die seltenste Version, die alte wird zu Staub, der Rest wird zu Staub (50/100/200/500/1000)
|
||||||
- Staub wertet gezielt auf (100/200/500/1000), eine Stufe abgeben bringt den Zerlegewert zurück
|
- Werkstatt: Staub wertet gezielt auf (100/200/500/1000), Zerlegen bringt den Wert zurück (Common verlässt die Sammlung), Herstellen einer fehlenden Karte kostet 200 Staub
|
||||||
- Kein Deckbau: alle ziehen vom gemeinsamen Stapel (jede Karte dreimal); zu Beginn fünf Karten, alle im ersten Zug spielbar; auf dem Stapel liegen drei Karten offen, nie teurer als das Mana des Zugs, beim Ziehen wählt man eine, eine neue rückt nach; das erste Ziehen im Zug ist frei, jedes weitere kostet 2 Mana
|
- Jeder hat sein Deck: 40 bis 60 eigene Karten, im Menü „Deck“ zusammengestellt (Auffüllen folgt dem Filter); zu Beginn fünf im ersten Zug spielbare Karten daraus; Ziehen bringt eine Karte, die erste im Zug frei, dann 1, 2, 3 … Mana; leerer Stapel: jeder Zug kostet 1, 2, 3 … Leben
|
||||||
- Mana für Ziehen, Legen (Kosten = 2/3 Angriff + 1/3 Leben, abgerundet; 0/2 kostet 0), und Verschmelzen, Abwerfen bringt 1; 1 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; höchstens 5 Handkarten
|
- Mana für Ziehen, Legen (Kosten = 2/3 Angriff + 1/3 Leben, abgerundet, höchstens 10; 0/2 kostet 0) und Verschmelzen (halbe Manasumme beider Karten), Abwerfen bringt 1; 1 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; höchstens 5 Handkarten
|
||||||
- Karten haben nur Angriff und Leben; Eigenschaften kommen allein über Elementpaare: Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug)
|
- Karten haben nur Angriff und Leben; Eigenschaften kommen allein über Elementpaare: Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug)
|
||||||
- Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink (auch im ersten Zug); verletzte Wesen heilen nicht von selbst
|
- Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink (auch im ersten Zug); verletzte Wesen heilen nicht von selbst
|
||||||
- Wesen lassen sich verdeckt legen (mittlere Maustaste oder Kartenmenü); aufdecken ist frei, frühestens im nächsten Zug, dann greift es sofort an; ein Angriff deckt auf
|
- Wesen lassen sich verdeckt legen (mittlere Maustaste oder Kartenmenü); aufdecken ist frei, frühestens im nächsten Zug, dann greift es sofort an; ein Angriff deckt auf
|
||||||
@@ -24,10 +23,10 @@
|
|||||||
- Kartenvorteil ist Manavorteil: Effekte geben Mana statt Nachziehen
|
- Kartenvorteil ist Manavorteil: Effekte geben Mana statt Nachziehen
|
||||||
|
|
||||||
## Elemente
|
## Elemente
|
||||||
- Jede Karte gehört zu genau einem Element
|
- Jede Karte gehört zu genau einem Element; je Element gibt es alle Werte von 0/1 bis 5/5 (120 Karten)
|
||||||
- Kombos sind die Namen verschmolzener Wesen (Feuer + Wasser wird Dampf, gleiche Elemente werden Inferno, Tiefsee, Berg, Orkan)
|
- Kombos sind die Namen verschmolzener Wesen (Feuer + Wasser wird Dampf, gleiche Elemente werden Inferno, Tiefsee, Berg, Orkan)
|
||||||
- Zwei eigene offene Wesen eines Elements: Sondereffekt für alle eigenen Wesen des Elements (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag)
|
- Zwei eigene offene Wesen eines Elements: Sondereffekt für alle eigenen Wesen des Elements (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag)
|
||||||
- Je 10 Mana an ausgespielten Karten eines Elements: +1 Angriff für die eigenen Wesen des Elements, jeder Spieler zählt für sich; verdeckt gelegte zählen mit, bekommen den Bonus aber erst offen; die Karte zeigt vorher, was sie bekäme; verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Effekte und Boni
|
- Je fünf ausgespielte Karten eines Elements: +1/+1 für die eigenen Wesen des Elements, jeder Spieler zählt für sich; verdeckt gelegte zählen mit, bekommen den Bonus aber erst offen; die Karte zeigt vorher, was sie bekäme; verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Effekte und Boni
|
||||||
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (wer im Vorteil ist, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht)
|
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (wer im Vorteil ist, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht)
|
||||||
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde stillhalten, auf dem Brett oder auf der Hand (Gamepad: über das Kartenmenü); das neue erbt Elemente und Eigenschaften beider, je Wert den besseren plus ein Drittel des schwächeren (aufgerundet, mindestens 1), Kosten folgen den Werten; Fusionskosten 1 Mana, verdoppelt je weiterer Verschmelzung
|
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde stillhalten, auf dem Brett oder auf der Hand (Gamepad: über das Kartenmenü); das neue erbt Elemente und Eigenschaften beider, je Wert den besseren plus ein Drittel des schwächeren (aufgerundet, mindestens 1), Kosten folgen den Werten; Fusionskosten 1 Mana, verdoppelt je weiterer Verschmelzung
|
||||||
- Alle Karten sind Wesen: keine Zauber, keine Fallen; Effekte lösen beim Ausspielen aus
|
- Alle Karten sind Wesen: keine Zauber, keine Fallen; Effekte lösen beim Ausspielen aus
|
||||||
|
|||||||
1662
data/karten.json
1662
data/karten.json
File diff suppressed because it is too large
Load Diff
48
design.md
48
design.md
@@ -6,7 +6,7 @@ Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entw
|
|||||||
|
|
||||||
- **Hearthstone** – Kosten und Werte springen aus dem Kartenrahmen (Kristall oben links, Angriff
|
- **Hearthstone** – Kosten und Werte springen aus dem Kartenrahmen (Kristall oben links, Angriff
|
||||||
und Leben in Kreisen unten), der Name liegt auf einem Band über die volle Breite, Text bleibt
|
und Leben in Kreisen unten), der Name liegt auf einem Band über die volle Breite, Text bleibt
|
||||||
kurz. Auf dem Brett: eigene Seite unten, Gegner oben, links Kästen und Stapel, rechts oben Knöpfe, Wesen
|
kurz. Auf dem Brett: eigene Seite unten, Gegner oben, links Elementzähler und Meldungen, rechts Stapel und Kästen beider Seiten, Wesen
|
||||||
als kompakte Kacheln statt voller Karten. Zugbeginn und Kartenzüge werden angesagt.
|
als kompakte Kacheln statt voller Karten. Zugbeginn und Kartenzüge werden angesagt.
|
||||||
- **Teamfight Tactics** – Synergien durch gleiche Elemente: zwei eigene geben den Effekt des
|
- **Teamfight Tactics** – Synergien durch gleiche Elemente: zwei eigene geben den Effekt des
|
||||||
Elements, drei auf dem Tisch den Wertebonus; die Elementleiste zeigt den Stand.
|
Elements, drei auf dem Tisch den Wertebonus; die Elementleiste zeigt den Stand.
|
||||||
@@ -41,14 +41,14 @@ Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
|
|||||||
|
|
||||||
- **Hand** als bis zu fünf Karten in Feldgröße unten, in einem leichten Bogen, die des Gegners
|
- **Hand** als bis zu fünf Karten in Feldgröße unten, in einem leichten Bogen, die des Gegners
|
||||||
gleich groß als Rückseiten oben; der
|
gleich groß als Rückseiten oben; der
|
||||||
Stapel links auf halber Höhe als drei offene Karten übereinander (Klick zieht die Karte, das erste
|
eigener Stapel rechts unten als Kartenstoß mit den Kosten des nächsten Ziehens (das erste im Zug
|
||||||
Ziehen im Zug frei), darüber der Gegnerkasten, darunter der eigene.
|
frei), darüber der eigene Kasten; der Stapel des Gegners rechts oben, sein Kasten darunter.
|
||||||
- **Verschmelzen** per Ziehen: eine Hand- oder Brettkarte auf eine passende ziehen, ein
|
- **Verschmelzen** per Ziehen: eine Hand- oder Brettkarte auf eine passende ziehen, ein
|
||||||
goldener Ring füllt sich eine Sekunde lang, dann verschmelzen die beiden. Mit Gamepad über
|
goldener Ring füllt sich eine Sekunde lang, dann verschmelzen die beiden. Mit Gamepad über
|
||||||
das Y-Menü („Verschmelzen mit …“, dann A auf dem Partner).
|
das Y-Menü („Verschmelzen mit …“, dann A auf dem Partner).
|
||||||
- **Eigenes Brett** mit fünf sichtbaren Plätzen; links daneben die eigene Leiste (Leben, Mana,
|
- **Eigenes Brett** mit fünf sichtbaren Plätzen; links daneben die eigene Leiste (Leben, Mana,
|
||||||
Einkommen, Zug), rechts „Rückgängig“, nur wenn es gerade möglich ist.
|
Einkommen, Zug), rechts „Rückgängig“, nur wenn es gerade möglich ist.
|
||||||
- **Elementleiste** je Spieler, die des Gegners darüber.
|
- **Elementzähler** je Spieler als Liste links neben seinem Brett, nur Elemente mit Wesen oder gespielten Karten.
|
||||||
Je Element zwei Symbole in seiner Form, gefüllt je offenem eigenen Wesen (beide voll: Sondereffekt),
|
Je Element zwei Symbole in seiner Form, gefüllt je offenem eigenen Wesen (beide voll: Sondereffekt),
|
||||||
daneben das Mana der ausgespielten Karten; je 10 gibt es +1 Angriff.
|
daneben das Mana der ausgespielten Karten; je 10 gibt es +1 Angriff.
|
||||||
- **Mittlere Maustaste** auf eine Handkarte legt sie verdeckt. **Kartenmenü** per Rechtsklick
|
- **Mittlere Maustaste** auf eine Handkarte legt sie verdeckt. **Kartenmenü** per Rechtsklick
|
||||||
@@ -59,7 +59,8 @@ Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
|
|||||||
macht Ziele, Texte und Balance eindeutig.
|
macht Ziele, Texte und Balance eindeutig.
|
||||||
- **Vorschau** steht fest rechts auf halber Höhe, darunter die Zusatzzeile, mit Resonanz, Feld
|
- **Vorschau** steht fest rechts auf halber Höhe, darunter die Zusatzzeile, mit Resonanz, Feld
|
||||||
und Feldkombo darunter.
|
und Feldkombo darunter.
|
||||||
- **Verlauf**, **Aufgeben** und **Zug beenden** als Knöpfe oben rechts; der Verlauf öffnet als Überlagerung.
|
- **Optionen** per Esc als Überlagerung: Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden; der Verlauf
|
||||||
|
öffnet darüber. Zug beenden per Leertaste oder Rechtsklick ins Leere, oben rechts nur Rückgängig.
|
||||||
- Handkarten tragen den Elementbonus, den sie offen gelegt bekämen; beim Angreifen
|
- Handkarten tragen den Elementbonus, den sie offen gelegt bekämen; beim Angreifen
|
||||||
steht der Elementkonter (+1/−1) am Ziel, bei Verdeckten nicht.
|
steht der Elementkonter (+1/−1) am Ziel, bei Verdeckten nicht.
|
||||||
|
|
||||||
@@ -75,14 +76,47 @@ Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
|
|||||||
|
|
||||||
## Menüs
|
## Menüs
|
||||||
|
|
||||||
- Hauptmenü: Titel und drei Spielarten links, darunter Booster, Sammlung, Einstellungen,
|
- Hauptmenü: Titel, Einzelspieler, Kampagne und Deck links, darunter Booster, Sammlung, Einstellungen,
|
||||||
Beenden; rechts drei Karten der Sammlung (höchste Stufen zuerst) als Schaufenster; Münzen und
|
Beenden; rechts drei Karten der Sammlung (höchste Stufen zuerst) als Schaufenster; Münzen und
|
||||||
Staub oben rechts.
|
Staub oben rechts.
|
||||||
- Unterbildschirme: Titel links oben, „Zurück“ rechts oben, Inhalt darunter.
|
- Unterbildschirme: Titel links oben, „Zurück“ rechts oben, Inhalt darunter.
|
||||||
- Kampagne als Pfad mit zehn Stationen je Kapitel, die nächste pulsiert; darunter die gewählte
|
- Kampagne als Pfad mit zehn Stationen je Kapitel, die nächste pulsiert; darunter die gewählte
|
||||||
Stufe mit Gegner, Leben, Belohnung und Merkmalen. Die Bosse werden über Leben und Merkmale
|
Stufe mit Gegner, Leben, Belohnung und Merkmalen. Die Bosse werden über Leben und Merkmale
|
||||||
stärker, nicht über Begleiter.
|
stärker, nicht über Begleiter.
|
||||||
- Sammlung: sechs Spalten, aufgewertete Karten zuerst, rechts die große Vorschau.
|
- Sammlung: sechs Spalten, eigene Karten zuerst (höchste Stufen vorn), fehlende abgedunkelt, rechts
|
||||||
|
die große Vorschau mit Aufwerten, Zerlegen oder Herstellen. Deck: gleiches Raster, nur eigene Karten,
|
||||||
|
Stapelkarten hell, die anderen abgedunkelt; Klick wechselt.
|
||||||
|
|
||||||
|
## Grafik
|
||||||
|
|
||||||
|
Ziel
|
||||||
|
- Wie ein normales Godot/Unity-Spiel, nicht primitiv
|
||||||
|
- Keine Kunstwerke
|
||||||
|
|
||||||
|
Stil
|
||||||
|
- Dunkler Tisch, Elemente leuchten
|
||||||
|
- Rahmen wie Hearthstone, Glanz wie Pokémon TCG
|
||||||
|
- Eine Palette, ein Font, eine Formensprache
|
||||||
|
- Palette aus style.gd erweitern, nicht ersetzen
|
||||||
|
- Font DejaVu Sans
|
||||||
|
|
||||||
|
Regeln
|
||||||
|
- Zahlen groß, Kontrast hoch
|
||||||
|
- Zustände sichtbar: Hover, Pressed, Disabled, Fokus
|
||||||
|
- Icons als SVG, ein Strichstil
|
||||||
|
|
||||||
|
Verbote
|
||||||
|
- Flache graue Panels
|
||||||
|
- Lila Verläufe
|
||||||
|
- Effekte ohne Zweck
|
||||||
|
|
||||||
|
Entschieden
|
||||||
|
- Richtung Stein: warm, Holz und Bronze, Bevel-Rahmen, Vignette
|
||||||
|
- Werte und Kristall bleiben im Kartenrahmen
|
||||||
|
- Rundungen und Kanten glatt, keine Treppchen
|
||||||
|
|
||||||
|
Offen
|
||||||
|
- Radius, Ränder, Schatten im Spiel nachziehen
|
||||||
|
|
||||||
## Offen
|
## Offen
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
|||||||
best = firsts[i].action
|
best = firsts[i].action
|
||||||
if best.type == "end":
|
if best.type == "end":
|
||||||
if game.can_draw(p)[0]:
|
if game.can_draw(p)[0]:
|
||||||
return {"type": "draw", "index": pick_index(game, p)} # Mana übrig und Platz auf der Hand: lieber ziehen als passen
|
return {"type": "draw"} # Mana übrig und Platz auf der Hand: lieber ziehen als passen
|
||||||
return best
|
return best
|
||||||
return map_action(game, view, best)
|
return map_action(game, view, best)
|
||||||
|
|
||||||
@@ -258,24 +258,10 @@ static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
|
|||||||
if t != null:
|
if t != null:
|
||||||
return {"type": "attack", "creature": cr, "target": t}
|
return {"type": "attack", "creature": cr, "target": t}
|
||||||
if game.can_draw(p)[0]:
|
if game.can_draw(p)[0]:
|
||||||
return {"type": "draw", "index": pick_index(game, p)}
|
return {"type": "draw"}
|
||||||
return {"type": "end"}
|
return {"type": "end"}
|
||||||
|
|
||||||
|
|
||||||
## Welche der offenen Karten: die wertvollste, die nach dem Ziehen noch bezahlbar wäre, sonst die billigste.
|
|
||||||
static func pick_index(game: Game, p: Game.Player) -> int:
|
|
||||||
var left: int = p.energy - game.draw_cost(p)
|
|
||||||
var best := 0
|
|
||||||
var best_score := -INF
|
|
||||||
for i in p.offer.size():
|
|
||||||
var c: Cards.Card = p.offer[i]
|
|
||||||
var score := float(c.attack + c.health) if c.cost <= left else -10.0 - c.cost
|
|
||||||
if score > best_score:
|
|
||||||
best_score = score
|
|
||||||
best = i
|
|
||||||
return best
|
|
||||||
|
|
||||||
|
|
||||||
## Ziel eines Angriffs: sichere Abtausche, dann gute Abtausche, dann das schwächste Wesen;
|
## Ziel eines Angriffs: sichere Abtausche, dann gute Abtausche, dann das schwächste Wesen;
|
||||||
## der Held nur, wenn kein Wesen mehr steht.
|
## der Held nur, wenn kein Wesen mehr steht.
|
||||||
static func pick_attack_target(game: Game, p: Game.Player, cr: Game.Creature):
|
static func pick_attack_target(game: Game, p: Game.Player, cr: Game.Creature):
|
||||||
@@ -302,7 +288,7 @@ static func apply_action(game: Game, p: Game.Player, action: Dictionary) -> void
|
|||||||
"play":
|
"play":
|
||||||
game.play_card(p, action.card, bool(action.get("face_down", false)))
|
game.play_card(p, action.card, bool(action.get("face_down", false)))
|
||||||
"draw":
|
"draw":
|
||||||
game.draw(p, int(action.get("index", 0)))
|
game.draw(p)
|
||||||
"reveal":
|
"reveal":
|
||||||
game.reveal(p, action.creature)
|
game.reveal(p, action.creature)
|
||||||
"attack":
|
"attack":
|
||||||
|
|||||||
@@ -64,9 +64,16 @@ static func stage(i: int) -> Dictionary:
|
|||||||
return CHAPTERS[chapter(i) - 1][i % STAGES.size()]
|
return CHAPTERS[chapter(i) - 1][i % STAGES.size()]
|
||||||
|
|
||||||
|
|
||||||
## Boss einer Stufe mit dem Leben und den Merkmalen der Stufe.
|
## Boss einer Stufe mit dem Leben, den Merkmalen und einem Stapel der Stufe; ab Kapitel 2 sind
|
||||||
static func opponent(stage_index: int) -> Game.Player:
|
## seine Karten eine Seltenheitsstufe höher (+1/+1), ab Kapitel 3 zwei.
|
||||||
|
static func opponent(stage_index: int, r: RandomNumberGenerator = null) -> Game.Player:
|
||||||
var st := stage(stage_index)
|
var st := stage(stage_index)
|
||||||
var boss := Game.Player.new(st.name, false, int(st.life), st.title)
|
var boss := Game.Player.new(st.name, false, int(st.life), st.title)
|
||||||
boss.perks = Array(st.perks)
|
boss.perks = Array(st.perks)
|
||||||
|
if r == null:
|
||||||
|
r = Game.make_rng()
|
||||||
|
var ch := chapter(stage_index)
|
||||||
|
boss.deck = DeckGen.opponent_deck(r, ch)
|
||||||
|
for id in boss.deck:
|
||||||
|
boss.looks[id] = ch - 1
|
||||||
return boss
|
return boss
|
||||||
|
|||||||
@@ -93,9 +93,9 @@ static func best_element(own: Array, other: Array) -> String:
|
|||||||
return best
|
return best
|
||||||
|
|
||||||
|
|
||||||
## Kosten einer Karte: zwei Drittel Angriff plus ein Drittel Leben, abgerundet (0/2 kostet nichts).
|
## Kosten einer Karte: zwei Drittel Angriff plus ein Drittel Leben, abgerundet, höchstens 10 (0/2 kostet nichts).
|
||||||
static func cost_of(attack: int, health: int) -> int:
|
static func cost_of(attack: int, health: int) -> int:
|
||||||
return (2 * attack + health) / 3
|
return mini(10, (2 * attack + health) / 3)
|
||||||
|
|
||||||
|
|
||||||
## Wert eines verschmolzenen Wesens: der bessere Wert plus ein Drittel des schwächeren (mindestens 1),
|
## Wert eines verschmolzenen Wesens: der bessere Wert plus ein Drittel des schwächeren (mindestens 1),
|
||||||
@@ -132,8 +132,7 @@ static func fused(a: Card, b: Card, attack: int = -1, health: int = -1) -> Card:
|
|||||||
for k in KEYWORDS:
|
for k in KEYWORDS:
|
||||||
if k in a.keywords or k in b.keywords:
|
if k in a.keywords or k in b.keywords:
|
||||||
c.keywords.append(k)
|
c.keywords.append(k)
|
||||||
c.fusions = maxi(a.fusions, b.fusions) + 1
|
c.look = maxi(a.look, b.look) # die höhere Stufe gewinnt
|
||||||
c.look = maxi(a.look, b.look) # die höhere Stufe gewinnt; Game.fuse rechnet die Sammlung ein
|
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
@@ -157,19 +156,18 @@ class Card extends RefCounted:
|
|||||||
var elements: Array
|
var elements: Array
|
||||||
var attack: int = 0
|
var attack: int = 0
|
||||||
var health: int = 0
|
var health: int = 0
|
||||||
var cost: int = 0 # abgeleitet: (2 · Angriff + Leben) / 3, abgerundet
|
var cost: int = 0 # abgeleitet: (2 · Angriff + Leben) / 3, abgerundet, höchstens 10 (aus den Grundwerten)
|
||||||
var keywords: Array = [] # feste Eigenschaften der Karte (Elementpaare geben ihre zur Laufzeit)
|
var keywords: Array = [] # feste Eigenschaften der Karte (Elementpaare geben ihre zur Laufzeit)
|
||||||
var fusions := 0 # wie oft verschmolzen (Verschmelzen kostet 1, 2, 4, 8 …)
|
var look := -1 # Seltenheitsstufe des Exemplars (gibt +1/+1 je Stufe); -1: Grundkarte aus dem Pool
|
||||||
var look := -1 # Seltenheitsstufe verschmolzener Karten (die höhere der Eltern); -1: aus der Sammlung des Besitzers
|
|
||||||
|
|
||||||
func to_dict() -> Dictionary:
|
func to_dict() -> Dictionary:
|
||||||
var d := {"id": id, "name": name, "name_en": name_en, "elements": elements, "attack": attack, "health": health}
|
var d := {"id": id, "name": name, "name_en": name_en, "elements": elements, "attack": attack, "health": health}
|
||||||
if not keywords.is_empty():
|
if not keywords.is_empty():
|
||||||
d["keywords"] = keywords
|
d["keywords"] = keywords
|
||||||
if fusions > 0:
|
|
||||||
d["fusions"] = fusions
|
|
||||||
if look >= 0:
|
if look >= 0:
|
||||||
d["look"] = look
|
d["look"] = look
|
||||||
|
if cost != Cards.cost_of(attack, health):
|
||||||
|
d["cost"] = cost
|
||||||
return d
|
return d
|
||||||
|
|
||||||
## Stufe dieser Karte für einen Besitzer mit dieser Sammlung (Game.Player.looks).
|
## Stufe dieser Karte für einen Besitzer mit dieser Sammlung (Game.Player.looks).
|
||||||
@@ -184,9 +182,8 @@ class Card extends RefCounted:
|
|||||||
c.elements = Array(d.elements)
|
c.elements = Array(d.elements)
|
||||||
c.attack = int(d.get("attack", 0))
|
c.attack = int(d.get("attack", 0))
|
||||||
c.health = int(d.get("health", 0))
|
c.health = int(d.get("health", 0))
|
||||||
c.cost = Cards.cost_of(c.attack, c.health)
|
c.cost = int(d.get("cost", Cards.cost_of(c.attack, c.health)))
|
||||||
c.keywords = Array(d.get("keywords", []))
|
c.keywords = Array(d.get("keywords", []))
|
||||||
c.fusions = int(d.get("fusions", 0))
|
|
||||||
c.look = int(d.get("look", -1))
|
c.look = int(d.get("look", -1))
|
||||||
return c
|
return c
|
||||||
|
|
||||||
@@ -194,6 +191,15 @@ class Card extends RefCounted:
|
|||||||
func copy() -> Card:
|
func copy() -> Card:
|
||||||
return Card.from_dict(to_dict())
|
return Card.from_dict(to_dict())
|
||||||
|
|
||||||
|
## Exemplar in einer Seltenheitsstufe: +1/+1 je Stufe, die Kosten bleiben die der Grundkarte.
|
||||||
|
func leveled(level: int) -> Card:
|
||||||
|
var c := copy()
|
||||||
|
c.look = level
|
||||||
|
c.attack += level
|
||||||
|
c.health += level
|
||||||
|
c.cost = cost
|
||||||
|
return c
|
||||||
|
|
||||||
## Anzeigename in der aktuellen Sprache.
|
## Anzeigename in der aktuellen Sprache.
|
||||||
func title() -> String:
|
func title() -> String:
|
||||||
if name_en != "" and TranslationServer.get_locale().begins_with("en"):
|
if name_en != "" and TranslationServer.get_locale().begins_with("en"):
|
||||||
@@ -206,10 +212,6 @@ class Card extends RefCounted:
|
|||||||
func combo() -> String:
|
func combo() -> String:
|
||||||
return Cards.combo_name(elements)
|
return Cards.combo_name(elements)
|
||||||
|
|
||||||
## Mana für die nächste Verschmelzung mit dieser Karte.
|
|
||||||
func fusion_cost() -> int:
|
|
||||||
return 1 << fusions
|
|
||||||
|
|
||||||
## Kartentext: die festen Eigenschaften.
|
## Kartentext: die festen Eigenschaften.
|
||||||
func text() -> String:
|
func text() -> String:
|
||||||
if keywords.is_empty():
|
if keywords.is_empty():
|
||||||
|
|||||||
@@ -1,16 +1,54 @@
|
|||||||
class_name DeckGen
|
class_name DeckGen
|
||||||
## Der gemeinsame Stapel (jede Karte dreimal) und Gegner.
|
## Stapel: die vier Startdecks, Auffüllen aus der Sammlung, zufällige Stapel für Gegner.
|
||||||
|
|
||||||
const COPIES := 3
|
const CHEAP_IN_RANDOM := 12 # so viele im ersten Zug bezahlbare Karten hat ein zufälliger Stapel mindestens
|
||||||
|
|
||||||
|
|
||||||
## Frisch gemischter Stapel; das Ende des Arrays ist oben.
|
## Zufälliger Stapel aus DECK_MIN verschiedenen Karten bis max_cost, mit genug billigen für den Anfang.
|
||||||
static func deck(r: RandomNumberGenerator) -> Array:
|
static func random_deck(r: RandomNumberGenerator, max_cost: int = 99) -> Array:
|
||||||
|
var cheap := Cards.all().filter(func(c): return c.cost <= Game.START_MANA)
|
||||||
|
var rest := Cards.all().filter(func(c): return c.cost > Game.START_MANA and c.cost <= max_cost)
|
||||||
|
Game.shuffle(cheap, r)
|
||||||
|
Game.shuffle(rest, r)
|
||||||
|
var out := []
|
||||||
|
for c in cheap.slice(0, CHEAP_IN_RANDOM) + rest + cheap.slice(CHEAP_IN_RANDOM):
|
||||||
|
if out.size() >= Game.DECK_MIN:
|
||||||
|
break
|
||||||
|
out.append(c.id)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Stapel eines Kampagnengegners: Kapitel 1 bis Kosten 3, Kapitel 2 bis 5, Kapitel 3 alles.
|
||||||
|
static func opponent_deck(r: RandomNumberGenerator, chapter: int) -> Array:
|
||||||
|
return random_deck(r, [3, 5, 99][clampi(chapter - 1, 0, 2)])
|
||||||
|
|
||||||
|
|
||||||
|
## Startdeck für ein Element: die 25 Karten des Elements bis Angriff 4, dazu je fünf billige der
|
||||||
|
## anderen Elemente (DECK_MIN Karten).
|
||||||
|
static func start_deck(element: String) -> Array:
|
||||||
var out := []
|
var out := []
|
||||||
for c in Cards.all():
|
for c in Cards.all():
|
||||||
for i in COPIES:
|
if c.elements[0] == element and c.attack <= 4:
|
||||||
out.append(c)
|
out.append(c.id)
|
||||||
Game.shuffle(out, r)
|
for e in Cards.ELEMENTS:
|
||||||
|
if e == element:
|
||||||
|
continue
|
||||||
|
var cheap := Cards.all().filter(func(c): return c.elements[0] == e and c.cost <= Game.START_MANA)
|
||||||
|
cheap.sort_custom(func(a, b): return a.cost < b.cost if a.cost != b.cost else a.attack > b.attack)
|
||||||
|
for c in cheap.slice(0, 5):
|
||||||
|
out.append(c.id)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Füllt deck aus pool (Karten-IDs) bis limit auf, die teuersten zuerst; nichts doppelt.
|
||||||
|
static func fill(deck: Array, pool: Array, limit: int) -> Array:
|
||||||
|
var out := deck.duplicate()
|
||||||
|
var cards := pool.filter(func(id): return not (id in out)).map(func(id): return Cards.by_id(id))
|
||||||
|
cards.sort_custom(func(a, b): return a.cost > b.cost if a.cost != b.cost else a.id < b.id)
|
||||||
|
for c in cards:
|
||||||
|
if out.size() >= limit:
|
||||||
|
break
|
||||||
|
out.append(c.id)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
149
scripts/game.gd
149
scripts/game.gd
@@ -7,11 +7,11 @@ const MAX_BOARD := 5
|
|||||||
const MAX_HAND := 5
|
const MAX_HAND := 5
|
||||||
const START_HAND := 5 # Startkarten, alle im ersten Zug bezahlbar
|
const START_HAND := 5 # Startkarten, alle im ersten Zug bezahlbar
|
||||||
const START_MANA := 1 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt
|
const START_MANA := 1 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt
|
||||||
const DRAW_COST := 2 # Ziehen: eine bezahlbare Karte vom Stapel
|
const DECK_MIN := 40 # Karten je Stapel: mindestens so viele (Gegner und Startdecks genau so viele)
|
||||||
|
const DECK_MAX := 60 # und höchstens so viele
|
||||||
const START_LIFE := 30
|
const START_LIFE := 30
|
||||||
const ELEMENT_PAIR := 2 # so viele eigene offene Wesen eines Elements: Sondereffekt des Elements
|
const ELEMENT_PAIR := 2 # so viele eigene offene Wesen eines Elements: Sondereffekt des Elements
|
||||||
const ELEMENT_MANA := 10 # je so viel Mana an ausgespielten Karten eines Elements: +1 Angriff für die eigenen Wesen des Elements
|
const ELEMENT_COUNT := 5 # je so viele ausgespielte Karten eines Elements: +1/+1 für die eigenen Wesen des Elements
|
||||||
const OFFER := 3 # offene Karten auf dem Stapel, höchstens so teuer wie das Mana des Zugs; beim Ziehen wählt man eine, eine neue rückt nach
|
|
||||||
const DISCARD_GAIN := 1 # Abwerfen einer Handkarte bringt so viel Mana
|
const DISCARD_GAIN := 1 # Abwerfen einer Handkarte bringt so viel Mana
|
||||||
|
|
||||||
# Merkmale der Kampagnen-Bosse (Player.perks).
|
# Merkmale der Kampagnen-Bosse (Player.perks).
|
||||||
@@ -129,17 +129,18 @@ class Player extends RefCounted:
|
|||||||
var is_human: bool
|
var is_human: bool
|
||||||
var life: int
|
var life: int
|
||||||
var max_life: int
|
var max_life: int
|
||||||
var hand: Array = [] # eigene Exemplare (Cards.Card.copy)
|
var hand: Array = [] # eigene Exemplare (Cards.Card, in der eigenen Seltenheitsstufe)
|
||||||
var board: Array = []
|
var board: Array = []
|
||||||
|
var deck: Array = [] # eigener Stapel: Karten-IDs, das Ende ist oben
|
||||||
var energy := 0 # Mana in diesem Zug; der Rest verfällt
|
var energy := 0 # Mana in diesem Zug; der Rest verfällt
|
||||||
var max_energy := 0 # Mana zu Zugbeginn (START_MANA + eigene Züge)
|
var max_energy := 0 # Mana zu Zugbeginn (START_MANA + eigene Züge)
|
||||||
var order := 0 # Platz in der Zugreihenfolge
|
var order := 0 # Platz in der Zugreihenfolge
|
||||||
var perks: Array = [] # Boss-Merkmale (siehe PERKS)
|
var perks: Array = [] # Boss-Merkmale (siehe PERKS)
|
||||||
var looks := {} # Karten-ID -> Seltenheitsstufe, reine Optik (siehe Profile)
|
var looks := {} # Karten-ID -> Seltenheitsstufe, reine Optik (siehe Profile)
|
||||||
var turns := 0 # begonnene eigene Züge
|
var turns := 0 # begonnene eigene Züge
|
||||||
var played := {} # Element -> Mana der bisher ausgespielten Karten (je ELEMENT_MANA: +1 Angriff)
|
var played := {} # Element -> bisher ausgespielte Karten (je ELEMENT_COUNT: +1/+1)
|
||||||
var offer: Array = [] # die OFFER offenen Karten auf dem Stapel (nur der Besitzer sieht sie)
|
var draws := 0 # in diesem Zug gezogene Karten; die n-te kostet n-1 Mana
|
||||||
var drew := false # in diesem Zug schon gezogen? Das erste Ziehen ist frei
|
var fatigue := 0 # Züge ohne Karten im Stapel: so viel Leben kostet der nächste
|
||||||
|
|
||||||
func _init(n: String, h: bool, l: int, t: String = "") -> void:
|
func _init(n: String, h: bool, l: int, t: String = "") -> void:
|
||||||
name = n
|
name = n
|
||||||
@@ -153,6 +154,25 @@ class Player extends RefCounted:
|
|||||||
func alive() -> bool:
|
func alive() -> bool:
|
||||||
return life > 0
|
return life > 0
|
||||||
|
|
||||||
|
## Eine Karte vom eigenen Stapel als Exemplar in der eigenen Stufe; null, wenn er leer ist.
|
||||||
|
## Mit max_cost kommt die oberste bezahlbare, zu teure wandern nach unten (Starthand).
|
||||||
|
func draw_from_deck(max_cost: int = 99) -> Cards.Card:
|
||||||
|
var skipped := []
|
||||||
|
var id := ""
|
||||||
|
while not deck.is_empty():
|
||||||
|
var top: String = deck.pop_back()
|
||||||
|
if Cards.by_id(top).cost <= max_cost:
|
||||||
|
id = top
|
||||||
|
break
|
||||||
|
skipped.append(top)
|
||||||
|
if id == "" and not skipped.is_empty(): # nichts Bezahlbares: dann eben die oberste
|
||||||
|
id = skipped.pop_front()
|
||||||
|
for sk in skipped:
|
||||||
|
deck.insert(0, sk)
|
||||||
|
if id == "":
|
||||||
|
return null
|
||||||
|
return Cards.by_id(id).leveled(int(looks.get(id, 0)))
|
||||||
|
|
||||||
## Offene eigene Wesen mit diesem Element.
|
## Offene eigene Wesen mit diesem Element.
|
||||||
func element_count(e: String) -> int:
|
func element_count(e: String) -> int:
|
||||||
var n := 0
|
var n := 0
|
||||||
@@ -172,20 +192,19 @@ class Player extends RefCounted:
|
|||||||
p.looks = looks
|
p.looks = looks
|
||||||
p.turns = turns
|
p.turns = turns
|
||||||
p.played = played.duplicate()
|
p.played = played.duplicate()
|
||||||
p.offer = offer.duplicate()
|
p.deck = deck.duplicate()
|
||||||
p.drew = drew
|
p.draws = draws
|
||||||
|
p.fatigue = fatigue
|
||||||
for c in board:
|
for c in board:
|
||||||
p.board.append(c.clone(p))
|
p.board.append(c.clone(p))
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
|
||||||
var human: Player # der (erste) menschliche Spieler
|
var human: Player # der menschliche Spieler
|
||||||
var humans: Array = [] # alle menschlichen Spieler (im Netz zwei)
|
|
||||||
var opponent: Player # der andere am Tisch
|
var opponent: Player # der andere am Tisch
|
||||||
var players: Array = [] # in Zugreihenfolge
|
var players: Array = [] # in Zugreihenfolge
|
||||||
var winner_player: Player = null
|
var winner_player: Player = null
|
||||||
var rng: RandomNumberGenerator
|
var rng: RandomNumberGenerator
|
||||||
var deck: Array = [] # gemeinsamer Stapel, das Ende ist oben (nur Host und Einzelspieler)
|
|
||||||
var turn := 0
|
var turn := 0
|
||||||
var current_idx := 0
|
var current_idx := 0
|
||||||
var log: Array[String] = []
|
var log: Array[String] = []
|
||||||
@@ -199,7 +218,6 @@ func _init(h: Player, o: Player, r: RandomNumberGenerator = null) -> void:
|
|||||||
human = h
|
human = h
|
||||||
opponent = o
|
opponent = o
|
||||||
players = [h, o]
|
players = [h, o]
|
||||||
humans = players.filter(func(p): return p.is_human)
|
|
||||||
if r == null:
|
if r == null:
|
||||||
r = make_rng()
|
r = make_rng()
|
||||||
rng = r
|
rng = r
|
||||||
@@ -250,7 +268,6 @@ func clone() -> Game:
|
|||||||
var idx := players.find(human)
|
var idx := players.find(human)
|
||||||
var g := Game.new(copies[idx], copies[1 - idx], RandomNumberGenerator.new())
|
var g := Game.new(copies[idx], copies[1 - idx], RandomNumberGenerator.new())
|
||||||
g.players = copies # gleiche Zugreihenfolge wie das Original
|
g.players = copies # gleiche Zugreihenfolge wie das Original
|
||||||
g.deck = deck.duplicate()
|
|
||||||
g.turn = turn
|
g.turn = turn
|
||||||
g.current_idx = current_idx
|
g.current_idx = current_idx
|
||||||
g.winner = winner
|
g.winner = winner
|
||||||
@@ -278,16 +295,21 @@ func masked_clone(viewer: Player) -> Game:
|
|||||||
|
|
||||||
# --- Zugablauf ---------------------------------------------------------------
|
# --- Zugablauf ---------------------------------------------------------------
|
||||||
|
|
||||||
|
## Jeder spielt mit seinem eigenen Stapel (leer: ein zufälliger); die Starthand sind fünf im
|
||||||
|
## ersten Zug bezahlbare Karten daraus.
|
||||||
func start(fixed_order: bool = false) -> void:
|
func start(fixed_order: bool = false) -> void:
|
||||||
if not fixed_order:
|
if not fixed_order:
|
||||||
shuffle(players, rng) # zufällige Zugreihenfolge
|
shuffle(players, rng) # zufällige Zugreihenfolge
|
||||||
deck = DeckGen.deck(rng)
|
|
||||||
for i in players.size():
|
for i in players.size():
|
||||||
var p: Player = players[i]
|
var p: Player = players[i]
|
||||||
p.order = i
|
p.order = i
|
||||||
for c in draw_cards(START_HAND, START_MANA):
|
if p.deck.is_empty():
|
||||||
p.hand.append(c.copy())
|
p.deck = DeckGen.random_deck(rng)
|
||||||
p.offer = draw_cards(OFFER, START_MANA)
|
shuffle(p.deck, rng)
|
||||||
|
for k in START_HAND:
|
||||||
|
var c := p.draw_from_deck(START_MANA)
|
||||||
|
if c != null:
|
||||||
|
p.hand.append(c)
|
||||||
turn = 1
|
turn = 1
|
||||||
current_idx = 0
|
current_idx = 0
|
||||||
say("Zugreihenfolge: " + ", ".join(players.map(func(p): return p.name)))
|
say("Zugreihenfolge: " + ", ".join(players.map(func(p): return p.name)))
|
||||||
@@ -297,54 +319,31 @@ func start(fixed_order: bool = false) -> void:
|
|||||||
begin_turn(current())
|
begin_turn(current())
|
||||||
|
|
||||||
|
|
||||||
## Nimmt n Karten vom Stapel, die höchstens max_cost kosten; zu teure wandern unter den Stapel.
|
## Ziehen: eine Karte vom eigenen Stapel. Die erste im Zug ist frei, jede weitere kostet eins
|
||||||
## Ist der Stapel leer, kommt ein frischer.
|
## mehr (0, 1, 2 …).
|
||||||
func draw_cards(n: int, max_cost: int = 999) -> Array:
|
|
||||||
var out := []
|
|
||||||
var skipped := []
|
|
||||||
var looked := 0
|
|
||||||
while out.size() < n:
|
|
||||||
if deck.is_empty():
|
|
||||||
deck = DeckGen.deck(rng)
|
|
||||||
say("Der Stapel wird neu gemischt.")
|
|
||||||
var c: Cards.Card = deck.pop_back()
|
|
||||||
looked += 1
|
|
||||||
if c.cost <= max_cost or looked > deck.size() + skipped.size() + n:
|
|
||||||
out.append(c)
|
|
||||||
else:
|
|
||||||
skipped.append(c)
|
|
||||||
for c in skipped:
|
|
||||||
deck.insert(0, c)
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
## Ziehen: eine der offenen Karten kommt auf die Hand, eine neue rückt nach. Das erste Ziehen
|
|
||||||
## im Zug ist frei, jedes weitere kostet DRAW_COST.
|
|
||||||
func can_draw(p: Player) -> Array:
|
func can_draw(p: Player) -> Array:
|
||||||
if winner != "" or p != current():
|
if winner != "" or p != current():
|
||||||
return [false, "Nicht dein Zug"]
|
return [false, "Nicht dein Zug"]
|
||||||
if p.hand.size() >= MAX_HAND:
|
if p.hand.size() >= MAX_HAND:
|
||||||
return [false, "Deine Hand ist voll"]
|
return [false, "Deine Hand ist voll"]
|
||||||
|
if p.deck.is_empty():
|
||||||
|
return [false, "Dein Stapel ist leer"]
|
||||||
if p.energy < draw_cost(p):
|
if p.energy < draw_cost(p):
|
||||||
return [false, "Nicht genug Mana"]
|
return [false, "Nicht genug Mana"]
|
||||||
return [true, ""]
|
return [true, ""]
|
||||||
|
|
||||||
|
|
||||||
func draw_cost(p: Player) -> int:
|
func draw_cost(p: Player) -> int:
|
||||||
return 0 if not p.drew or "ziehen_gratis" in p.perks else DRAW_COST
|
return maxi(0, p.draws - (1 if "ziehen_gratis" in p.perks else 0))
|
||||||
|
|
||||||
|
|
||||||
func draw(p: Player, index: int = 0) -> String:
|
func draw(p: Player) -> String:
|
||||||
var ok := can_draw(p)
|
var ok := can_draw(p)
|
||||||
if not ok[0]:
|
if not ok[0]:
|
||||||
return ok[1]
|
return ok[1]
|
||||||
if index < 0 or index >= p.offer.size():
|
|
||||||
return "Keine Karte gewählt"
|
|
||||||
p.energy -= draw_cost(p)
|
p.energy -= draw_cost(p)
|
||||||
p.drew = true
|
p.draws += 1
|
||||||
var card: Cards.Card = p.offer[index]
|
p.hand.append(p.draw_from_deck())
|
||||||
p.hand.append(card.copy())
|
|
||||||
p.offer[index] = draw_cards(1, maxi(p.max_energy, START_MANA))[0] # nie teurer als das Mana dieses Zugs
|
|
||||||
say("%s zieht." % p.name)
|
say("%s zieht." % p.name)
|
||||||
emit({"type": "draw", "player": players.find(p)})
|
emit({"type": "draw", "player": players.find(p)})
|
||||||
return ""
|
return ""
|
||||||
@@ -359,9 +358,15 @@ func begin_turn(p: Player) -> void:
|
|||||||
c.ready = true # lag eine Runde: greift jetzt an, Verdecktes darf aufgedeckt werden
|
c.ready = true # lag eine Runde: greift jetzt an, Verdecktes darf aufgedeckt werden
|
||||||
c.attacks_done = 0
|
c.attacks_done = 0
|
||||||
p.turns += 1
|
p.turns += 1
|
||||||
p.drew = false # das erste Ziehen im Zug ist wieder frei
|
p.draws = 0 # das erste Ziehen im Zug ist wieder frei
|
||||||
say("--- Zug %d: %s ---" % [turn, p.name])
|
say("--- Zug %d: %s ---" % [turn, p.name])
|
||||||
emit({"type": "turn", "player": players.find(p)})
|
emit({"type": "turn", "player": players.find(p)})
|
||||||
|
if p.deck.is_empty(): # Erschöpfung: ohne Stapel kostet jeder Zug mehr Leben, damit es ein Ende gibt
|
||||||
|
p.fatigue += 1
|
||||||
|
p.life -= p.fatigue
|
||||||
|
say("%s hat keine Karten mehr und verliert %d Leben." % [p.name, p.fatigue])
|
||||||
|
emit({"type": "damage", "target": ref_of(p), "amount": p.fatigue})
|
||||||
|
check_end()
|
||||||
|
|
||||||
|
|
||||||
## Zugende: Wesen mit Regeneration heilen ganz, dann ist der andere dran.
|
## Zugende: Wesen mit Regeneration heilen ganz, dann ist der andere dran.
|
||||||
@@ -417,7 +422,7 @@ func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
|
|||||||
cr.max_health += 1
|
cr.max_health += 1
|
||||||
p.board.append(cr)
|
p.board.append(cr)
|
||||||
for e in card.elements:
|
for e in card.elements:
|
||||||
p.played[e] = int(p.played.get(e, 0)) + card.cost
|
p.played[e] = int(p.played.get(e, 0)) + 1
|
||||||
if face_down:
|
if face_down:
|
||||||
say("%s legt ein Wesen verdeckt." % p.name)
|
say("%s legt ein Wesen verdeckt." % p.name)
|
||||||
else:
|
else:
|
||||||
@@ -492,9 +497,9 @@ static func look_of(p: Player, c: Cards.Card) -> int:
|
|||||||
return c.look if c.look >= 0 else int(p.looks.get(c.id, 0))
|
return c.look if c.look >= 0 else int(p.looks.get(c.id, 0))
|
||||||
|
|
||||||
|
|
||||||
## Kosten: 1 Mana, verdoppelt je Verschmelzung, die schon in einer der beiden steckt.
|
## Kosten: die Hälfte der Manasumme beider Karten, aufgerundet.
|
||||||
static func fusion_cost(a, b) -> int:
|
static func fusion_cost(a, b) -> int:
|
||||||
return 1 << maxi(card_of(a).fusions, card_of(b).fusions)
|
return ceili((card_of(a).cost + card_of(b).cost) / 2.0)
|
||||||
|
|
||||||
|
|
||||||
## a und b: zwei eigene Wesen auf dem Brett oder zwei eigene Handkarten.
|
## a und b: zwei eigene Wesen auf dem Brett oder zwei eigene Handkarten.
|
||||||
@@ -539,11 +544,11 @@ func fuse(p: Player, a, b) -> String:
|
|||||||
## das Ergebnis ist nur angriffsbereit, wenn beide es waren.
|
## das Ergebnis ist nur angriffsbereit, wenn beide es waren.
|
||||||
func _fuse_board(p: Player, a: Creature, b: Creature) -> String:
|
func _fuse_board(p: Player, a: Creature, b: Creature) -> String:
|
||||||
var card := Cards.fused(a.card, b.card, Cards.fusion_value(a.attack, b.attack),
|
var card := Cards.fused(a.card, b.card, Cards.fusion_value(a.attack, b.attack),
|
||||||
Cards.fusion_value(a.max_health, b.max_health))
|
Cards.fusion_value(a.max_health - a.aura, b.max_health - b.aura))
|
||||||
card.look = maxi(look_of(p, a.card), look_of(p, b.card))
|
card.look = maxi(look_of(p, a.card), look_of(p, b.card))
|
||||||
var cr := Creature.new(card, p, next_uid)
|
var cr := Creature.new(card, p, next_uid)
|
||||||
next_uid += 1
|
next_uid += 1
|
||||||
cr.health = mini(card.health, Cards.fusion_value(a.health, b.health))
|
cr.health = mini(card.health, Cards.fusion_value(mini(a.health, a.max_health - a.aura), mini(b.health, b.max_health - b.aura)))
|
||||||
cr.ready = a.ready and b.ready
|
cr.ready = a.ready and b.ready
|
||||||
cr.attacks_done = maxi(a.attacks_done, b.attacks_done)
|
cr.attacks_done = maxi(a.attacks_done, b.attacks_done)
|
||||||
var at := p.board.find(a)
|
var at := p.board.find(a)
|
||||||
@@ -560,11 +565,11 @@ func _fuse_board(p: Player, a: Creature, b: Creature) -> String:
|
|||||||
|
|
||||||
# --- Elementboni ---------------------------------------------------------------
|
# --- Elementboni ---------------------------------------------------------------
|
||||||
|
|
||||||
## +1 Angriff je volle ELEMENT_MANA an ausgespielten Karten eines Elements des Spielers; mehrere Elemente zählen alle.
|
## +1/+1 je volle ELEMENT_COUNT ausgespielte Karten eines Elements des Spielers; mehrere Elemente zählen alle.
|
||||||
static func aura_of(elements: Array, played: Dictionary) -> int:
|
static func aura_of(elements: Array, played: Dictionary) -> int:
|
||||||
var a := 0
|
var a := 0
|
||||||
for e in elements:
|
for e in elements:
|
||||||
a += int(played.get(e, 0)) / ELEMENT_MANA
|
a += int(played.get(e, 0)) / ELEMENT_COUNT
|
||||||
return a
|
return a
|
||||||
|
|
||||||
|
|
||||||
@@ -579,18 +584,24 @@ func aura_for(c: Creature) -> int:
|
|||||||
return a
|
return a
|
||||||
|
|
||||||
|
|
||||||
## Rechnet die Elementboni neu; sie wirken nur auf den Angriff (Creature.power).
|
## Rechnet die Elementboni neu. Kommt ein Bonus, steigen Leben und Höchstleben; geht er
|
||||||
|
## (beim Verdecken), sinkt nur das Höchstleben, das Leben wird gekappt.
|
||||||
func apply_auras() -> void:
|
func apply_auras() -> void:
|
||||||
for p in players:
|
for p in players:
|
||||||
for c in p.board:
|
for c in p.board:
|
||||||
c.aura = aura_for(c)
|
var a := aura_for(c)
|
||||||
|
if a != c.aura:
|
||||||
|
var delta: int = a - c.aura
|
||||||
|
c.max_health += delta
|
||||||
|
c.health = mini(c.health + maxi(delta, 0), c.max_health)
|
||||||
|
c.aura = a
|
||||||
|
|
||||||
|
|
||||||
## Vorschau fürs Ausspielen: der Elementbonus, den die Karte offen gelegt bekäme (sie zählt selbst mit).
|
## Vorschau fürs Ausspielen: der Elementbonus, den die Karte offen gelegt bekäme (sie zählt selbst mit).
|
||||||
func forecast(p: Player, card: Cards.Card) -> int:
|
func forecast(p: Player, card: Cards.Card) -> int:
|
||||||
var played := p.played.duplicate()
|
var played := p.played.duplicate()
|
||||||
for e in card.elements:
|
for e in card.elements:
|
||||||
played[e] = int(played.get(e, 0)) + card.cost
|
played[e] = int(played.get(e, 0)) + 1
|
||||||
return aura_of(card.elements, played) * (2 if "element_doppelt" in p.perks else 1)
|
return aura_of(card.elements, played) * (2 if "element_doppelt" in p.perks else 1)
|
||||||
|
|
||||||
|
|
||||||
@@ -685,20 +696,7 @@ func check_end() -> void:
|
|||||||
return
|
return
|
||||||
winner_player = alive[0]
|
winner_player = alive[0]
|
||||||
winner = "human" if winner_player.is_human else "opponents"
|
winner = "human" if winner_player.is_human else "opponents"
|
||||||
if humans.size() == 1:
|
say("Der Gegner ist besiegt!" if winner == "human" else "Du wurdest besiegt.")
|
||||||
say("Der Gegner ist besiegt!" if winner == "human" else "Du wurdest besiegt.")
|
|
||||||
else:
|
|
||||||
say("%s gewinnt!" % winner_player.name)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Zustand für das Netzwerk (siehe NetState) ------------------------------------
|
|
||||||
|
|
||||||
func snapshot(viewer: int) -> Dictionary:
|
|
||||||
return NetState.snapshot(self, viewer)
|
|
||||||
|
|
||||||
|
|
||||||
static func from_snapshot(d: Dictionary) -> Game:
|
|
||||||
return NetState.from_snapshot(d)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Gegner und Kampfstart ---------------------------------------------------
|
# --- Gegner und Kampfstart ---------------------------------------------------
|
||||||
@@ -711,7 +709,7 @@ static func shuffle(arr: Array, r: RandomNumberGenerator) -> void:
|
|||||||
arr[j] = tmp
|
arr[j] = tmp
|
||||||
|
|
||||||
|
|
||||||
## Duell gegen einen zufälligen Gegner. rules: {"human_perks": [...], "opponent_life": int, "looks": {...}}
|
## Duell gegen einen zufälligen Gegner. rules: {"human_perks": [...], "opponent_life": int, "looks": {...}, "deck": [ids]}
|
||||||
static func new_battle(r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game:
|
static func new_battle(r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game:
|
||||||
if r == null:
|
if r == null:
|
||||||
r = make_rng()
|
r = make_rng()
|
||||||
@@ -724,6 +722,7 @@ static func new_battle_with(opp: Player, r: RandomNumberGenerator = null, fixed_
|
|||||||
var human := Player.new("Spieler", true, START_LIFE)
|
var human := Player.new("Spieler", true, START_LIFE)
|
||||||
human.perks = Array(rules.get("human_perks", []))
|
human.perks = Array(rules.get("human_perks", []))
|
||||||
human.looks = rules.get("looks", {})
|
human.looks = rules.get("looks", {})
|
||||||
|
human.deck = rules.get("deck", []).duplicate()
|
||||||
var g := Game.new(human, opp, r)
|
var g := Game.new(human, opp, r)
|
||||||
for p in g.players:
|
for p in g.players:
|
||||||
if "leben_35" in p.perks:
|
if "leben_35" in p.perks:
|
||||||
|
|||||||
443
scripts/net.gd
443
scripts/net.gd
@@ -1,443 +0,0 @@
|
|||||||
class_name Net
|
|
||||||
extends Node
|
|
||||||
## LAN-Mehrspieler: der Host führt das Spiel, Mitspieler senden Aktionen und erhalten
|
|
||||||
## Zustandsbilder aus ihrer Sicht (fremde Hände bleiben verborgen).
|
|
||||||
|
|
||||||
signal lobby_changed
|
|
||||||
signal started(seat: int)
|
|
||||||
signal snapshot_received(snap: Dictionary)
|
|
||||||
signal action_applied
|
|
||||||
signal ended(reason: String)
|
|
||||||
signal migrated # nach einem Host-Ausfall: dieser Rechner führt das Spiel jetzt
|
|
||||||
signal status_changed
|
|
||||||
|
|
||||||
const PORT := 24455
|
|
||||||
|
|
||||||
var peer: ENetMultiplayerPeer
|
|
||||||
var is_host := false
|
|
||||||
var connected := false
|
|
||||||
var my_name := "Spieler"
|
|
||||||
var my_pick := {"looks": {}} # eigene Kartenoptik (Seltenheit), die der Gegner sieht
|
|
||||||
var names := {} # peer_id -> Name (nur im Host vollständig, sonst Kopie)
|
|
||||||
var picks := {} # peer_id -> {"looks": {...}} (Host)
|
|
||||||
var seats := {} # peer_id -> Spielerindex (Host)
|
|
||||||
var my_seat := -1
|
|
||||||
var game: Game # Host
|
|
||||||
var status := ""
|
|
||||||
var waiting := {} # Name -> {"seat": int, "since": float}: Platz nach Verbindungsabbruch freihalten
|
|
||||||
var rejoin_seconds := 60.0
|
|
||||||
var spectator := false # eigene Rolle: nur zuschauen
|
|
||||||
var spectators := {} # Host: peer_id -> Name
|
|
||||||
var successor_ip := "" # Mitspieler: Adresse des Nachfolge-Hosts
|
|
||||||
var i_am_successor := false
|
|
||||||
var backup := {} # Nachfolger: voller Spielstand nach jeder Aktion
|
|
||||||
var backup_names := {} # Nachfolger: Platz -> Name aller Menschen
|
|
||||||
var last_winner := "" # Mitspieler: Sieger laut letztem Zustandsbild
|
|
||||||
var reconnecting := false
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
multiplayer.peer_connected.connect(_on_peer_connected)
|
|
||||||
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
|
||||||
multiplayer.connected_to_server.connect(_on_connected)
|
|
||||||
multiplayer.connection_failed.connect(func():
|
|
||||||
if not reconnecting:
|
|
||||||
_fail("Verbindung fehlgeschlagen"))
|
|
||||||
multiplayer.server_disconnected.connect(_on_server_lost)
|
|
||||||
|
|
||||||
|
|
||||||
func host(port: int = PORT) -> Error:
|
|
||||||
peer = ENetMultiplayerPeer.new()
|
|
||||||
var err := peer.create_server(port, 8) # ein Mitspieler, der Rest Zuschauer
|
|
||||||
if err != OK:
|
|
||||||
status = "Kann nicht hosten (Port %d belegt?)" % port
|
|
||||||
return err
|
|
||||||
multiplayer.multiplayer_peer = peer
|
|
||||||
is_host = true
|
|
||||||
connected = true
|
|
||||||
names = {1: my_name}
|
|
||||||
picks = {1: my_pick}
|
|
||||||
spectators = {}
|
|
||||||
status = "Warte auf Mitspieler …"
|
|
||||||
lobby_changed.emit()
|
|
||||||
return OK
|
|
||||||
|
|
||||||
|
|
||||||
func join(ip: String, port: int = PORT) -> Error:
|
|
||||||
peer = ENetMultiplayerPeer.new()
|
|
||||||
var err := peer.create_client(ip, port)
|
|
||||||
if err != OK:
|
|
||||||
status = "Adresse ungültig"
|
|
||||||
return err
|
|
||||||
multiplayer.multiplayer_peer = peer
|
|
||||||
is_host = false
|
|
||||||
status = "Verbinde mit %s …" % ip
|
|
||||||
return OK
|
|
||||||
|
|
||||||
|
|
||||||
func leave() -> void:
|
|
||||||
if multiplayer.multiplayer_peer != null:
|
|
||||||
multiplayer.multiplayer_peer.close()
|
|
||||||
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
|
||||||
peer = null
|
|
||||||
is_host = false
|
|
||||||
connected = false
|
|
||||||
names = {}
|
|
||||||
picks = {}
|
|
||||||
seats = {}
|
|
||||||
waiting = {}
|
|
||||||
my_seat = -1
|
|
||||||
game = null
|
|
||||||
status = ""
|
|
||||||
spectator = false
|
|
||||||
spectators = {}
|
|
||||||
successor_ip = ""
|
|
||||||
i_am_successor = false
|
|
||||||
backup = {}
|
|
||||||
backup_names = {}
|
|
||||||
last_winner = ""
|
|
||||||
|
|
||||||
|
|
||||||
func local_addresses() -> Array:
|
|
||||||
return Array(IP.get_local_addresses()).filter(func(a): return "." in a and not a.begins_with("127."))
|
|
||||||
|
|
||||||
|
|
||||||
func _fail(reason: String) -> void:
|
|
||||||
status = reason
|
|
||||||
ended.emit(reason)
|
|
||||||
leave()
|
|
||||||
|
|
||||||
|
|
||||||
func _on_connected() -> void:
|
|
||||||
connected = true
|
|
||||||
status = "Verbunden"
|
|
||||||
register.rpc_id(1, my_name, my_pick, spectator)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Host-Wechsel ---------------------------------------------------------------
|
|
||||||
# Der Host schickt dem Mitspieler mit der kleinsten Kennung nach jeder Aktion den vollen
|
|
||||||
# Spielstand und allen die Adresse dieses Nachfolgers. Fällt der Host aus, eröffnet der
|
|
||||||
# Nachfolger das Spiel neu, die anderen verbinden sich mit ihm und kehren auf ihre Plätze zurück.
|
|
||||||
|
|
||||||
func _successor_id() -> int:
|
|
||||||
var ids: Array = seats.keys().filter(func(id): return id != 1)
|
|
||||||
ids.sort()
|
|
||||||
return ids[0] if not ids.is_empty() else -1
|
|
||||||
|
|
||||||
|
|
||||||
func _update_successor() -> void:
|
|
||||||
if not is_host or game == null:
|
|
||||||
return
|
|
||||||
var succ := _successor_id()
|
|
||||||
var ip := ""
|
|
||||||
if succ > 0 and peer != null and peer.get_peer(succ) != null:
|
|
||||||
ip = peer.get_peer(succ).get_remote_address()
|
|
||||||
for id in seats:
|
|
||||||
if id != 1:
|
|
||||||
set_successor.rpc_id(id, ip, id == succ)
|
|
||||||
|
|
||||||
|
|
||||||
## Platz -> Name aller Menschen (auch der gerade fehlenden), für die Rückkehr nach dem Wechsel.
|
|
||||||
func _seat_names() -> Dictionary:
|
|
||||||
var out := {}
|
|
||||||
for id in seats:
|
|
||||||
out[seats[id]] = names.get(id, "")
|
|
||||||
for pname in waiting:
|
|
||||||
out[waiting[pname].seat] = pname
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "reliable")
|
|
||||||
func set_successor(ip: String, me: bool) -> void:
|
|
||||||
successor_ip = ip
|
|
||||||
i_am_successor = me
|
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "reliable")
|
|
||||||
func receive_backup(snap: Dictionary, seat_names: Dictionary) -> void:
|
|
||||||
backup = snap
|
|
||||||
backup_names = seat_names
|
|
||||||
|
|
||||||
|
|
||||||
func _on_server_lost() -> void:
|
|
||||||
if is_host:
|
|
||||||
return
|
|
||||||
if last_winner != "":
|
|
||||||
_fail("Host hat das Spiel beendet")
|
|
||||||
elif i_am_successor and not backup.is_empty():
|
|
||||||
_become_host()
|
|
||||||
elif successor_ip != "":
|
|
||||||
status = "Host weg, verbinde mit dem Nachfolger …"
|
|
||||||
status_changed.emit()
|
|
||||||
_reconnect(successor_ip)
|
|
||||||
else:
|
|
||||||
_fail("Host hat das Spiel beendet")
|
|
||||||
|
|
||||||
|
|
||||||
func _become_host() -> void:
|
|
||||||
var snap := backup
|
|
||||||
var seat_names := backup_names
|
|
||||||
var seat := my_seat
|
|
||||||
if multiplayer.multiplayer_peer != null:
|
|
||||||
multiplayer.multiplayer_peer.close()
|
|
||||||
var err := FAILED
|
|
||||||
for attempt in 5:
|
|
||||||
err = host()
|
|
||||||
if err == OK:
|
|
||||||
break
|
|
||||||
await get_tree().create_timer(1.0).timeout
|
|
||||||
if err != OK:
|
|
||||||
_fail("Host weg, Übernahme fehlgeschlagen")
|
|
||||||
return
|
|
||||||
game = Game.from_snapshot(snap)
|
|
||||||
seats = {1: seat}
|
|
||||||
my_seat = seat
|
|
||||||
waiting = {}
|
|
||||||
for s in seat_names:
|
|
||||||
if int(s) != seat:
|
|
||||||
waiting[seat_names[s]] = {"seat": int(s), "since": Time.get_unix_time_from_system()}
|
|
||||||
game.say("%s übernimmt als Host." % my_name)
|
|
||||||
status = "Du bist jetzt Host, die anderen verbinden neu …"
|
|
||||||
successor_ip = ""
|
|
||||||
i_am_successor = false
|
|
||||||
backup = {}
|
|
||||||
migrated.emit()
|
|
||||||
|
|
||||||
|
|
||||||
func _reconnect(ip: String) -> void:
|
|
||||||
reconnecting = true
|
|
||||||
await get_tree().create_timer(2.0).timeout
|
|
||||||
for attempt in 5:
|
|
||||||
if join(ip) == OK:
|
|
||||||
var t := 0.0
|
|
||||||
while not connected and t < 4.0 and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTING:
|
|
||||||
await get_tree().process_frame
|
|
||||||
t += get_process_delta_time()
|
|
||||||
if connected:
|
|
||||||
reconnecting = false
|
|
||||||
return
|
|
||||||
await get_tree().create_timer(1.0).timeout
|
|
||||||
reconnecting = false
|
|
||||||
_fail("Nachfolge-Host nicht erreichbar")
|
|
||||||
|
|
||||||
|
|
||||||
func _on_peer_connected(_id: int) -> void:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
func _on_peer_disconnected(id: int) -> void:
|
|
||||||
if not is_host:
|
|
||||||
return
|
|
||||||
var pname: String = names.get(id, "")
|
|
||||||
names.erase(id)
|
|
||||||
picks.erase(id)
|
|
||||||
spectators.erase(id)
|
|
||||||
if game != null and seats.has(id):
|
|
||||||
var p: Game.Player = game.players[seats[id]]
|
|
||||||
if game.winner == "":
|
|
||||||
waiting[pname] = {"seat": seats[id], "since": Time.get_unix_time_from_system()}
|
|
||||||
game.say("%s hat die Verbindung verloren, der Platz bleibt %d s frei." % [p.name, int(rejoin_seconds)])
|
|
||||||
seats.erase(id)
|
|
||||||
_update_successor()
|
|
||||||
action_applied.emit()
|
|
||||||
lobby_update.rpc(names, spectators)
|
|
||||||
lobby_changed.emit()
|
|
||||||
|
|
||||||
|
|
||||||
## Host, je Bild: wartende Spieler überspringen; nach Ablauf der Frist scheiden sie aus.
|
|
||||||
func tick() -> void:
|
|
||||||
if not is_host or game == null or game.winner != "":
|
|
||||||
return
|
|
||||||
var now := Time.get_unix_time_from_system()
|
|
||||||
for pname in waiting.keys():
|
|
||||||
var w: Dictionary = waiting[pname]
|
|
||||||
var p: Game.Player = game.players[w.seat]
|
|
||||||
if now - float(w.since) > rejoin_seconds:
|
|
||||||
waiting.erase(pname)
|
|
||||||
game.say("%s kehrt nicht zurück und scheidet aus." % p.name)
|
|
||||||
game.damage(p, 999)
|
|
||||||
game.cleanup()
|
|
||||||
game.check_end()
|
|
||||||
if game.winner == "" and game.current() == p:
|
|
||||||
game.end_turn() # sonst bliebe das Spiel beim Ausgeschiedenen hängen
|
|
||||||
action_applied.emit()
|
|
||||||
elif game.current() == p:
|
|
||||||
game.say("%s ist nicht da, der Zug wird übersprungen." % p.name)
|
|
||||||
game.end_turn()
|
|
||||||
action_applied.emit()
|
|
||||||
|
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
|
||||||
func register(name: String, pick: Dictionary, as_spectator: bool = false) -> void:
|
|
||||||
if not is_host:
|
|
||||||
return
|
|
||||||
var id := multiplayer.get_remote_sender_id()
|
|
||||||
if as_spectator:
|
|
||||||
spectators[id] = name
|
|
||||||
lobby_update.rpc(names, spectators)
|
|
||||||
lobby_changed.emit()
|
|
||||||
if game != null:
|
|
||||||
start_game.rpc_id(id, -1)
|
|
||||||
receive_snapshot.rpc_id(id, game.snapshot(-2))
|
|
||||||
return
|
|
||||||
if game != null:
|
|
||||||
# Laufendes Spiel: nur Rückkehr auf einen freigehaltenen Platz
|
|
||||||
if waiting.has(name):
|
|
||||||
seats[id] = waiting[name].seat
|
|
||||||
names[id] = name
|
|
||||||
waiting.erase(name)
|
|
||||||
game.say("%s ist zurück." % name)
|
|
||||||
start_game.rpc_id(id, seats[id])
|
|
||||||
receive_snapshot.rpc_id(id, game.snapshot(seats[id]))
|
|
||||||
_update_successor()
|
|
||||||
action_applied.emit()
|
|
||||||
else:
|
|
||||||
status = "%s wollte beitreten, das Spiel läuft schon" % name
|
|
||||||
return
|
|
||||||
names[id] = name
|
|
||||||
picks[id] = pick
|
|
||||||
lobby_update.rpc(names, spectators)
|
|
||||||
lobby_changed.emit()
|
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "reliable")
|
|
||||||
func lobby_update(n: Dictionary, sp: Dictionary = {}) -> void:
|
|
||||||
names = n
|
|
||||||
spectators = sp
|
|
||||||
lobby_changed.emit()
|
|
||||||
|
|
||||||
|
|
||||||
## Host: Duell mit dem ersten Mitspieler starten; allein gegen die KI.
|
|
||||||
func start_battle() -> void:
|
|
||||||
var ids: Array = names.keys()
|
|
||||||
ids.sort()
|
|
||||||
ids = ids.slice(0, 2)
|
|
||||||
var r := Game.make_rng()
|
|
||||||
var players := []
|
|
||||||
seats = {}
|
|
||||||
for id in ids:
|
|
||||||
var pick: Dictionary = picks.get(id, {})
|
|
||||||
var p := Game.Player.new(names[id], true, Game.START_LIFE)
|
|
||||||
for cid in pick.get("looks", {}):
|
|
||||||
if Cards.has_id(cid):
|
|
||||||
p.looks[cid] = int(pick.looks[cid])
|
|
||||||
players.append(p)
|
|
||||||
if players.size() < 2:
|
|
||||||
players.append(DeckGen.random_opponent())
|
|
||||||
game = Game.new(players[0], players[1], r)
|
|
||||||
game.start()
|
|
||||||
for id in ids:
|
|
||||||
seats[id] = game.players.find(players[ids.find(id)])
|
|
||||||
my_seat = seats[1]
|
|
||||||
for id in ids:
|
|
||||||
if id != 1:
|
|
||||||
start_game.rpc_id(id, seats[id])
|
|
||||||
for id in spectators:
|
|
||||||
start_game.rpc_id(id, -1)
|
|
||||||
_update_successor()
|
|
||||||
started.emit(my_seat)
|
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "reliable")
|
|
||||||
func start_game(seat: int) -> void:
|
|
||||||
my_seat = seat
|
|
||||||
started.emit(seat)
|
|
||||||
|
|
||||||
|
|
||||||
## Host: Zustand an alle senden (Mitspieler aus ihrer Sicht, Zuschauer ohne Verborgenes,
|
|
||||||
## der Nachfolger zusätzlich den vollen Spielstand).
|
|
||||||
func broadcast() -> void:
|
|
||||||
if not is_host or game == null:
|
|
||||||
return
|
|
||||||
for id in seats:
|
|
||||||
if id != 1:
|
|
||||||
receive_snapshot.rpc_id(id, game.snapshot(seats[id]))
|
|
||||||
for id in spectators:
|
|
||||||
receive_snapshot.rpc_id(id, game.snapshot(-2))
|
|
||||||
var succ := _successor_id()
|
|
||||||
if succ > 0:
|
|
||||||
receive_backup.rpc_id(succ, game.snapshot(-1), _seat_names())
|
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "reliable")
|
|
||||||
func receive_snapshot(snap: Dictionary) -> void:
|
|
||||||
last_winner = str(snap.get("winner", ""))
|
|
||||||
snapshot_received.emit(snap)
|
|
||||||
|
|
||||||
|
|
||||||
## Mitspieler: Aktion an den Host schicken.
|
|
||||||
func send_action(action: Dictionary) -> void:
|
|
||||||
if is_host:
|
|
||||||
_apply(1, action)
|
|
||||||
else:
|
|
||||||
request_action.rpc_id(1, action)
|
|
||||||
|
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
|
||||||
func request_action(action: Dictionary) -> void:
|
|
||||||
if not is_host:
|
|
||||||
return
|
|
||||||
_apply(multiplayer.get_remote_sender_id(), action)
|
|
||||||
|
|
||||||
|
|
||||||
func _apply(id: int, action: Dictionary) -> void:
|
|
||||||
if game == null or not seats.has(id):
|
|
||||||
return
|
|
||||||
var p: Game.Player = game.players[seats[id]]
|
|
||||||
if game.winner != "":
|
|
||||||
return
|
|
||||||
if game.current() != p:
|
|
||||||
return
|
|
||||||
match str(action.get("type", "")):
|
|
||||||
"play":
|
|
||||||
var idx := int(action.get("hand", -1))
|
|
||||||
if idx < 0 or idx >= p.hand.size():
|
|
||||||
return
|
|
||||||
game.play_card(p, p.hand[idx], bool(action.get("face_down", false)))
|
|
||||||
"reveal":
|
|
||||||
var cr := game.find_creature(int(action.get("uid", -1)))
|
|
||||||
if cr != null:
|
|
||||||
game.reveal(p, cr)
|
|
||||||
"discard":
|
|
||||||
var hi := int(action.get("hand", -1))
|
|
||||||
if hi >= 0 and hi < p.hand.size():
|
|
||||||
game.discard(p, p.hand[hi])
|
|
||||||
"attack":
|
|
||||||
var cr := game.find_creature(int(action.get("uid", -1)))
|
|
||||||
var t = resolve_ref(action.get("target"))
|
|
||||||
if cr != null and t != null:
|
|
||||||
game.attack(cr, t)
|
|
||||||
"draw":
|
|
||||||
game.draw(p, int(action.get("index", 0)))
|
|
||||||
"fuse": # zwei Wesen (uid, other) oder zwei Handkarten (hand, other_hand)
|
|
||||||
var a = game.find_creature(int(action.get("uid", -1)))
|
|
||||||
var b = game.find_creature(int(action.get("other", -1)))
|
|
||||||
var i := int(action.get("hand", -1))
|
|
||||||
var j := int(action.get("other_hand", -1))
|
|
||||||
if a == null and i >= 0 and j >= 0 and i < p.hand.size() and j < p.hand.size():
|
|
||||||
a = p.hand[i]
|
|
||||||
b = p.hand[j]
|
|
||||||
if a != null and b != null:
|
|
||||||
game.fuse(p, a, b)
|
|
||||||
"end":
|
|
||||||
game.end_turn()
|
|
||||||
_:
|
|
||||||
return
|
|
||||||
action_applied.emit()
|
|
||||||
|
|
||||||
|
|
||||||
func resolve_ref(ref):
|
|
||||||
if ref == null or not (ref is Dictionary):
|
|
||||||
return null
|
|
||||||
if ref.get("kind") == "creature":
|
|
||||||
return game.find_creature(int(ref.get("uid", -1)))
|
|
||||||
var i := int(ref.get("index", -1))
|
|
||||||
if i >= 0 and i < game.players.size():
|
|
||||||
return game.players[i]
|
|
||||||
return null
|
|
||||||
|
|
||||||
|
|
||||||
static func ref_dict(target) -> Dictionary:
|
|
||||||
if target is Game.Creature:
|
|
||||||
return {"kind": "creature", "uid": target.uid}
|
|
||||||
return {"kind": "player", "index": target.order}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://drhh23lblvq8o
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
class_name NetState
|
|
||||||
## Serialisierter Spielzustand für das Netzwerk (ausgelagert aus den Spielregeln).
|
|
||||||
|
|
||||||
|
|
||||||
## Karten reisen als ID; eigene Exemplare mit Schlüsselwörtern und verschmolzene als Ganzes.
|
|
||||||
static func card_ref(c: Cards.Card):
|
|
||||||
return c.id if Cards.has_id(c.id) and c.keywords.is_empty() and c.fusions == 0 else c.to_dict()
|
|
||||||
|
|
||||||
|
|
||||||
static func card_of(ref) -> Cards.Card:
|
|
||||||
return Cards.Card.from_dict(ref) if ref is Dictionary else Cards.by_id(ref).copy()
|
|
||||||
|
|
||||||
|
|
||||||
## Zustand aus Sicht eines Spielers: fremde Hand nur als Anzahl, fremde
|
|
||||||
## verdeckte Wesen ohne Karte und Werte. viewer -1: alles sichtbar (Host-Wechsel); -2: Zuschauer.
|
|
||||||
static func snapshot(g: Game, viewer: int) -> Dictionary:
|
|
||||||
var ps := []
|
|
||||||
for i in g.players.size():
|
|
||||||
var p: Game.Player = g.players[i]
|
|
||||||
var mine := i == viewer or viewer == -1
|
|
||||||
var board := []
|
|
||||||
for c in p.board:
|
|
||||||
var masked: bool = c.hidden and not mine
|
|
||||||
board.append({"uid": c.uid, "card": card_ref(Cards.unknown() if masked else c.card),
|
|
||||||
"attack": 0 if masked else c.attack, "health": 0 if masked else c.health,
|
|
||||||
"max_health": 0 if masked else c.max_health, "attacks_done": c.attacks_done,
|
|
||||||
"ready": c.ready, "hidden": c.hidden, "aura": c.aura})
|
|
||||||
ps.append({"name": p.name, "title": p.title, "is_human": p.is_human, "life": p.life, "max_life": p.max_life,
|
|
||||||
"energy": p.energy, "max_energy": p.max_energy,
|
|
||||||
"order": p.order, "perks": p.perks, "looks": p.looks, "turns": p.turns, "played": p.played,
|
|
||||||
"hand": p.hand.map(card_ref) if mine else p.hand.size(), "board": board,
|
|
||||||
"offer": p.offer.map(card_ref) if mine else [], "drew": p.drew})
|
|
||||||
var evs := []
|
|
||||||
for ev in g.events:
|
|
||||||
var e: Dictionary = ev.duplicate()
|
|
||||||
if e.has("card"):
|
|
||||||
e["card"] = card_ref(e.card)
|
|
||||||
evs.append(e)
|
|
||||||
return {"players": ps, "human": g.players.find(g.human), "turn": g.turn, "current_idx": g.current_idx,
|
|
||||||
"winner": g.winner, "winner_player": g.players.find(g.winner_player) if g.winner_player != null else -1,
|
|
||||||
"next_uid": g.next_uid, "deck_size": g.deck.size(),
|
|
||||||
"log": g.log.slice(maxi(0, g.log.size() - 80)), "events": evs}
|
|
||||||
|
|
||||||
|
|
||||||
## Baut einen Spielzustand aus einem Snapshot; verborgene Karten werden durch Platzhalter ersetzt.
|
|
||||||
static func from_snapshot(d: Dictionary) -> Game:
|
|
||||||
var placeholder: Cards.Card = Cards.all()[0]
|
|
||||||
var list := []
|
|
||||||
for pd in d.players:
|
|
||||||
var p := Game.Player.new(pd.name, pd.is_human, int(pd.life), pd.title)
|
|
||||||
p.max_life = int(pd.max_life)
|
|
||||||
p.energy = int(pd.energy)
|
|
||||||
p.max_energy = int(pd.max_energy)
|
|
||||||
p.order = int(pd.order)
|
|
||||||
p.perks = Array(pd.perks)
|
|
||||||
for cid in pd.get("looks", {}):
|
|
||||||
p.looks[cid] = int(pd.looks[cid])
|
|
||||||
p.turns = int(pd.turns)
|
|
||||||
for e in pd.get("played", {}):
|
|
||||||
p.played[e] = int(pd.played[e])
|
|
||||||
if pd.hand is Array:
|
|
||||||
for ref in pd.hand:
|
|
||||||
p.hand.append(card_of(ref))
|
|
||||||
else:
|
|
||||||
for i in int(pd.hand):
|
|
||||||
p.hand.append(placeholder)
|
|
||||||
for ref in pd.get("offer", []):
|
|
||||||
p.offer.append(card_of(ref))
|
|
||||||
p.drew = bool(pd.get("drew", false))
|
|
||||||
for cd in pd.board:
|
|
||||||
var card := card_of(cd.card)
|
|
||||||
var c := Game.Creature.new(card, p, int(cd.uid))
|
|
||||||
c.attack = int(cd.attack)
|
|
||||||
c.health = int(cd.health)
|
|
||||||
c.max_health = int(cd.max_health)
|
|
||||||
c.attacks_done = int(cd.get("attacks_done", 0))
|
|
||||||
c.ready = bool(cd.get("ready", false))
|
|
||||||
c.hidden = bool(cd.get("hidden", false))
|
|
||||||
c.aura = int(cd.get("aura", 0))
|
|
||||||
p.board.append(c)
|
|
||||||
list.append(p)
|
|
||||||
var hi := int(d.human)
|
|
||||||
var g := Game.new(list[hi], list[1 - hi], RandomNumberGenerator.new())
|
|
||||||
g.players = list
|
|
||||||
g.turn = int(d.turn)
|
|
||||||
g.current_idx = int(d.current_idx)
|
|
||||||
g.winner = d.winner
|
|
||||||
g.winner_player = list[int(d.winner_player)] if int(d.winner_player) >= 0 else null
|
|
||||||
g.next_uid = int(d.next_uid)
|
|
||||||
for i in int(d.get("deck_size", 0)):
|
|
||||||
g.deck.append(placeholder) # nur die Größe zählt; gezogen wird beim Host
|
|
||||||
for line in d.log:
|
|
||||||
g.log.append(str(line))
|
|
||||||
for ev in d.events:
|
|
||||||
var e: Dictionary = ev.duplicate()
|
|
||||||
if e.has("card"):
|
|
||||||
e["card"] = card_of(e.card)
|
|
||||||
for key in ["player", "uid", "amount"]:
|
|
||||||
if e.has(key):
|
|
||||||
e[key] = int(e[key])
|
|
||||||
for key in ["target", "attacker"]:
|
|
||||||
if e.has(key):
|
|
||||||
var r: Dictionary = e[key]
|
|
||||||
if r.has("uid"):
|
|
||||||
r["uid"] = int(r.uid)
|
|
||||||
if r.has("index"):
|
|
||||||
r["index"] = int(r.index)
|
|
||||||
g.events.append(e)
|
|
||||||
return g
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://c3ao1mpgbrter
|
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
class_name Profile
|
class_name Profile
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
## Spielstand: Münzen, Staub, Sammlung, Kampagne, Einstellungen.
|
## Spielstand: Münzen, Staub, Sammlung, Stapel, Kampagne, Einstellungen.
|
||||||
## Jede Karte gehört jedem von Anfang an. Gesammelt wird die Seltenheitsstufe, und die
|
## Man startet mit einem von vier Startdecks (je ein Element im Mittelpunkt); Booster und die
|
||||||
## ist reine Optik: sie ändert weder Werte noch Text einer Karte.
|
## Werkstatt bringen den Rest. Jede eigene Karte hat eine Seltenheitsstufe, die +1/+1 je Stufe
|
||||||
|
## gibt (Cards.Card.leveled); kommt eine seltenere Version, ersetzt sie die alte, die zu Staub wird.
|
||||||
|
|
||||||
const SAVE_VERSION := 6
|
const SAVE_VERSION := 7
|
||||||
|
const CRAFT_COST := 200 # Staub für eine neue Karte (Common)
|
||||||
const SAVE_PATH := "user://spielstand.json"
|
const SAVE_PATH := "user://spielstand.json"
|
||||||
const BOOSTER_COST := 100
|
const BOOSTER_COST := 100
|
||||||
const BOOSTER_SIZE := 5
|
const BOOSTER_SIZE := 5
|
||||||
const RARITY_WEIGHTS := {"Common": 50.0, "Rare": 25.0, "Super Rare": 15.0, "Ultra Rare": 8.0, "Ultimate Rare": 2.0}
|
const RARITY_WEIGHTS := {"Common": 45.0, "Rare": 25.0, "Super Rare": 15.0, "Ultra Rare": 10.0, "Ultimate Rare": 5.0}
|
||||||
const START_COINS := 150
|
const START_COINS := 150
|
||||||
const REWARD_WIN := 100
|
const REWARD_WIN := 100
|
||||||
const REWARD_LOSS := 50
|
const REWARD_LOSS := 50
|
||||||
@@ -24,19 +26,34 @@ const DUST_UPGRADE := {"Rare": 100, "Super Rare": 200, "Ultra Rare": 500, "Ultim
|
|||||||
|
|
||||||
var coins := 0
|
var coins := 0
|
||||||
var dust := 0
|
var dust := 0
|
||||||
var collection := {} # id -> Seltenheitsstufe (Index in Cards.RARITIES), fehlend = Common
|
var collection := {} # eigene Karten: id -> Seltenheitsstufe (Index in Cards.RARITIES)
|
||||||
|
var deck: Array = [] # Karten-IDs des eigenen Stapels (Game.DECK_MIN bis DECK_MAX)
|
||||||
var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen
|
var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen
|
||||||
var settings := {"volume": 0.0, "music": 0.0, "ai_speed": 1.0, "animations": true, "language": "de", "difficulty": "normal", "big_font": false}
|
var settings := {"volume": 0.0, "music": 0.0, "ai_speed": 1.0, "animations": true, "language": "de", "difficulty": "normal", "big_font": false}
|
||||||
var notices: Array[String] = [] # Meldungen für die Oberfläche (Kampagnenfortschritt)
|
var notices: Array[String] = [] # Meldungen für die Oberfläche (Kampagnenfortschritt)
|
||||||
var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1)
|
var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1)
|
||||||
|
|
||||||
|
|
||||||
|
## Neues Profil: noch ohne Karten, das Startdeck wählt der Spieler (choose_start).
|
||||||
static func starter() -> Profile:
|
static func starter() -> Profile:
|
||||||
var p := Profile.new()
|
var p := Profile.new()
|
||||||
p.coins = START_COINS
|
p.coins = START_COINS
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
## Hat der Spieler schon ein Startdeck gewählt?
|
||||||
|
func started() -> bool:
|
||||||
|
return not collection.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
## Das Startdeck eines Elements wird Sammlung und Stapel.
|
||||||
|
func choose_start(element: String) -> void:
|
||||||
|
collection = {}
|
||||||
|
deck = DeckGen.start_deck(element)
|
||||||
|
for id in deck:
|
||||||
|
collection[id] = 0
|
||||||
|
|
||||||
|
|
||||||
## Lädt den Spielstand; bei Schaden die Sicherung; sonst ein neues Profil.
|
## Lädt den Spielstand; bei Schaden die Sicherung; sonst ein neues Profil.
|
||||||
static func load_or_new(path: String = SAVE_PATH) -> Profile:
|
static func load_or_new(path: String = SAVE_PATH) -> Profile:
|
||||||
for candidate in [path, path + ".bak"]:
|
for candidate in [path, path + ".bak"]:
|
||||||
@@ -52,7 +69,7 @@ static func load_or_new(path: String = SAVE_PATH) -> Profile:
|
|||||||
|
|
||||||
|
|
||||||
## Liest jede Spielstand-Version; ältere Stände behalten Münzen, Staub, Kampagne und
|
## Liest jede Spielstand-Version; ältere Stände behalten Münzen, Staub, Kampagne und
|
||||||
## Einstellungen, die alte Sammlung (Anzahl je Karte) hat keine Entsprechung mehr.
|
## Einstellungen und bekommen die Startsammlung.
|
||||||
static func from_dict(d: Dictionary) -> Profile:
|
static func from_dict(d: Dictionary) -> Profile:
|
||||||
var p := Profile.new()
|
var p := Profile.new()
|
||||||
p.coins = int(d.get("coins", 0))
|
p.coins = int(d.get("coins", 0))
|
||||||
@@ -63,6 +80,11 @@ static func from_dict(d: Dictionary) -> Profile:
|
|||||||
for cid in coll:
|
for cid in coll:
|
||||||
if Cards.has_id(cid):
|
if Cards.has_id(cid):
|
||||||
p.collection[cid] = clampi(int(coll[cid]), 0, Cards.RARITIES.size() - 1)
|
p.collection[cid] = clampi(int(coll[cid]), 0, Cards.RARITIES.size() - 1)
|
||||||
|
var ids := []
|
||||||
|
for cid in d.get("deck", []):
|
||||||
|
if p.collection.has(cid) and not (cid in ids):
|
||||||
|
ids.append(cid)
|
||||||
|
p.deck = ids.slice(0, Game.DECK_MAX)
|
||||||
var se = d.get("settings", {})
|
var se = d.get("settings", {})
|
||||||
p.settings = {"volume": float(se.get("volume", 0.0)), "music": float(se.get("music", 0.0)),
|
p.settings = {"volume": float(se.get("volume", 0.0)), "music": float(se.get("music", 0.0)),
|
||||||
"ai_speed": float(se.get("ai_speed", 1.0)), "animations": bool(se.get("animations", true)),
|
"ai_speed": float(se.get("ai_speed", 1.0)), "animations": bool(se.get("animations", true)),
|
||||||
@@ -77,7 +99,7 @@ static func from_dict(d: Dictionary) -> Profile:
|
|||||||
|
|
||||||
|
|
||||||
func to_dict() -> Dictionary:
|
func to_dict() -> Dictionary:
|
||||||
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "collection": collection,
|
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "collection": collection, "deck": deck,
|
||||||
"campaign_stage": campaign_stage, "settings": settings, "perk": perk}
|
"campaign_stage": campaign_stage, "settings": settings, "perk": perk}
|
||||||
|
|
||||||
|
|
||||||
@@ -97,7 +119,11 @@ func take_notices() -> Array[String]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
# --- Sammlung: Seltenheit als Aussehen --------------------------------------------
|
# --- Sammlung und Stapel -----------------------------------------------------------
|
||||||
|
|
||||||
|
func owns(cid: String) -> bool:
|
||||||
|
return collection.has(cid)
|
||||||
|
|
||||||
|
|
||||||
func level(cid: String) -> int:
|
func level(cid: String) -> int:
|
||||||
return int(collection.get(cid, 0))
|
return int(collection.get(cid, 0))
|
||||||
@@ -116,17 +142,29 @@ func looks() -> Dictionary:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
## Anzahl Karten über Common.
|
## Der Stapel für den Kampf: der gewählte, mit weiteren eigenen Karten bis DECK_MIN aufgefüllt,
|
||||||
func upgraded_count() -> int:
|
## falls er zu klein ist (Karten können durch Zerlegen wegfallen).
|
||||||
var n := 0
|
func deck_cards() -> Array:
|
||||||
for cid in collection:
|
return DeckGen.fill(deck.filter(func(id): return collection.has(id)), collection.keys(), Game.DECK_MIN)
|
||||||
if int(collection[cid]) > 0:
|
|
||||||
n += 1
|
|
||||||
return n
|
## Karte in den Stapel oder heraus. Gibt "" zurück oder eine Fehlermeldung.
|
||||||
|
func toggle_deck(cid: String) -> String:
|
||||||
|
if cid in deck:
|
||||||
|
deck.erase(cid)
|
||||||
|
return ""
|
||||||
|
if not owns(cid):
|
||||||
|
return "Karte nicht in der Sammlung"
|
||||||
|
if deck.size() >= Game.DECK_MAX:
|
||||||
|
return "Der Stapel ist voll (%d Karten)" % Game.DECK_MAX
|
||||||
|
deck.append(cid)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
## Wertet eine Karte um eine Stufe auf. Gibt "" zurück oder eine Fehlermeldung.
|
## Wertet eine Karte um eine Stufe auf. Gibt "" zurück oder eine Fehlermeldung.
|
||||||
func upgrade(cid: String) -> String:
|
func upgrade(cid: String) -> String:
|
||||||
|
if not owns(cid):
|
||||||
|
return "Karte nicht in der Sammlung"
|
||||||
var lvl := level(cid)
|
var lvl := level(cid)
|
||||||
if lvl >= Cards.RARITIES.size() - 1:
|
if lvl >= Cards.RARITIES.size() - 1:
|
||||||
return "Schon auf der höchsten Stufe"
|
return "Schon auf der höchsten Stufe"
|
||||||
@@ -138,13 +176,29 @@ func upgrade(cid: String) -> String:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
## Gibt eine Stufe ab und bekommt Staub dafür. Gibt "" zurück oder eine Fehlermeldung.
|
## Entzaubern: eine Stufe abgeben bringt Staub; eine Common-Karte verlässt die Sammlung.
|
||||||
|
## Gibt "" zurück oder eine Fehlermeldung.
|
||||||
func dismantle(cid: String) -> String:
|
func dismantle(cid: String) -> String:
|
||||||
|
if not owns(cid):
|
||||||
|
return "Karte nicht in der Sammlung"
|
||||||
var lvl := level(cid)
|
var lvl := level(cid)
|
||||||
if lvl == 0:
|
|
||||||
return "Common-Karten lassen sich nicht zerlegen"
|
|
||||||
dust += DUST_DISMANTLE[Cards.RARITIES[lvl]]
|
dust += DUST_DISMANTLE[Cards.RARITIES[lvl]]
|
||||||
collection[cid] = lvl - 1
|
if lvl == 0:
|
||||||
|
collection.erase(cid)
|
||||||
|
deck.erase(cid)
|
||||||
|
else:
|
||||||
|
collection[cid] = lvl - 1
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
## Herstellen: eine fehlende Karte für Staub als Common. Gibt "" zurück oder eine Fehlermeldung.
|
||||||
|
func craft(cid: String) -> String:
|
||||||
|
if owns(cid):
|
||||||
|
return "Karte schon in der Sammlung"
|
||||||
|
if dust < CRAFT_COST:
|
||||||
|
return "Nicht genug Staub (%d nötig)" % CRAFT_COST
|
||||||
|
dust -= CRAFT_COST
|
||||||
|
collection[cid] = 0
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@@ -164,8 +218,9 @@ static func roll_booster(r: RandomNumberGenerator) -> Array:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
## Öffnet einen Booster. Höhere Stufen werten die Karte auf, alles andere wird zu Staub.
|
## Öffnet einen Booster. Neue Karten kommen in Sammlung und Stapel (solange Platz ist). Von jeder
|
||||||
## Gibt die Ergebnisse zurück, leer wenn die Münzen nicht reichen.
|
## Karte zählt die seltenste Version: eine höhere ersetzt die alte, die zu Staub wird; eine gleiche
|
||||||
|
## oder niedrigere wird selbst zu Staub. Gibt die Ergebnisse zurück, leer wenn die Münzen nicht reichen.
|
||||||
func open_booster(r: RandomNumberGenerator = null) -> Array:
|
func open_booster(r: RandomNumberGenerator = null) -> Array:
|
||||||
if coins < BOOSTER_COST:
|
if coins < BOOSTER_COST:
|
||||||
return []
|
return []
|
||||||
@@ -175,14 +230,20 @@ func open_booster(r: RandomNumberGenerator = null) -> Array:
|
|||||||
var out := roll_booster(r)
|
var out := roll_booster(r)
|
||||||
for res in out:
|
for res in out:
|
||||||
var lvl: int = Cards.RARITIES.find(res.rarity)
|
var lvl: int = Cards.RARITIES.find(res.rarity)
|
||||||
if lvl > level(res.card.id):
|
res["new"] = not owns(res.card.id)
|
||||||
|
res["upgraded"] = false
|
||||||
|
res["dust"] = 0
|
||||||
|
if res.new:
|
||||||
collection[res.card.id] = lvl
|
collection[res.card.id] = lvl
|
||||||
|
if deck.size() < Game.DECK_MAX:
|
||||||
|
deck.append(res.card.id)
|
||||||
|
elif lvl > level(res.card.id):
|
||||||
res["upgraded"] = true
|
res["upgraded"] = true
|
||||||
res["dust"] = 0
|
res["dust"] = DUST_DISMANTLE[rarity(res.card.id)]
|
||||||
|
collection[res.card.id] = lvl
|
||||||
else:
|
else:
|
||||||
res["upgraded"] = false
|
|
||||||
res["dust"] = DUST_DISMANTLE[res.rarity]
|
res["dust"] = DUST_DISMANTLE[res.rarity]
|
||||||
dust += res.dust
|
dust += res.dust
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func _next_zone(step: int) -> void:
|
|||||||
func items() -> Array:
|
func items() -> Array:
|
||||||
match zone:
|
match zone:
|
||||||
"deck":
|
"deck":
|
||||||
return screen.offer_views
|
return [screen.deck] if screen.deck.visible else []
|
||||||
"hand":
|
"hand":
|
||||||
return screen.hand_views
|
return screen.hand_views
|
||||||
"own":
|
"own":
|
||||||
@@ -105,7 +105,7 @@ func confirm() -> void:
|
|||||||
var item = items[index]
|
var item = items[index]
|
||||||
match zone:
|
match zone:
|
||||||
"deck":
|
"deck":
|
||||||
screen.on_click("deck", index)
|
screen.on_click("deck", null)
|
||||||
"hand":
|
"hand":
|
||||||
screen.on_click("hand", item.card)
|
screen.on_click("hand", item.card)
|
||||||
"own":
|
"own":
|
||||||
|
|||||||
@@ -1,31 +1,29 @@
|
|||||||
class_name BattleLayout
|
class_name BattleLayout
|
||||||
## Feste Bildschirmaufteilung des Duells (1280x800). Mitte von oben nach unten: Hand des Gegners
|
## Feste Bildschirmaufteilung des Duells (1280x800). Mitte von oben nach unten: Hand des Gegners
|
||||||
## (Rückseiten), sein Brett, seine Elementleiste, die eigene Elementleiste (die Naht liegt auf halber
|
## (Rückseiten), sein Brett, eigenes Brett, eigene Hand. Links: Kasten des Gegners, daneben die
|
||||||
## Höhe), eigenes Brett, eigene Hand. Links: Kasten des Gegners, die drei offenen Karten des
|
## Elementzähler beider Seiten je neben ihrem Brett, Zusatzzeile und Meldungen. Rechts von oben:
|
||||||
## Stapels (mittig), eigener Kasten. Rechts oben die Knöpfe, rechts mittig die Vorschau, darunter Meldungen.
|
## Stapel und Kasten des Gegners, die Vorschau, eigener Kasten und Stapel. Optionen per Esc.
|
||||||
|
|
||||||
const B_GIVEUP := Rect2(1070, 10, 96, 32)
|
const B_UNDO := Rect2(20, 10, 210, 32) # links oben: Rückgängig, darunter Überspringen (Gegnerzug)
|
||||||
const B_LOG := Rect2(1172, 10, 96, 32)
|
const B_SKIP := Rect2(20, 48, 210, 32)
|
||||||
const B_END := Rect2(1070, 48, 198, 32)
|
const OPP_DECK := Rect2(1090, 10, 130, 182) # Stapel des Gegners rechts oben
|
||||||
const OPP_BOX := Rect2(20, 11, 210, 96)
|
const OPP_BOX := Rect2(1050, 200, 210, 70) # Kasten des Gegners darunter
|
||||||
const OPP_HAND_Y := 11
|
const OPP_HAND_Y := 11
|
||||||
const FIELD_OPP := Rect2(240, 364, 800, 32)
|
const OPP_ELEMENTS := Rect2(20, 220, 210, 128) # Elementzähler des Gegners, links von seinem Brett
|
||||||
const FIELD := Rect2(240, 404, 800, 32)
|
const OWN_ELEMENTS := Rect2(20, 440, 210, 128) # eigene Elementzähler, links vom eigenen Brett
|
||||||
const MSG := Rect2(1040, 630, 220, 60)
|
const INFO := Rect2(20, 580, 210, 60) # Zusatzzeile zur Vorschau
|
||||||
const STATUS := Rect2(20, 694, 210, 96)
|
const MSG := Rect2(20, 650, 210, 80) # Meldungen
|
||||||
const B_UNDO := Rect2(1070, 86, 198, 32)
|
const STATUS := Rect2(1050, 526, 210, 70) # eigener Kasten rechts über dem Stapel
|
||||||
|
const DECK := Rect2(1090, 606, 130, 182) # eigener Stapel rechts unten
|
||||||
|
const OPTIONS := Rect2(440, 180, 400, 420) # Optionsmenü (Esc)
|
||||||
const OWN_X := 345
|
const OWN_X := 345
|
||||||
const HAND_W := 110 # Handkarten so groß wie die Wesen auf dem Feld
|
const HAND_W := 110 # Handkarten so groß wie die Wesen auf dem Feld
|
||||||
const HAND_H := 140
|
const HAND_H := 140
|
||||||
const HAND_GAP := 10
|
const HAND_GAP := 10
|
||||||
const HAND_Y := 610
|
const HAND_Y := 610
|
||||||
const HAND_ARC := 3.0 # Grad Neigung und Pixel Absenkung je Platz von der Mitte (leichter Bogen)
|
const HAND_ARC := 3.0 # Grad Neigung und Pixel Absenkung je Platz von der Mitte (leichter Bogen)
|
||||||
const OFFER_X := 70 # die drei offenen Karten des Stapels, links übereinander um die Mitte
|
|
||||||
const OFFER_Y := 180
|
|
||||||
const OFFER_GAP := 10
|
|
||||||
const DECK_LABEL := Rect2(20, 632, 210, 22)
|
|
||||||
const LOG_PANEL := Rect2(240, 60, 800, 560)
|
const LOG_PANEL := Rect2(240, 60, 800, 560)
|
||||||
const PREVIEW_SIZE := Vector2(180, 252)
|
const PREVIEW_SIZE := Vector2(170, 238)
|
||||||
const CENTER_X := 240
|
const CENTER_X := 240
|
||||||
const CENTER_W := 800
|
const CENTER_W := 800
|
||||||
|
|
||||||
@@ -61,11 +59,11 @@ static func hand_arc(count: int, i: int, mirrored: bool = false) -> Array:
|
|||||||
return [t * HAND_ARC * sign, t * t * HAND_ARC * sign]
|
return [t * HAND_ARC * sign, t * t * HAND_ARC * sign]
|
||||||
|
|
||||||
|
|
||||||
## Vorschau der gezeigten Karte: fest rechts auf halber Höhe.
|
## Vorschau der gezeigten Karte: fest rechts zwischen den beiden Kästen.
|
||||||
static func preview_rect(_anchor: Rect2) -> Rect2:
|
static func preview_rect(_anchor: Rect2) -> Rect2:
|
||||||
return Rect2(Vector2(1050, 400 - PREVIEW_SIZE.y / 2), PREVIEW_SIZE)
|
return Rect2(Vector2(1070, 278), PREVIEW_SIZE)
|
||||||
|
|
||||||
|
|
||||||
## Zusatzzeile (Elementbonus, Eigenschaften) unter der Vorschau.
|
## Zusatzzeile (Elementbonus, Eigenschaften) links unter den Elementzählern.
|
||||||
static func info_rect(preview: Rect2) -> Rect2:
|
static func info_rect(_preview: Rect2) -> Rect2:
|
||||||
return Rect2(1030, preview.end.y + 8, 230, 80)
|
return INFO
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
class_name BattleScreen
|
class_name BattleScreen
|
||||||
extends Control
|
extends Control
|
||||||
## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx.
|
## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx.
|
||||||
|
## Esc öffnet die Optionen (Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden); den Zug beendet
|
||||||
|
## die Leertaste oder der Rechtsklick ins Leere.
|
||||||
## Mittlere Maustaste auf eine Handkarte legt sie verdeckt. Rechtsklick auf eine Handkarte oder
|
## Mittlere Maustaste auf eine Handkarte legt sie verdeckt. Rechtsklick auf eine Handkarte oder
|
||||||
## ein eigenes Wesen öffnet das Kartenmenü: verdeckt legen, aufdecken, Eigenschaften kaufen.
|
## ein eigenes Wesen öffnet das Kartenmenü: verdeckt legen, aufdecken, Eigenschaften kaufen.
|
||||||
## Verschmelzen: eine Karte auf eine andere ziehen und dort eine Sekunde stillhalten. Mit
|
## Verschmelzen: eine Karte auf eine andere ziehen und dort eine Sekunde stillhalten. Mit
|
||||||
## Gamepad öffnet Y das Menü, dort zusätzlich mit „Verschmelzen mit …“.
|
## Gamepad öffnet Y das Menü, dort zusätzlich mit „Verschmelzen mit …“.
|
||||||
## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer).
|
## Modi: Einzelspieler und Kampagne.
|
||||||
|
|
||||||
var app
|
var app
|
||||||
var stage := -1 # Kampagnenstufe oder -1
|
var stage := -1 # Kampagnenstufe oder -1
|
||||||
var net_play := false # Netzwerkspiel (auch als Zuschauer)
|
|
||||||
var online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1
|
|
||||||
var net: Net
|
|
||||||
var game: Game
|
var game: Game
|
||||||
var perspective: Game.Player # der Mensch, dessen Hand unten liegt
|
var perspective: Game.Player # der Mensch, dessen Hand unten liegt
|
||||||
var fx: BattleFx
|
var fx: BattleFx
|
||||||
@@ -60,12 +59,10 @@ var flash_until := {} # Spielerindex -> Zeitpunkt (ms)
|
|||||||
var hero_layer: Control
|
var hero_layer: Control
|
||||||
var tile_layer: Control
|
var tile_layer: Control
|
||||||
var hand_layer: Control
|
var hand_layer: Control
|
||||||
var offer_views: Array = [] # die offenen Karten des Stapels (CardView)
|
var deck: Deck
|
||||||
var offer_layer: Control
|
var opp_deck: Deck # Stapel des Gegners, nur zum Ansehen
|
||||||
var deck_label: Label
|
|
||||||
var draw_enabled := false
|
|
||||||
var log_overlay: Control
|
var log_overlay: Control
|
||||||
var b_log: Button
|
var options: Control # Optionsmenü (Esc): Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden
|
||||||
var status_panel: Panel
|
var status_panel: Panel
|
||||||
var fx_layer: Control
|
var fx_layer: Control
|
||||||
var log_box: RichTextLabel
|
var log_box: RichTextLabel
|
||||||
@@ -74,8 +71,6 @@ var name_label: Label
|
|||||||
var life_label: Label
|
var life_label: Label
|
||||||
var energy_label: Label
|
var energy_label: Label
|
||||||
var msg_label: Label
|
var msg_label: Label
|
||||||
var b_end: Button
|
|
||||||
var b_giveup: Button
|
|
||||||
var element_view: ElementView # eigene Elementleiste
|
var element_view: ElementView # eigene Elementleiste
|
||||||
var opp_element_view: ElementView # die des Gegners darüber
|
var opp_element_view: ElementView # die des Gegners darüber
|
||||||
var info_label: Label # Vorschau: Elementbonus, Merkmale
|
var info_label: Label # Vorschau: Elementbonus, Merkmale
|
||||||
@@ -89,7 +84,6 @@ var reward_label: Label
|
|||||||
var notice_label: Label
|
var notice_label: Label
|
||||||
var confirm: Control
|
var confirm: Control
|
||||||
var cursor_layer: Control
|
var cursor_layer: Control
|
||||||
var wait_label: Label
|
|
||||||
var b_skip: Button
|
var b_skip: Button
|
||||||
var b_undo: Button
|
var b_undo: Button
|
||||||
|
|
||||||
@@ -98,30 +92,6 @@ func _init(campaign_stage: int = -1) -> void:
|
|||||||
stage = campaign_stage
|
stage = campaign_stage
|
||||||
|
|
||||||
|
|
||||||
static func online(seat: int) -> BattleScreen:
|
|
||||||
var b := BattleScreen.new(-1)
|
|
||||||
b.net_play = true
|
|
||||||
b.online_seat = seat
|
|
||||||
return b
|
|
||||||
|
|
||||||
|
|
||||||
func is_online() -> bool:
|
|
||||||
return net_play
|
|
||||||
|
|
||||||
|
|
||||||
## Zuschauer im Netzwerk: kein eigener Platz, keine Hand.
|
|
||||||
func spectating() -> bool:
|
|
||||||
return is_online() and online_seat < 0
|
|
||||||
|
|
||||||
|
|
||||||
func is_client() -> bool:
|
|
||||||
return is_online() and not net.is_host
|
|
||||||
|
|
||||||
|
|
||||||
func counts_for_profile() -> bool:
|
|
||||||
return not is_online()
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
var s: Dictionary = app.profile.settings
|
var s: Dictionary = app.profile.settings
|
||||||
fx = BattleFx.new(self)
|
fx = BattleFx.new(self)
|
||||||
@@ -129,31 +99,19 @@ func _ready() -> void:
|
|||||||
pad = BattleGamepad.new(self)
|
pad = BattleGamepad.new(self)
|
||||||
ai_delay = 0.75 / float(s.ai_speed)
|
ai_delay = 0.75 / float(s.ai_speed)
|
||||||
_build_static()
|
_build_static()
|
||||||
if is_online():
|
if stage >= 0:
|
||||||
net = app.net
|
|
||||||
net.ended.connect(_on_net_ended)
|
|
||||||
net.migrated.connect(_on_migrated)
|
|
||||||
net.status_changed.connect(_on_net_status)
|
|
||||||
if net.is_host:
|
|
||||||
game = net.game
|
|
||||||
net.action_applied.connect(after_action)
|
|
||||||
else:
|
|
||||||
net.snapshot_received.connect(_on_snapshot)
|
|
||||||
wait_label.visible = true
|
|
||||||
return # der erste Zustand kommt vom Host
|
|
||||||
elif stage >= 0:
|
|
||||||
var r := Game.make_rng()
|
var r := Game.make_rng()
|
||||||
game = Game.new_battle_with(Campaign.opponent(stage), r, false, own_rules())
|
game = Game.new_battle_with(Campaign.opponent(stage, r), r, false, own_rules())
|
||||||
else:
|
else:
|
||||||
game = Game.new_battle(Game.make_rng(), own_rules())
|
game = Game.new_battle(Game.make_rng(), own_rules())
|
||||||
smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile()
|
smart_ai = bool(app.profile.difficulty().smart)
|
||||||
_begin()
|
_begin()
|
||||||
|
|
||||||
|
|
||||||
## Kartenoptik, Gegnerleben (Schwierigkeit) und das eigene Merkmal (nach Kapitel 1).
|
## Eigener Stapel und Kartenstufen, Gegnerleben (Schwierigkeit) und das eigene Merkmal (nach Kapitel 1).
|
||||||
func own_rules() -> Dictionary:
|
func own_rules() -> Dictionary:
|
||||||
var p: Profile = app.profile
|
var p: Profile = app.profile
|
||||||
var rules := {"looks": p.looks()}
|
var rules := {"looks": p.looks(), "deck": p.deck_cards()}
|
||||||
if stage < 0:
|
if stage < 0:
|
||||||
rules["opponent_life"] = int(p.difficulty().life)
|
rules["opponent_life"] = int(p.difficulty().life)
|
||||||
if p.perk != "" and p.perk_unlocked():
|
if p.perk != "" and p.perk_unlocked():
|
||||||
@@ -162,37 +120,22 @@ func own_rules() -> Dictionary:
|
|||||||
|
|
||||||
|
|
||||||
func _begin() -> void:
|
func _begin() -> void:
|
||||||
if is_online():
|
game.events.clear()
|
||||||
perspective = game.players[maxi(online_seat, 0)]
|
perspective = game.human
|
||||||
else:
|
|
||||||
game.events.clear()
|
|
||||||
perspective = game.current() if game.current().is_human else game.humans[0]
|
|
||||||
build_hero_boxes()
|
build_hero_boxes()
|
||||||
if is_online() and net.is_host:
|
|
||||||
net.broadcast()
|
|
||||||
game.events.clear()
|
|
||||||
refresh()
|
refresh()
|
||||||
announce_turn()
|
announce_turn()
|
||||||
|
|
||||||
|
|
||||||
func _build_static() -> void:
|
func _build_static() -> void:
|
||||||
var L := BattleLayout
|
var L := BattleLayout
|
||||||
# oben links: Aufgeben und Verlauf, klein
|
b_skip = Style.button(self, "Überspringen", L.B_SKIP, func(): skip_ai = true)
|
||||||
b_giveup = Style.button(self, "Aufgeben", L.B_GIVEUP, func(): confirm.visible = true)
|
b_skip.add_theme_font_size_override("font_size", 13)
|
||||||
b_giveup.add_theme_font_size_override("font_size", 13)
|
|
||||||
b_log = Style.button(self, "Verlauf", L.B_LOG, toggle_log)
|
|
||||||
b_log.add_theme_font_size_override("font_size", 13)
|
|
||||||
b_end = Style.button(self, "Zug beenden", L.B_END, end_turn, Style.PRIMARY)
|
|
||||||
b_end.add_theme_font_size_override("font_size", 15)
|
|
||||||
b_skip = Style.button(self, "Überspringen", L.B_END, func(): skip_ai = true)
|
|
||||||
b_skip.add_theme_font_size_override("font_size", 15)
|
|
||||||
b_skip.visible = false
|
b_skip.visible = false
|
||||||
# Mitte: Elementzähler, Meldung, eigenes Brett mit fünf Plätzen, eigene Leiste links daneben
|
# Elementzähler beider Seiten links neben ihrem Brett
|
||||||
Style.panel(self, L.FIELD_OPP, Style.darker(Style.PANEL, 0.9), 8)
|
opp_element_view = ElementView.new(L.OPP_ELEMENTS)
|
||||||
opp_element_view = ElementView.new(L.FIELD_OPP)
|
|
||||||
add_child(opp_element_view)
|
add_child(opp_element_view)
|
||||||
Style.panel(self, L.FIELD, Style.darker(Style.PANEL, 0.9), 8)
|
element_view = ElementView.new(L.OWN_ELEMENTS)
|
||||||
element_view = ElementView.new(L.FIELD)
|
|
||||||
add_child(element_view)
|
add_child(element_view)
|
||||||
msg_label = Style.label(self, "", 14, Style.ORANGE, L.MSG, "center", true)
|
msg_label = Style.label(self, "", 14, Style.ORANGE, L.MSG, "center", true)
|
||||||
msg_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
msg_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
@@ -207,18 +150,21 @@ func _build_static() -> void:
|
|||||||
var sx: float = L.STATUS.position.x
|
var sx: float = L.STATUS.position.x
|
||||||
var sy: float = L.STATUS.position.y
|
var sy: float = L.STATUS.position.y
|
||||||
var sw: float = L.STATUS.size.x
|
var sw: float = L.STATUS.size.x
|
||||||
name_label = Style.label(self, "", 18, Style.TEXT, Rect2(sx + 8, sy + 6, sw - 16, 26), "center", true)
|
name_label = Style.label(self, "", 16, Style.TEXT, Rect2(sx + 8, sy + 4, sw - 16, 22), "center", true)
|
||||||
name_label.clip_text = true
|
name_label.clip_text = true
|
||||||
life_label = Style.label(self, "", 30, Style.RED, Rect2(sx + 8, sy + 42, sw / 2 - 8, 40), "left", true)
|
life_label = Style.label(self, "", 26, Style.RED, Rect2(sx + 10, sy + 30, sw / 2 - 10, 34), "left", true)
|
||||||
energy_label = Style.label(self, "", 30, Style.BLUE, Rect2(sx + sw / 2, sy + 42, sw / 2 - 8, 40), "right", true)
|
energy_label = Style.label(self, "", 26, Style.BLUE, Rect2(sx + sw / 2, sy + 30, sw / 2 - 10, 34), "right", true)
|
||||||
b_undo = Style.button(self, "Rückgängig", L.B_UNDO, undo)
|
b_undo = Style.button(self, "Rückgängig", L.B_UNDO, undo)
|
||||||
b_undo.add_theme_font_size_override("font_size", 13)
|
b_undo.add_theme_font_size_override("font_size", 13)
|
||||||
b_undo.visible = false
|
b_undo.visible = false
|
||||||
deck_label = Style.label(self, "", 13, Style.MUTED, L.DECK_LABEL, "center")
|
deck = Deck.new(L.DECK)
|
||||||
wait_label = Style.label(self, "Warte auf den Host …", 22, Style.MUTED, Rect2(0, 300, 1280, 40), "center")
|
deck.clicked.connect(func(): on_click("deck", null))
|
||||||
wait_label.visible = false
|
add_child(deck)
|
||||||
|
opp_deck = Deck.new(L.OPP_DECK)
|
||||||
|
opp_deck.interactive = false
|
||||||
|
opp_deck.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(opp_deck)
|
||||||
hero_layer = _layer()
|
hero_layer = _layer()
|
||||||
offer_layer = _layer()
|
|
||||||
tile_layer = _layer()
|
tile_layer = _layer()
|
||||||
hand_layer = _layer()
|
hand_layer = _layer()
|
||||||
preview_holder = _layer()
|
preview_holder = _layer()
|
||||||
@@ -252,6 +198,28 @@ func _build_static() -> void:
|
|||||||
log_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
log_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
log_overlay.add_child(log_box)
|
log_overlay.add_child(log_box)
|
||||||
Style.button(log_overlay, "Schließen", Rect2(L.LOG_PANEL.end.x - 180, L.LOG_PANEL.end.y - 52, 160, 40), func(): log_overlay.visible = false)
|
Style.button(log_overlay, "Schließen", Rect2(L.LOG_PANEL.end.x - 180, L.LOG_PANEL.end.y - 52, 160, 40), func(): log_overlay.visible = false)
|
||||||
|
# Optionsmenü (Esc)
|
||||||
|
options = Style.overlay(self)
|
||||||
|
options.get_child(0).gui_input.connect(func(ev):
|
||||||
|
if ev is InputEventMouseButton and ev.pressed:
|
||||||
|
options.visible = false)
|
||||||
|
Style.panel(options, L.OPTIONS, Style.PANEL, 10, Style.GOLD, 2)
|
||||||
|
Style.label(options, "Optionen", 24, Style.GOLD, Rect2(L.OPTIONS.position.x, L.OPTIONS.position.y + 18, L.OPTIONS.size.x, 34), "center", true)
|
||||||
|
var oy := L.OPTIONS.position.y + 76
|
||||||
|
for entry in [["Verlauf", func():
|
||||||
|
options.visible = false
|
||||||
|
log_overlay.visible = true],
|
||||||
|
["Aufgeben", func():
|
||||||
|
options.visible = false
|
||||||
|
confirm.visible = true],
|
||||||
|
["Einstellungen", func():
|
||||||
|
options.visible = false
|
||||||
|
app.show_screen(SettingsScreen.new(), true)],
|
||||||
|
["Hauptmenü", leave],
|
||||||
|
["Beenden", func(): get_tree().quit()]]:
|
||||||
|
var ob := Style.button(options, entry[0], Rect2(L.OPTIONS.position.x + 60, oy, L.OPTIONS.size.x - 120, 48), entry[1])
|
||||||
|
ob.add_theme_font_size_override("font_size", 18)
|
||||||
|
oy += 60
|
||||||
# Spielende
|
# Spielende
|
||||||
overlay = _overlay()
|
overlay = _overlay()
|
||||||
result_label = Style.label(overlay, "", 56, Style.GREEN, Rect2(0, 250, 1280, 80), "center", true)
|
result_label = Style.label(overlay, "", 56, Style.GREEN, Rect2(0, 250, 1280, 80), "center", true)
|
||||||
@@ -261,8 +229,8 @@ func _build_static() -> void:
|
|||||||
confirm = Style.confirm(self, "Wirklich aufgeben?", "", "Ja, aufgeben", leave, "Weiterspielen")
|
confirm = Style.confirm(self, "Wirklich aufgeben?", "", "Ja, aufgeben", leave, "Weiterspielen")
|
||||||
|
|
||||||
|
|
||||||
func toggle_log() -> void:
|
func toggle_options() -> void:
|
||||||
log_overlay.visible = not log_overlay.visible
|
options.visible = not options.visible
|
||||||
|
|
||||||
|
|
||||||
func _layer() -> Control:
|
func _layer() -> Control:
|
||||||
@@ -277,8 +245,6 @@ func _overlay() -> Control:
|
|||||||
|
|
||||||
|
|
||||||
func leave() -> void:
|
func leave() -> void:
|
||||||
if is_online():
|
|
||||||
net.leave()
|
|
||||||
app.show_screen(MenuScreen.new())
|
app.show_screen(MenuScreen.new())
|
||||||
|
|
||||||
|
|
||||||
@@ -336,24 +302,22 @@ func build_hero_boxes() -> void:
|
|||||||
var title: String = ("%s · %s" % [o.name, o.title]) if o.title != "" else o.name
|
var title: String = ("%s · %s" % [o.name, o.title]) if o.title != "" else o.name
|
||||||
if not o.perks.is_empty():
|
if not o.perks.is_empty():
|
||||||
title += " ★"
|
title += " ★"
|
||||||
labels["name"] = Style.label(hero_layer, title, 18, Style.TEXT, Rect2(box.position.x + 8, box.position.y + 6, box.size.x - 16, 26), "center", true)
|
labels["name"] = Style.label(hero_layer, title, 16, Style.TEXT, Rect2(box.position.x + 8, box.position.y + 4, box.size.x - 16, 22), "center", true)
|
||||||
labels.name.clip_text = true
|
labels.name.clip_text = true
|
||||||
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
labels["life"] = Style.label(hero_layer, "", 30, Style.RED, Rect2(box.position.x + 8, box.position.y + 42, box.size.x / 2 - 8, 40), "left", true)
|
labels["life"] = Style.label(hero_layer, "", 26, Style.RED, Rect2(box.position.x + 10, box.position.y + 30, box.size.x / 2 - 10, 34), "left", true)
|
||||||
labels["stats"] = Style.label(hero_layer, "", 30, Style.BLUE, Rect2(box.position.x + box.size.x / 2, box.position.y + 42, box.size.x / 2 - 8, 40), "right", true)
|
labels["stats"] = Style.label(hero_layer, "", 26, Style.BLUE, Rect2(box.position.x + box.size.x / 2, box.position.y + 30, box.size.x / 2 - 10, 34), "right", true)
|
||||||
hero_labels[o] = labels
|
hero_labels[o] = labels
|
||||||
|
|
||||||
|
|
||||||
# --- Ablauf und Eingabe ------------------------------------------------------------------
|
# --- Ablauf und Eingabe ------------------------------------------------------------------
|
||||||
|
|
||||||
func my_turn() -> bool:
|
func my_turn() -> bool:
|
||||||
if spectating():
|
|
||||||
return false
|
|
||||||
return game != null and game.winner == "" and game.current() == perspective
|
return game != null and game.winner == "" and game.current() == perspective
|
||||||
|
|
||||||
|
|
||||||
func any_overlay() -> bool:
|
func any_overlay() -> bool:
|
||||||
return confirm.visible or overlay.visible or log_overlay.visible or menu_layer.visible
|
return confirm.visible or overlay.visible or log_overlay.visible or menu_layer.visible or options.visible
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
@@ -400,10 +364,12 @@ func _input(event: InputEvent) -> void:
|
|||||||
close_menu()
|
close_menu()
|
||||||
elif log_overlay.visible:
|
elif log_overlay.visible:
|
||||||
log_overlay.visible = false
|
log_overlay.visible = false
|
||||||
|
elif options.visible:
|
||||||
|
options.visible = false
|
||||||
elif selected != null or fuse_first != null:
|
elif selected != null or fuse_first != null:
|
||||||
cancel_all()
|
cancel_all()
|
||||||
elif not any_overlay():
|
elif not any_overlay():
|
||||||
confirm.visible = true
|
options.visible = true
|
||||||
KEY_SPACE:
|
KEY_SPACE:
|
||||||
if my_turn() and not any_overlay():
|
if my_turn() and not any_overlay():
|
||||||
end_turn()
|
end_turn()
|
||||||
@@ -451,8 +417,6 @@ func _process(delta: float) -> void:
|
|||||||
_end_drag()
|
_end_drag()
|
||||||
do_fuse(a, b)
|
do_fuse(a, b)
|
||||||
after_action()
|
after_action()
|
||||||
if is_online() and net.is_host:
|
|
||||||
net.tick()
|
|
||||||
if game.winner != "":
|
if game.winner != "":
|
||||||
if not ended:
|
if not ended:
|
||||||
ended = true
|
ended = true
|
||||||
@@ -461,8 +425,6 @@ func _process(delta: float) -> void:
|
|||||||
var cur := game.current()
|
var cur := game.current()
|
||||||
if cur.is_human:
|
if cur.is_human:
|
||||||
return
|
return
|
||||||
if is_client():
|
|
||||||
return
|
|
||||||
if skip_ai:
|
if skip_ai:
|
||||||
ai_timer = 0.0
|
ai_timer = 0.0
|
||||||
else:
|
else:
|
||||||
@@ -506,15 +468,10 @@ func _exit_tree() -> void:
|
|||||||
|
|
||||||
func _finish() -> void:
|
func _finish() -> void:
|
||||||
var won := game.winner == "human"
|
var won := game.winner == "human"
|
||||||
if counts_for_profile():
|
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
|
||||||
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
|
reward = app.profile.record_battle(won, stage, factor)
|
||||||
reward = app.profile.record_battle(won, stage, factor)
|
app.save()
|
||||||
app.save()
|
app.sfx.play("win" if won else "lose")
|
||||||
app.sfx.play("win" if won else "lose")
|
|
||||||
else:
|
|
||||||
var wp := game.winner_player
|
|
||||||
var mine: bool = wp != null and wp == perspective
|
|
||||||
app.sfx.play("win" if mine else "lose")
|
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
@@ -631,10 +588,7 @@ func fusion_partners(target) -> Array:
|
|||||||
|
|
||||||
|
|
||||||
func play(card: Cards.Card, face_down: bool = false) -> void:
|
func play(card: Cards.Card, face_down: bool = false) -> void:
|
||||||
if is_client():
|
var before: Game = game.clone()
|
||||||
net.send_action({"type": "play", "hand": perspective.hand.find(card), "face_down": face_down})
|
|
||||||
return
|
|
||||||
var before: Game = game.clone() if not is_online() else null
|
|
||||||
var log_len := game.log.size()
|
var log_len := game.log.size()
|
||||||
var from := _hand_rect_of(card)
|
var from := _hand_rect_of(card)
|
||||||
var err := game.play_card(perspective, card, face_down)
|
var err := game.play_card(perspective, card, face_down)
|
||||||
@@ -655,9 +609,6 @@ func play(card: Cards.Card, face_down: bool = false) -> void:
|
|||||||
## Deckt ein eigenes verdecktes Wesen auf (kostenlos).
|
## Deckt ein eigenes verdecktes Wesen auf (kostenlos).
|
||||||
func do_reveal(cr: Game.Creature) -> void:
|
func do_reveal(cr: Game.Creature) -> void:
|
||||||
clear_undo()
|
clear_undo()
|
||||||
if is_client():
|
|
||||||
net.send_action({"type": "reveal", "uid": cr.uid})
|
|
||||||
return
|
|
||||||
var err := game.reveal(perspective, cr)
|
var err := game.reveal(perspective, cr)
|
||||||
if err != "":
|
if err != "":
|
||||||
say(tr(err))
|
say(tr(err))
|
||||||
@@ -667,7 +618,7 @@ func do_reveal(cr: Game.Creature) -> void:
|
|||||||
## Kartenmenü neben einer Handkarte oder einem eigenen Wesen. full: auch verschmelzen anbieten
|
## Kartenmenü neben einer Handkarte oder einem eigenen Wesen. full: auch verschmelzen anbieten
|
||||||
## (Gamepad, das nicht ziehen kann).
|
## (Gamepad, das nicht ziehen kann).
|
||||||
func open_menu(target, anchor: Rect2, full: bool = false) -> void:
|
func open_menu(target, anchor: Rect2, full: bool = false) -> void:
|
||||||
if spectating() or not my_turn():
|
if not my_turn():
|
||||||
return
|
return
|
||||||
cancel()
|
cancel()
|
||||||
Style.clear(menu_layer)
|
Style.clear(menu_layer)
|
||||||
@@ -685,7 +636,7 @@ func open_menu(target, anchor: Rect2, full: bool = false) -> void:
|
|||||||
entries.append([tr("Aufdecken"), func(): do_reveal(target), game.can_reveal(perspective, target)[0]])
|
entries.append([tr("Aufdecken"), func(): do_reveal(target), game.can_reveal(perspective, target)[0]])
|
||||||
if full:
|
if full:
|
||||||
var partners := fusion_partners(target)
|
var partners := fusion_partners(target)
|
||||||
entries.append([tr("Verschmelzen mit … ◆ %d") % card.fusion_cost(), func():
|
entries.append([tr("Verschmelzen mit …"), func():
|
||||||
fuse_first = target
|
fuse_first = target
|
||||||
say(tr("Ziel wählen"))
|
say(tr("Ziel wählen"))
|
||||||
refresh(), not partners.is_empty()])
|
refresh(), not partners.is_empty()])
|
||||||
@@ -729,22 +680,16 @@ func close_menu() -> void:
|
|||||||
## Abwerfen: eine Handkarte gegen Mana loswerden.
|
## Abwerfen: eine Handkarte gegen Mana loswerden.
|
||||||
func do_discard(card: Cards.Card) -> void:
|
func do_discard(card: Cards.Card) -> void:
|
||||||
clear_undo()
|
clear_undo()
|
||||||
if is_client():
|
|
||||||
net.send_action({"type": "discard", "hand": perspective.hand.find(card)})
|
|
||||||
return
|
|
||||||
var err := game.discard(perspective, card)
|
var err := game.discard(perspective, card)
|
||||||
if err != "":
|
if err != "":
|
||||||
say(tr(err))
|
say(tr(err))
|
||||||
after_action()
|
after_action()
|
||||||
|
|
||||||
|
|
||||||
## Ziehen: kostet Mana, eine Karte kommt auf die Hand.
|
## Ziehen: eine Karte vom eigenen Stapel, die erste im Zug frei.
|
||||||
func do_draw(index: int = 0) -> void:
|
func do_draw() -> void:
|
||||||
clear_undo()
|
clear_undo()
|
||||||
if is_client():
|
var err := game.draw(perspective)
|
||||||
net.send_action({"type": "draw", "index": index})
|
|
||||||
return
|
|
||||||
var err := game.draw(perspective, index)
|
|
||||||
if err != "":
|
if err != "":
|
||||||
say(tr(err))
|
say(tr(err))
|
||||||
else:
|
else:
|
||||||
@@ -755,12 +700,6 @@ func do_draw(index: int = 0) -> void:
|
|||||||
func do_fuse(a, b) -> void:
|
func do_fuse(a, b) -> void:
|
||||||
cancel()
|
cancel()
|
||||||
clear_undo()
|
clear_undo()
|
||||||
if is_client():
|
|
||||||
if a is Cards.Card:
|
|
||||||
net.send_action({"type": "fuse", "hand": perspective.hand.find(a), "other_hand": perspective.hand.find(b)})
|
|
||||||
else:
|
|
||||||
net.send_action({"type": "fuse", "uid": a.uid, "other": b.uid})
|
|
||||||
return
|
|
||||||
var err := game.fuse(perspective, a, b)
|
var err := game.fuse(perspective, a, b)
|
||||||
if err != "":
|
if err != "":
|
||||||
say(tr(err))
|
say(tr(err))
|
||||||
@@ -784,8 +723,6 @@ func undo() -> void:
|
|||||||
restored.say("Rückgängig.")
|
restored.say("Rückgängig.")
|
||||||
game = restored
|
game = restored
|
||||||
perspective = game.players[idx]
|
perspective = game.players[idx]
|
||||||
if is_online() and net.is_host:
|
|
||||||
net.game = game
|
|
||||||
clear_undo()
|
clear_undo()
|
||||||
cancel()
|
cancel()
|
||||||
build_hero_boxes()
|
build_hero_boxes()
|
||||||
@@ -797,9 +734,6 @@ func end_turn() -> void:
|
|||||||
return
|
return
|
||||||
cancel()
|
cancel()
|
||||||
clear_undo()
|
clear_undo()
|
||||||
if is_client():
|
|
||||||
net.send_action({"type": "end"})
|
|
||||||
return
|
|
||||||
game.end_turn()
|
game.end_turn()
|
||||||
ai_timer = ai_delay
|
ai_timer = ai_delay
|
||||||
after_action()
|
after_action()
|
||||||
@@ -823,17 +757,14 @@ func on_click(kind: String, obj) -> void:
|
|||||||
selected = null
|
selected = null
|
||||||
if kind == "enemy" or kind == "hero":
|
if kind == "enemy" or kind == "hero":
|
||||||
clear_undo()
|
clear_undo()
|
||||||
if is_client():
|
var err := g.attack(cr, obj)
|
||||||
net.send_action({"type": "attack", "uid": cr.uid, "target": Net.ref_dict(obj)})
|
if err != "":
|
||||||
else:
|
say(tr(err))
|
||||||
var err := g.attack(cr, obj)
|
|
||||||
if err != "":
|
|
||||||
say(tr(err))
|
|
||||||
after_action()
|
after_action()
|
||||||
return
|
return
|
||||||
match kind:
|
match kind:
|
||||||
"deck":
|
"deck":
|
||||||
do_draw(int(obj) if obj != null else 0)
|
do_draw()
|
||||||
"hand":
|
"hand":
|
||||||
var ok := g.can_play(perspective, obj)
|
var ok := g.can_play(perspective, obj)
|
||||||
if not ok[0]:
|
if not ok[0]:
|
||||||
@@ -949,6 +880,41 @@ func update_attack_preview() -> void:
|
|||||||
v.queue_redraw()
|
v.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
## Der eigene Stapel: Kartenstoß mit Kosten des nächsten Ziehens und Restzahl.
|
||||||
|
class Deck extends Control:
|
||||||
|
signal clicked
|
||||||
|
var enabled := true
|
||||||
|
var interactive := true # false: der Stapel des Gegners, nur Anzahl
|
||||||
|
var count := 0
|
||||||
|
var cost := 0
|
||||||
|
|
||||||
|
func _init(rect: Rect2) -> void:
|
||||||
|
position = rect.position
|
||||||
|
size = rect.size
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
|
||||||
|
func _gui_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
accept_event()
|
||||||
|
clicked.emit()
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
for k in range(2, -1, -1):
|
||||||
|
var r := Rect2(Vector2(k * 3, -k * 3), size - Vector2(6, 12))
|
||||||
|
r.position.y += 6
|
||||||
|
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD if enabled and interactive else Style.MUTED, 2), r)
|
||||||
|
if k == 0:
|
||||||
|
Style.border(self, r.grow(-12), Color("5a3c82"), 2, 6)
|
||||||
|
if interactive:
|
||||||
|
Style.text(self, tr("Ziehen"), 16, Style.TEXT if enabled else Style.MUTED, r.get_center() + Vector2(0, -12), "center", true)
|
||||||
|
Style.text(self, "◆ %d" % cost, 20, Style.BLUE if enabled else Style.MUTED, r.get_center() + Vector2(0, 14), "center", true)
|
||||||
|
Style.text(self, str(count), 12, Style.MUTED, Vector2(r.get_center().x, r.end.y - 18), "center")
|
||||||
|
else:
|
||||||
|
Style.text(self, str(count), 22, Style.TEXT, r.get_center(), "center", true)
|
||||||
|
if interactive and not enabled:
|
||||||
|
draw_rect(Rect2(Vector2.ZERO, size), Color(0, 0, 0, 0.35))
|
||||||
|
|
||||||
|
|
||||||
## Eine verdeckte Handkarte des Gegners in voller Größe.
|
## Eine verdeckte Handkarte des Gegners in voller Größe.
|
||||||
class CardBack extends Control:
|
class CardBack extends Control:
|
||||||
func _init(rect: Rect2) -> void:
|
func _init(rect: Rect2) -> void:
|
||||||
@@ -967,51 +933,10 @@ class CardBack extends Control:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
# --- Netzwerk -------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func _on_snapshot(snap: Dictionary) -> void:
|
|
||||||
var first := game == null
|
|
||||||
game = Game.from_snapshot(snap)
|
|
||||||
wait_label.visible = false
|
|
||||||
if first:
|
|
||||||
_begin()
|
|
||||||
return
|
|
||||||
perspective = game.players[maxi(online_seat, 0)]
|
|
||||||
build_hero_boxes()
|
|
||||||
after_action()
|
|
||||||
|
|
||||||
|
|
||||||
## Host-Wechsel: das Spiel läuft jetzt auf diesem Rechner weiter.
|
|
||||||
func _on_migrated() -> void:
|
|
||||||
game = net.game
|
|
||||||
if not net.action_applied.is_connected(after_action):
|
|
||||||
net.action_applied.connect(after_action)
|
|
||||||
wait_label.visible = false
|
|
||||||
perspective = game.players[online_seat]
|
|
||||||
build_hero_boxes()
|
|
||||||
say("Du bist jetzt Host.")
|
|
||||||
after_action()
|
|
||||||
|
|
||||||
|
|
||||||
func _on_net_status() -> void:
|
|
||||||
wait_label.text = net.status
|
|
||||||
wait_label.visible = true
|
|
||||||
|
|
||||||
|
|
||||||
func _on_net_ended(reason: String) -> void:
|
|
||||||
if game == null or game.winner == "":
|
|
||||||
say(reason)
|
|
||||||
await get_tree().create_timer(1.5).timeout
|
|
||||||
if is_inside_tree():
|
|
||||||
app.show_screen(MenuScreen.new())
|
|
||||||
|
|
||||||
|
|
||||||
# --- Ereignisse ----------------------------------------------------------------------------
|
# --- Ereignisse ----------------------------------------------------------------------------
|
||||||
|
|
||||||
## Nach jeder Aktion: Ereignisse sichern, Ansicht angleichen, Animationen abspielen.
|
## Nach jeder Aktion: Ereignisse sichern, Ansicht angleichen, Animationen abspielen.
|
||||||
func after_action() -> void:
|
func after_action() -> void:
|
||||||
if is_online() and net.is_host:
|
|
||||||
net.broadcast()
|
|
||||||
var evs := game.events.duplicate()
|
var evs := game.events.duplicate()
|
||||||
game.events.clear()
|
game.events.clear()
|
||||||
var centers := {}
|
var centers := {}
|
||||||
@@ -1138,23 +1063,12 @@ func refresh() -> void:
|
|||||||
tiles.erase(uid)
|
tiles.erase(uid)
|
||||||
fx.fade_out(t)
|
fx.fade_out(t)
|
||||||
update_attack_preview()
|
update_attack_preview()
|
||||||
Style.clear(offer_layer)
|
deck.enabled = mine and g.can_draw(h)[0]
|
||||||
offer_views = []
|
deck.count = h.deck.size()
|
||||||
draw_enabled = mine and g.can_draw(h)[0]
|
deck.cost = g.draw_cost(h)
|
||||||
if not spectating():
|
deck.queue_redraw()
|
||||||
for i in h.offer.size():
|
opp_deck.count = opponent().deck.size()
|
||||||
var oc: Cards.Card = h.offer[i]
|
opp_deck.queue_redraw()
|
||||||
var orect := Rect2(BattleLayout.OFFER_X, BattleLayout.OFFER_Y + i * (BattleLayout.HAND_H + BattleLayout.OFFER_GAP), BattleLayout.HAND_W, BattleLayout.HAND_H)
|
|
||||||
var ov := CardView.new(oc, orect.size, false, oc.rarity_in(h.looks))
|
|
||||||
ov.position = orect.position
|
|
||||||
ov.dim = not draw_enabled
|
|
||||||
var idx := i
|
|
||||||
ov.clicked.connect(func(_v): on_click("deck", idx))
|
|
||||||
ov.hover.connect(func(c, e): show_preview(c, e, h.looks, "", orect))
|
|
||||||
offer_layer.add_child(ov)
|
|
||||||
offer_views.append(ov)
|
|
||||||
deck_label.text = "%s ◆ %d · %d" % [tr("Ziehen"), g.draw_cost(h), g.deck.size()]
|
|
||||||
deck_label.visible = not spectating()
|
|
||||||
|
|
||||||
Style.clear(hand_layer)
|
Style.clear(hand_layer)
|
||||||
for o in others(): # die Hand des Gegners oben, nur Rückseiten, spiegelverkehrter Bogen
|
for o in others(): # die Hand des Gegners oben, nur Rückseiten, spiegelverkehrter Bogen
|
||||||
@@ -1165,9 +1079,7 @@ func refresh() -> void:
|
|||||||
back.rotation_degrees = arc[0]
|
back.rotation_degrees = arc[0]
|
||||||
back.position.y += arc[1]
|
back.position.y += arc[1]
|
||||||
hand_layer.add_child(back)
|
hand_layer.add_child(back)
|
||||||
if spectating():
|
if true:
|
||||||
Style.label(hand_layer, "Zuschauer: Hände sind verdeckt", 18, Style.MUTED, Rect2(240, BattleLayout.HAND_Y + 18, 800, 30), "center")
|
|
||||||
else:
|
|
||||||
for i in h.hand.size():
|
for i in h.hand.size():
|
||||||
var card: Cards.Card = h.hand[i]
|
var card: Cards.Card = h.hand[i]
|
||||||
var playable: bool = mine and g.can_play(h, card)[0]
|
var playable: bool = mine and g.can_play(h, card)[0]
|
||||||
@@ -1199,10 +1111,8 @@ func refresh() -> void:
|
|||||||
name_label.text = h.name
|
name_label.text = h.name
|
||||||
life_label.text = "♥ %d" % maxi(h.life, 0)
|
life_label.text = "♥ %d" % maxi(h.life, 0)
|
||||||
energy_label.text = "◆ %d" % h.energy
|
energy_label.text = "◆ %d" % h.energy
|
||||||
b_end.disabled = not mine
|
var ai_running: bool = g.winner == "" and not g.current().is_human
|
||||||
var ai_running: bool = g.winner == "" and not g.current().is_human and not is_client()
|
|
||||||
b_skip.visible = ai_running
|
b_skip.visible = ai_running
|
||||||
b_end.visible = not ai_running and not spectating()
|
|
||||||
b_undo.visible = undo_state != null and mine
|
b_undo.visible = undo_state != null and mine
|
||||||
element_view.update_from(g, h)
|
element_view.update_from(g, h)
|
||||||
opp_element_view.update_from(g, opponent())
|
opp_element_view.update_from(g, opponent())
|
||||||
@@ -1222,20 +1132,13 @@ func refresh() -> void:
|
|||||||
pad.draw_cursor()
|
pad.draw_cursor()
|
||||||
|
|
||||||
if g.winner != "":
|
if g.winner != "":
|
||||||
var wp := g.winner_player
|
var won := g.winner == "human"
|
||||||
if counts_for_profile():
|
result_label.text = "Sieg!" if won else "Niederlage"
|
||||||
var won := g.winner == "human"
|
result_label.add_theme_color_override("font_color", Style.GREEN if won else Style.RED)
|
||||||
result_label.text = "Sieg!" if won else "Niederlage"
|
reward_label.text = "+%d Münzen" % maxi(reward, 0)
|
||||||
result_label.add_theme_color_override("font_color", Style.GREEN if won else Style.RED)
|
var notes: Array[String] = app.profile.take_notices()
|
||||||
reward_label.text = "+%d Münzen" % maxi(reward, 0)
|
if not notes.is_empty():
|
||||||
var notes: Array[String] = app.profile.take_notices()
|
notice_label.text = "\n".join(notes)
|
||||||
if not notes.is_empty():
|
|
||||||
notice_label.text = "\n".join(notes)
|
|
||||||
else:
|
|
||||||
var mine_win: bool = wp != null and wp == perspective
|
|
||||||
result_label.text = ("%s gewinnt!" % wp.name) if wp != null and wp.is_human else "Die KI gewinnt"
|
|
||||||
result_label.add_theme_color_override("font_color", Style.GREEN if mine_win else Style.RED)
|
|
||||||
reward_label.text = "Gemeinsame Partie, keine Münzen"
|
|
||||||
overlay.visible = true
|
overlay.visible = true
|
||||||
confirm.visible = false
|
confirm.visible = false
|
||||||
|
|
||||||
|
|||||||
@@ -137,13 +137,15 @@ func refresh() -> void:
|
|||||||
elif revealed.has(i):
|
elif revealed.has(i):
|
||||||
var res: Dictionary = cards[i]
|
var res: Dictionary = cards[i]
|
||||||
var c: Cards.Card = res.card
|
var c: Cards.Card = res.card
|
||||||
var v := CardView.new(c, r.size, false, res.rarity)
|
var v := CardView.new(c.leveled(Cards.RARITIES.find(res.rarity)), r.size, false, res.rarity)
|
||||||
v.position = r.position
|
v.position = r.position
|
||||||
slots.add_child(v)
|
slots.add_child(v)
|
||||||
if fresh.has(i):
|
if fresh.has(i):
|
||||||
flip(v, fresh[i], res.rarity)
|
flip(v, fresh[i], res.rarity)
|
||||||
Style.label(slots, tr(res.rarity), 16, Cards.RARITY_COLORS[res.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true)
|
Style.label(slots, tr(res.rarity), 16, Cards.RARITY_COLORS[res.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true)
|
||||||
if res.upgraded:
|
if res.get("new", false):
|
||||||
|
Style.label(slots, "Neu!", 16, Style.GOLD, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
|
||||||
|
elif res.upgraded:
|
||||||
Style.label(slots, "Aufgewertet", 16, Style.GREEN, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
|
Style.label(slots, "Aufgewertet", 16, Style.GREEN, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
|
||||||
else:
|
else:
|
||||||
Style.label(slots, "+%d Staub" % res.dust, 14, Style.MUTED, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center")
|
Style.label(slots, "+%d Staub" % res.dust, 14, Style.MUTED, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class_name CollectionScreen
|
class_name CollectionScreen
|
||||||
extends Control
|
extends Control
|
||||||
## Sammlung: alle Karten gehören dir. Gesammelt wird die Seltenheitsstufe — reine Optik.
|
## Sammlung: alle Karten des Spiels, die fehlenden abgedunkelt. Eigene Karten lassen sich
|
||||||
## Aufwerten kostet Staub, eine Stufe abgeben bringt Staub.
|
## aufwerten (Staub) oder zerlegen (bringt Staub), fehlende herstellen (Staub).
|
||||||
|
|
||||||
var app
|
var app
|
||||||
var filter := ""
|
var filter := ""
|
||||||
@@ -15,6 +15,7 @@ var preview_holder: Control
|
|||||||
var info_label: Label
|
var info_label: Label
|
||||||
var b_dismantle: Button
|
var b_dismantle: Button
|
||||||
var b_upgrade: Button
|
var b_upgrade: Button
|
||||||
|
var b_craft: Button
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -43,6 +44,7 @@ func _ready() -> void:
|
|||||||
info_label = Style.label(self, "", 15, Style.MUTED, Rect2(840, 476, 380, 30), "center")
|
info_label = Style.label(self, "", 15, Style.MUTED, Rect2(840, 476, 380, 30), "center")
|
||||||
b_upgrade = Style.button(self, "", Rect2(840, 530, 380, 46), upgrade, Style.PRIMARY)
|
b_upgrade = Style.button(self, "", Rect2(840, 530, 380, 46), upgrade, Style.PRIMARY)
|
||||||
b_dismantle = Style.button(self, "", Rect2(840, 586, 380, 46), dismantle, Color("5a3c28"))
|
b_dismantle = Style.button(self, "", Rect2(840, 586, 380, 46), dismantle, Color("5a3c28"))
|
||||||
|
b_craft = Style.button(self, "", Rect2(840, 530, 380, 46), craft, Style.PRIMARY)
|
||||||
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(840, 650, 380, 48), "center")
|
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(840, 650, 380, 48), "center")
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
@@ -57,7 +59,7 @@ func set_filter(e: String) -> void:
|
|||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
## Aufgewertete Karten zuerst, sonst in Kartenreihenfolge.
|
## Eigene Karten zuerst (höhere Stufen vorn), dann die fehlenden, sonst in Kartenreihenfolge.
|
||||||
func cards() -> Array:
|
func cards() -> Array:
|
||||||
var out: Array = Cards.all().duplicate()
|
var out: Array = Cards.all().duplicate()
|
||||||
if filter != "":
|
if filter != "":
|
||||||
@@ -69,7 +71,10 @@ func cards() -> Array:
|
|||||||
var order := {}
|
var order := {}
|
||||||
for i in out.size():
|
for i in out.size():
|
||||||
order[out[i].id] = i
|
order[out[i].id] = i
|
||||||
out.sort_custom(func(a, b): return p.level(a.id) > p.level(b.id) if p.level(a.id) != p.level(b.id) else order[a.id] < order[b.id])
|
out.sort_custom(func(a, b):
|
||||||
|
if p.owns(a.id) != p.owns(b.id):
|
||||||
|
return p.owns(a.id)
|
||||||
|
return p.level(a.id) > p.level(b.id) if p.level(a.id) != p.level(b.id) else order[a.id] < order[b.id])
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -111,11 +116,31 @@ func dismantle() -> void:
|
|||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func craft() -> void:
|
||||||
|
if selected == null:
|
||||||
|
return
|
||||||
|
var err: String = app.profile.craft(selected.id)
|
||||||
|
if err == "":
|
||||||
|
app.sfx.play("combo")
|
||||||
|
app.save()
|
||||||
|
say(tr("%s hergestellt") % selected.title(), true)
|
||||||
|
else:
|
||||||
|
say(err, false)
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
func refresh() -> void:
|
func refresh() -> void:
|
||||||
var p: Profile = app.profile
|
var p: Profile = app.profile
|
||||||
dust_label.text = "Staub: %d" % p.dust
|
dust_label.text = "Staub: %d" % p.dust
|
||||||
grid.fill(cards(), func(c): return p.rarity(c.id))
|
var shown := cards()
|
||||||
|
grid.fill(shown.map(func(c): return c.leveled(p.level(c.id))), func(c): return Cards.RARITIES[c.look])
|
||||||
|
var views := grid.views()
|
||||||
|
for i in views.size():
|
||||||
|
views[i].dim = not p.owns(shown[i].id)
|
||||||
Style.clear(preview_holder)
|
Style.clear(preview_holder)
|
||||||
|
b_craft.visible = false
|
||||||
|
b_upgrade.visible = true
|
||||||
|
b_dismantle.visible = true
|
||||||
if selected == null:
|
if selected == null:
|
||||||
info_label.text = "Keine Karte gewählt"
|
info_label.text = "Keine Karte gewählt"
|
||||||
b_upgrade.text = "Aufwerten"
|
b_upgrade.text = "Aufwerten"
|
||||||
@@ -123,11 +148,19 @@ func refresh() -> void:
|
|||||||
b_upgrade.disabled = true
|
b_upgrade.disabled = true
|
||||||
b_dismantle.disabled = true
|
b_dismantle.disabled = true
|
||||||
return
|
return
|
||||||
var v := CardView.new(selected, Vector2(220, 308), false, p.rarity(selected.id))
|
var v := CardView.new(selected.leveled(p.level(selected.id)), Vector2(220, 308), false, p.rarity(selected.id))
|
||||||
v.position = Vector2(920, 152)
|
v.position = Vector2(920, 152)
|
||||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
preview_holder.add_child(v)
|
preview_holder.add_child(v)
|
||||||
var lvl := p.level(selected.id)
|
var lvl := p.level(selected.id)
|
||||||
|
if not p.owns(selected.id):
|
||||||
|
info_label.text = tr("Nicht in der Sammlung")
|
||||||
|
b_upgrade.visible = false
|
||||||
|
b_dismantle.visible = false
|
||||||
|
b_craft.visible = true
|
||||||
|
b_craft.text = tr("Herstellen (%d Staub)") % Profile.CRAFT_COST
|
||||||
|
b_craft.disabled = p.dust < Profile.CRAFT_COST
|
||||||
|
return
|
||||||
info_label.text = "%s · %s" % [tr(selected.combo()), tr(p.rarity(selected.id))]
|
info_label.text = "%s · %s" % [tr(selected.combo()), tr(p.rarity(selected.id))]
|
||||||
if lvl >= Cards.RARITIES.size() - 1:
|
if lvl >= Cards.RARITIES.size() - 1:
|
||||||
b_upgrade.text = "Höchste Stufe"
|
b_upgrade.text = "Höchste Stufe"
|
||||||
@@ -136,5 +169,5 @@ func refresh() -> void:
|
|||||||
var next: String = Cards.RARITIES[lvl + 1]
|
var next: String = Cards.RARITIES[lvl + 1]
|
||||||
b_upgrade.text = "Aufwerten zu %s (%d Staub)" % [tr(next), Profile.DUST_UPGRADE[next]]
|
b_upgrade.text = "Aufwerten zu %s (%d Staub)" % [tr(next), Profile.DUST_UPGRADE[next]]
|
||||||
b_upgrade.disabled = p.dust < Profile.DUST_UPGRADE[next]
|
b_upgrade.disabled = p.dust < Profile.DUST_UPGRADE[next]
|
||||||
b_dismantle.text = "Zerlegen" if lvl == 0 else "Zerlegen (+%d Staub)" % Profile.DUST_DISMANTLE[Cards.RARITIES[lvl]]
|
b_dismantle.text = "Zerlegen (+%d Staub)" % Profile.DUST_DISMANTLE[Cards.RARITIES[lvl]]
|
||||||
b_dismantle.disabled = lvl == 0
|
b_dismantle.disabled = false
|
||||||
|
|||||||
93
scripts/ui/deck_screen.gd
Normal file
93
scripts/ui/deck_screen.gd
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
class_name DeckScreen
|
||||||
|
extends Control
|
||||||
|
## Deck zusammenstellen: alle eigenen Karten im Raster, ein Klick nimmt eine Karte in den
|
||||||
|
## Stapel oder wieder heraus. Karten außerhalb des Stapels sind abgedunkelt. Der Stapel hält
|
||||||
|
## 40 bis 60 Karten; „Auffüllen“ nimmt die teuersten eigenen Karten des gewählten Filters.
|
||||||
|
|
||||||
|
var app
|
||||||
|
var filter := ""
|
||||||
|
var grid: CardGrid
|
||||||
|
var count_label: Label
|
||||||
|
var msg_label: Label
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
Style.header(self, "Deck", func(): app.show_screen(MenuScreen.new()))
|
||||||
|
Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind(""))
|
||||||
|
for i in Cards.ELEMENTS.size():
|
||||||
|
var e: String = Cards.ELEMENTS[i]
|
||||||
|
var fb := Style.button(self, e, Rect2(108 + i * 82, 86, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||||
|
fb.add_theme_font_size_override("font_size", 14)
|
||||||
|
grid = CardGrid.new(Rect2(40, 132, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 650))
|
||||||
|
grid.card_clicked.connect(toggle)
|
||||||
|
add_child(grid)
|
||||||
|
Style.panel(self, Rect2(820, 86, 420, 696))
|
||||||
|
count_label = Style.chip(self, "", Rect2(860, 100, 340, 36), Style.BLUE)
|
||||||
|
var hint := Style.label(self, "Klick nimmt eine Karte in den Stapel oder heraus. Auffüllen nimmt die teuersten Karten des Filters.", 15, Style.MUTED, Rect2(840, 160, 380, 60), "center")
|
||||||
|
hint.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||||
|
Style.button(self, "Auffüllen", Rect2(840, 240, 380, 46), fill_up)
|
||||||
|
Style.button(self, "Leeren", Rect2(840, 296, 380, 46), clear_deck, Color("5a3c28"))
|
||||||
|
msg_label = Style.label(self, "", 15, Style.RED, Rect2(840, 360, 380, 48), "center")
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if event.is_action_pressed("ui_cancel"):
|
||||||
|
app.show_screen(MenuScreen.new())
|
||||||
|
|
||||||
|
|
||||||
|
func set_filter(e: String) -> void:
|
||||||
|
filter = e
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func toggle(card: Cards.Card) -> void:
|
||||||
|
var err: String = app.profile.toggle_deck(card.id)
|
||||||
|
msg_label.text = err
|
||||||
|
if err == "":
|
||||||
|
app.sfx.play("click")
|
||||||
|
app.save()
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
## Füllt mit den teuersten eigenen Karten des gewählten Filters bis zum Höchstwert auf.
|
||||||
|
func fill_up() -> void:
|
||||||
|
var p: Profile = app.profile
|
||||||
|
var pool: Array = p.collection.keys()
|
||||||
|
if filter != "":
|
||||||
|
pool = pool.filter(func(id): return filter in Cards.by_id(id).elements)
|
||||||
|
p.deck = DeckGen.fill(p.deck, pool, Game.DECK_MAX)
|
||||||
|
app.save()
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func clear_deck() -> void:
|
||||||
|
app.profile.deck = []
|
||||||
|
app.save()
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
## Eigene Karten, die im Stapel zuerst, sonst nach Kosten.
|
||||||
|
func cards() -> Array:
|
||||||
|
var p: Profile = app.profile
|
||||||
|
var out: Array = p.collection.keys().map(func(id): return Cards.by_id(id))
|
||||||
|
if filter != "":
|
||||||
|
out = out.filter(func(c): return filter in c.elements)
|
||||||
|
out.sort_custom(func(a, b):
|
||||||
|
var ia: bool = a.id in p.deck
|
||||||
|
var ib: bool = b.id in p.deck
|
||||||
|
if ia != ib:
|
||||||
|
return ia
|
||||||
|
return a.cost < b.cost if a.cost != b.cost else a.id < b.id)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
func refresh() -> void:
|
||||||
|
var p: Profile = app.profile
|
||||||
|
count_label.text = tr("Stapel: %d Karten (%d bis %d)") % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX]
|
||||||
|
count_label.add_theme_color_override("font_color", Style.RED if p.deck.size() < Game.DECK_MIN else Style.BLUE)
|
||||||
|
var shown := cards()
|
||||||
|
grid.fill(shown.map(func(c): return c.leveled(p.level(c.id))), func(c): return Cards.RARITIES[c.look])
|
||||||
|
var views := grid.views()
|
||||||
|
for i in views.size():
|
||||||
|
views[i].dim = not (shown[i].id in p.deck)
|
||||||
1
scripts/ui/deck_screen.gd.uid
Normal file
1
scripts/ui/deck_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ctt36od26uyea
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
class_name ElementView
|
class_name ElementView
|
||||||
extends Control
|
extends Control
|
||||||
## Elementzähler der eigenen Seite, mittig: je Element zweimal das Symbol seines Sondereffekts in
|
## Elementzähler einer Seite als Liste neben ihrem Brett. Es stehen nur Elemente da, bei denen
|
||||||
## der Elementfarbe, leuchtend je offenem eigenen Wesen (beide: Effekt aktiv), daneben in der
|
## etwas passiert ist: je Zeile Name, zweimal das Symbol seines Sondereffekts (leuchtend je
|
||||||
## Elementfarbe das Mana der bisher ausgespielten Karten (je Game.ELEMENT_MANA gibt es +1 Angriff).
|
## offenem Wesen, beide: Effekt aktiv), die Zahl der ausgespielten Karten und ab
|
||||||
|
## Game.ELEMENT_COUNT der Bonus +1/+1.
|
||||||
|
|
||||||
const ENTRY_W := 180.0
|
const ROW := 32.0
|
||||||
|
|
||||||
var own := {} # offene eigene Wesen je Element
|
var own := {} # offene eigene Wesen je Element
|
||||||
var played := {} # ausgespielte Karten je Element
|
var played := {} # ausgespielte Karten je Element
|
||||||
@@ -31,37 +32,41 @@ func paired(e: String) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
func bonus(e: String) -> int:
|
func bonus(e: String) -> int:
|
||||||
return int(played.get(e, 0)) / Game.ELEMENT_MANA
|
return int(played.get(e, 0)) / Game.ELEMENT_COUNT
|
||||||
|
|
||||||
|
|
||||||
|
## Elemente, bei denen sich etwas getan hat (offene Wesen oder ausgespielte Karten).
|
||||||
|
func shown() -> Array:
|
||||||
|
return Cards.ELEMENTS.filter(func(e): return int(own.get(e, 0)) > 0 or int(played.get(e, 0)) > 0)
|
||||||
|
|
||||||
|
|
||||||
## Der Inhalt als Text (Tests, Protokoll): Element offen/gespielt, dazu Effekt und Bonus.
|
## Der Inhalt als Text (Tests, Protokoll): Element offen/gespielt, dazu Effekt und Bonus.
|
||||||
func text() -> String:
|
func text() -> String:
|
||||||
var parts: Array[String] = []
|
var parts: Array[String] = []
|
||||||
for e in Cards.ELEMENTS:
|
for e in shown():
|
||||||
var s := "%s %d/%d" % [tr(e), int(own.get(e, 0)), int(played.get(e, 0))]
|
var s := "%s %d/%d" % [tr(e), int(own.get(e, 0)), int(played.get(e, 0))]
|
||||||
if paired(e):
|
if paired(e):
|
||||||
s += " (%s)" % tr(Cards.ELEMENT_KEYWORD[e])
|
s += " (%s)" % tr(Cards.ELEMENT_KEYWORD[e])
|
||||||
if bonus(e) > 0:
|
if bonus(e) > 0:
|
||||||
s += " (+%d)" % bonus(e)
|
s += " (+%d/+%d)" % [bonus(e), bonus(e)]
|
||||||
parts.append(s)
|
parts.append(s)
|
||||||
return " · ".join(parts)
|
return " · ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
var x0 := (size.x - ENTRY_W * Cards.ELEMENTS.size()) / 2
|
var y := ROW / 2
|
||||||
var cy := size.y / 2
|
for e in shown():
|
||||||
for i in Cards.ELEMENTS.size():
|
|
||||||
var e: String = Cards.ELEMENTS[i]
|
|
||||||
var col: Color = Cards.ELEMENT_COLORS[e]
|
var col: Color = Cards.ELEMENT_COLORS[e]
|
||||||
var active := paired(e) or bonus(e) > 0
|
var active := paired(e) or bonus(e) > 0
|
||||||
var px := x0 + i * ENTRY_W + 10
|
draw_style_box(Style.box(Color(col, 0.18 if active else 0.1), 6, Color(col, 0.6 if active else 0.0), 1 if active else 0), Rect2(0, y - ROW / 2 + 2, size.x, ROW - 4))
|
||||||
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(px, cy), "midleft", active)
|
var px := 10.0
|
||||||
px += Style.FONT_BOLD.get_string_size(tr(e), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(13)).x + 10
|
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(px, y), "midleft", active)
|
||||||
|
px = 76
|
||||||
for k in Game.ELEMENT_PAIR:
|
for k in Game.ELEMENT_PAIR:
|
||||||
var filled := k < int(own.get(e, 0))
|
var filled := k < int(own.get(e, 0))
|
||||||
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + k * 24 + 10, cy), 8, Style.lighter(col, 0.45) if filled else Color(col, 0.3))
|
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + k * 24 + 10, y), 8, Style.lighter(col, 0.45) if filled else Color(col, 0.3))
|
||||||
px += Game.ELEMENT_PAIR * 24 + 8
|
px += Game.ELEMENT_PAIR * 24 + 8
|
||||||
Style.text(self, str(int(played.get(e, 0))), 15, Style.lighter(col, 0.45), Vector2(px, cy), "midleft", true)
|
Style.text(self, str(int(played.get(e, 0))), 15, Style.lighter(col, 0.45), Vector2(px, y), "midleft", true)
|
||||||
px += 20
|
|
||||||
if bonus(e) > 0:
|
if bonus(e) > 0:
|
||||||
Style.text(self, "+%d" % bonus(e), 12, Style.GREEN, Vector2(px, cy), "midleft", true)
|
Style.text(self, "+%d/+%d" % [bonus(e), bonus(e)], 12, Style.GREEN, Vector2(size.x - 6, y), "midright", true)
|
||||||
|
y += ROW
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
class_name LobbyScreen
|
|
||||||
extends Control
|
|
||||||
## Netzwerkspiel im lokalen Netz: Spiel eröffnen oder beitreten.
|
|
||||||
|
|
||||||
var app
|
|
||||||
var net: Net
|
|
||||||
var status_label: Label
|
|
||||||
var list_label: Label
|
|
||||||
var ip_box: LineEdit
|
|
||||||
var b_host: Button
|
|
||||||
var b_join: Button
|
|
||||||
var b_watch: Button
|
|
||||||
var b_start: Button
|
|
||||||
var addr_label: Label
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
net = app.net
|
|
||||||
net.my_name = "Spieler"
|
|
||||||
net.my_pick = {"looks": app.profile.looks()}
|
|
||||||
Style.header(self, "Netzwerkspiel", _back)
|
|
||||||
var col := Style.column(self, 760, 110, 10)
|
|
||||||
var hr := Style.row(col, 12)
|
|
||||||
hr.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
||||||
b_host = Style.cbutton(hr, "Spiel eröffnen", Vector2(200, 46), _host, Color("285a3c"))
|
|
||||||
ip_box = LineEdit.new()
|
|
||||||
ip_box.custom_minimum_size = Vector2(220, 46)
|
|
||||||
ip_box.placeholder_text = "Adresse des Hosts"
|
|
||||||
ip_box.text = "127.0.0.1"
|
|
||||||
hr.add_child(ip_box)
|
|
||||||
b_join = Style.cbutton(hr, "Beitreten", Vector2(160, 46), _join)
|
|
||||||
b_watch = Style.cbutton(hr, "Zuschauen", Vector2(150, 46), _watch)
|
|
||||||
addr_label = Style.clabel(col, "", 14, Style.MUTED)
|
|
||||||
status_label = Style.clabel(col, "", 16, Style.BLUE)
|
|
||||||
Style.spacer(col, 6)
|
|
||||||
Style.clabel(col, "Am Tisch", 20, Style.TEXT, "center", true)
|
|
||||||
list_label = Style.clabel(col, "", 16, Style.TEXT)
|
|
||||||
Style.spacer(col, 10)
|
|
||||||
var br := Style.row(col, 20)
|
|
||||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
||||||
b_start = Style.cbutton(br, "Start", Vector2(200, 48), _start, Color("285a3c"))
|
|
||||||
net.lobby_changed.connect(refresh)
|
|
||||||
net.started.connect(_started)
|
|
||||||
net.ended.connect(func(_reason): refresh())
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
|
||||||
if net.lobby_changed.is_connected(refresh):
|
|
||||||
net.lobby_changed.disconnect(refresh)
|
|
||||||
if net.started.is_connected(_started):
|
|
||||||
net.started.disconnect(_started)
|
|
||||||
|
|
||||||
|
|
||||||
func _host() -> void:
|
|
||||||
net.host()
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
|
|
||||||
func _join() -> void:
|
|
||||||
net.join(ip_box.text.strip_edges())
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
|
|
||||||
## Als Zuschauer beitreten: sieht die Partie ohne Hände, spielt nicht mit.
|
|
||||||
func _watch() -> void:
|
|
||||||
net.spectator = true
|
|
||||||
net.join(ip_box.text.strip_edges())
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
|
|
||||||
func _start() -> void:
|
|
||||||
net.start_battle()
|
|
||||||
|
|
||||||
|
|
||||||
func _started(seat: int) -> void:
|
|
||||||
app.show_screen(BattleScreen.online(seat))
|
|
||||||
|
|
||||||
|
|
||||||
func _back() -> void:
|
|
||||||
net.leave()
|
|
||||||
app.show_screen(MenuScreen.new())
|
|
||||||
|
|
||||||
|
|
||||||
func refresh() -> void:
|
|
||||||
status_label.text = net.status
|
|
||||||
var lines: Array[String] = []
|
|
||||||
var ids: Array = net.names.keys()
|
|
||||||
ids.sort()
|
|
||||||
for id in ids:
|
|
||||||
lines.append("%s%s" % [net.names[id], " (Host)" if id == 1 else ""])
|
|
||||||
for id in net.spectators:
|
|
||||||
lines.append("%s (%s)" % [net.spectators[id], tr("Zuschauer")])
|
|
||||||
list_label.text = "\n".join(lines) if not lines.is_empty() else "–"
|
|
||||||
b_host.disabled = net.connected
|
|
||||||
b_join.disabled = net.connected
|
|
||||||
b_watch.disabled = net.connected
|
|
||||||
ip_box.editable = not net.connected
|
|
||||||
var humans := ids.size()
|
|
||||||
b_start.disabled = not net.is_host
|
|
||||||
b_start.text = tr("Start") if humans >= 2 else tr("Start gegen die KI")
|
|
||||||
addr_label.text = ("Deine Adressen: " + ", ".join(net.local_addresses())) if net.is_host else ""
|
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
|
||||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE and not ip_box.has_focus():
|
|
||||||
_back()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://bhnnxjh6rsxnf
|
|
||||||
@@ -5,7 +5,6 @@ var profile: Profile
|
|||||||
var screen: Control
|
var screen: Control
|
||||||
var save_path: String = Profile.SAVE_PATH
|
var save_path: String = Profile.SAVE_PATH
|
||||||
var sfx: Sfx
|
var sfx: Sfx
|
||||||
var net: Net
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -17,14 +16,9 @@ func _ready() -> void:
|
|||||||
add_child(bg)
|
add_child(bg)
|
||||||
sfx = Sfx.new()
|
sfx = Sfx.new()
|
||||||
add_child(sfx)
|
add_child(sfx)
|
||||||
# Fester Pfad /root/Net: Netzwerkaufrufe werden über den Knotenpfad zugestellt,
|
|
||||||
# der bei Host und Mitspielern gleich sein muss.
|
|
||||||
net = Net.new()
|
|
||||||
net.name = "Net"
|
|
||||||
get_tree().root.add_child.call_deferred(net)
|
|
||||||
profile = Profile.load_or_new(save_path)
|
profile = Profile.load_or_new(save_path)
|
||||||
apply_settings()
|
apply_settings()
|
||||||
show_screen(MenuScreen.new())
|
show_screen(MenuScreen.new() if profile.started() else StartDeckScreen.new())
|
||||||
|
|
||||||
|
|
||||||
func apply_settings() -> void:
|
func apply_settings() -> void:
|
||||||
@@ -36,20 +30,32 @@ func apply_settings() -> void:
|
|||||||
theme = Style.make_theme()
|
theme = Style.make_theme()
|
||||||
|
|
||||||
|
|
||||||
## Neues Profil: Sammlung und Fortschritt werden verworfen.
|
## Neues Profil: Sammlung und Fortschritt werden verworfen, das Startdeck wird neu gewählt.
|
||||||
func reset_profile() -> void:
|
func reset_profile() -> void:
|
||||||
profile = Profile.starter()
|
profile = Profile.starter()
|
||||||
save()
|
save()
|
||||||
apply_settings()
|
apply_settings()
|
||||||
|
show_screen(StartDeckScreen.new())
|
||||||
|
|
||||||
|
|
||||||
func set_language(code: String) -> void:
|
func set_language(code: String) -> void:
|
||||||
TranslationServer.set_locale(code)
|
TranslationServer.set_locale(code)
|
||||||
|
|
||||||
|
|
||||||
func show_screen(s: Control) -> void:
|
var kept: Array = [] # vorige Bildschirme, zu denen back() zurückführt (Einstellungen aus dem Kampf)
|
||||||
|
|
||||||
|
|
||||||
|
## keep: der aktuelle Bildschirm bleibt erhalten und kommt mit back() zurück.
|
||||||
|
func show_screen(s: Control, keep: bool = false) -> void:
|
||||||
if screen != null:
|
if screen != null:
|
||||||
screen.queue_free()
|
if keep:
|
||||||
|
remove_child(screen)
|
||||||
|
kept.append(screen)
|
||||||
|
else:
|
||||||
|
screen.queue_free()
|
||||||
|
for k in kept:
|
||||||
|
k.queue_free()
|
||||||
|
kept.clear()
|
||||||
screen = s
|
screen = s
|
||||||
s.app = self
|
s.app = self
|
||||||
s.set_anchors_preset(Control.PRESET_FULL_RECT)
|
s.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
@@ -106,11 +112,17 @@ func _first_button(node: Node) -> Button:
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
## Zurück: zum gemerkten Bildschirm, sonst ins Hauptmenü.
|
||||||
|
func back() -> void:
|
||||||
|
if kept.is_empty():
|
||||||
|
show_screen(MenuScreen.new())
|
||||||
|
return
|
||||||
|
var prev: Control = kept.pop_back()
|
||||||
|
screen.queue_free()
|
||||||
|
screen = prev
|
||||||
|
add_child(prev)
|
||||||
|
|
||||||
|
|
||||||
func save() -> void:
|
func save() -> void:
|
||||||
profile.save(save_path)
|
profile.save(save_path)
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
|
||||||
if net != null and net.is_inside_tree():
|
|
||||||
net.leave()
|
|
||||||
net.queue_free()
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func _ready() -> void:
|
|||||||
var y := 230
|
var y := 230
|
||||||
for entry in [["Einzelspieler", func(): app.show_screen(OpponentSelectScreen.new())],
|
for entry in [["Einzelspieler", func(): app.show_screen(OpponentSelectScreen.new())],
|
||||||
["Kampagne", func(): app.show_screen(CampaignScreen.new())],
|
["Kampagne", func(): app.show_screen(CampaignScreen.new())],
|
||||||
["Netzwerkspiel", func(): app.show_screen(LobbyScreen.new())]]:
|
["Deck", func(): app.show_screen(DeckScreen.new())]]:
|
||||||
var b := Style.button(self, entry[0], Rect2(80, y, 380, 62), entry[1], Style.PRIMARY)
|
var b := Style.button(self, entry[0], Rect2(80, y, 380, 62), entry[1], Style.PRIMARY)
|
||||||
b.add_theme_font_size_override("font_size", 24)
|
b.add_theme_font_size_override("font_size", 24)
|
||||||
y += 74
|
y += 74
|
||||||
@@ -35,9 +35,9 @@ func _ready() -> void:
|
|||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
## Drei Karten der Sammlung, die höchsten Stufen zuerst, leicht gefächert.
|
## Drei eigene Karten, die höchsten Stufen zuerst, leicht gefächert.
|
||||||
func _showcase(p: Profile) -> void:
|
func _showcase(p: Profile) -> void:
|
||||||
var cards: Array = Cards.all().duplicate()
|
var cards: Array = p.collection.keys().map(func(id): return Cards.by_id(id))
|
||||||
cards.sort_custom(func(a, b):
|
cards.sort_custom(func(a, b):
|
||||||
if p.level(a.id) != p.level(b.id):
|
if p.level(a.id) != p.level(b.id):
|
||||||
return p.level(a.id) > p.level(b.id)
|
return p.level(a.id) > p.level(b.id)
|
||||||
@@ -45,7 +45,7 @@ func _showcase(p: Profile) -> void:
|
|||||||
var spots := [[Vector2(830, 300), -0.14], [Vector2(950, 270), 0.0], [Vector2(1070, 300), 0.14]]
|
var spots := [[Vector2(830, 300), -0.14], [Vector2(950, 270), 0.0], [Vector2(1070, 300), 0.14]]
|
||||||
for i in 3:
|
for i in 3:
|
||||||
var c: Cards.Card = cards[i]
|
var c: Cards.Card = cards[i]
|
||||||
var v := CardView.new(c, Vector2(180, 252), false, p.rarity(c.id))
|
var v := CardView.new(c.leveled(p.level(c.id)), Vector2(180, 252), false, p.rarity(c.id))
|
||||||
v.position = spots[i][0]
|
v.position = spots[i][0]
|
||||||
v.rotation = spots[i][1]
|
v.rotation = spots[i][1]
|
||||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
@@ -55,8 +55,8 @@ func _showcase(p: Profile) -> void:
|
|||||||
func refresh() -> void:
|
func refresh() -> void:
|
||||||
var p: Profile = app.profile
|
var p: Profile = app.profile
|
||||||
coins_label.text = tr("Münzen: %d · Staub: %d") % [p.coins, p.dust]
|
coins_label.text = tr("Münzen: %d · Staub: %d") % [p.coins, p.dust]
|
||||||
coll_label.text = tr("Aufgewertet: %d von %d Karten · Kampagne: %d/%d") % [
|
coll_label.text = tr("Sammlung: %d von %d Karten · Kampagne: %d/%d") % [
|
||||||
p.upgraded_count(), Cards.all().size(), p.campaign_stage, Campaign.total()]
|
p.collection.size(), Cards.all().size(), p.campaign_stage, Campaign.total()]
|
||||||
var notes := p.take_notices()
|
var notes := p.take_notices()
|
||||||
notice_label.text = " · ".join(notes)
|
notice_label.text = " · ".join(notes)
|
||||||
if not notes.is_empty():
|
if not notes.is_empty():
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ var speed_label: Label
|
|||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
var s: Dictionary = app.profile.settings
|
var s: Dictionary = app.profile.settings
|
||||||
Style.header(self, "Einstellungen", func(): app.show_screen(MenuScreen.new()))
|
Style.header(self, "Einstellungen", func(): app.back())
|
||||||
var col := Style.column(self, 700, 110, 24)
|
var col := Style.column(self, 700, 110, 24)
|
||||||
var grid := GridContainer.new()
|
var grid := GridContainer.new()
|
||||||
grid.columns = 3
|
grid.columns = 3
|
||||||
@@ -86,7 +86,6 @@ func _ready() -> void:
|
|||||||
|
|
||||||
func _reset() -> void:
|
func _reset() -> void:
|
||||||
app.reset_profile()
|
app.reset_profile()
|
||||||
app.show_screen(MenuScreen.new())
|
|
||||||
|
|
||||||
|
|
||||||
func _on_big_font(on: bool) -> void:
|
func _on_big_font(on: bool) -> void:
|
||||||
@@ -139,4 +138,4 @@ func _on_anim(on: bool) -> void:
|
|||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||||
app.show_screen(MenuScreen.new())
|
app.back()
|
||||||
|
|||||||
37
scripts/ui/start_deck_screen.gd
Normal file
37
scripts/ui/start_deck_screen.gd
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
class_name StartDeckScreen
|
||||||
|
extends Control
|
||||||
|
## Erster Start: eines von vier Startdecks wählen, je eines mit einem Element im Mittelpunkt
|
||||||
|
## (25 Karten davon, je fünf der anderen). Danach geht es ins Menü.
|
||||||
|
|
||||||
|
var app
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
Style.label(self, tr("Wähle dein Startdeck"), 40, Style.GOLD, Rect2(0, 60, 1280, 60), "center", true)
|
||||||
|
Style.label(self, tr("25 Karten des Elements, dazu je fünf der anderen. Booster und Werkstatt bringen später den Rest."), 17, Style.MUTED, Rect2(140, 124, 1000, 30), "center")
|
||||||
|
for i in Cards.ELEMENTS.size():
|
||||||
|
var e: String = Cards.ELEMENTS[i]
|
||||||
|
var x := 70 + i * 290
|
||||||
|
var col: Color = Cards.ELEMENT_COLORS[e]
|
||||||
|
Style.panel(self, Rect2(x, 190, 260, 470), Style.darker(col, 0.75), 12, col, 2)
|
||||||
|
var ids := DeckGen.start_deck(e)
|
||||||
|
var sample: Array = ids.filter(func(id): return Cards.by_id(id).elements[0] == e)
|
||||||
|
sample.sort_custom(func(a, b): return Cards.by_id(a).cost > Cards.by_id(b).cost)
|
||||||
|
for k in 3:
|
||||||
|
var c: Cards.Card = Cards.by_id(sample[k * 7])
|
||||||
|
var v := CardView.new(c, Vector2(110, 140), false, "Common")
|
||||||
|
v.position = Vector2(x + 20 + k * 40, 210 + k * 14)
|
||||||
|
v.rotation_degrees = -6 + k * 6
|
||||||
|
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(v)
|
||||||
|
Style.label(self, tr(e), 28, Style.TEXT, Rect2(x, 400, 260, 40), "center", true)
|
||||||
|
Style.label(self, tr(Cards.KEYWORD_TEXT[Cards.ELEMENT_KEYWORD[e]]), 15, Style.MUTED, Rect2(x + 16, 446, 228, 60), "center")
|
||||||
|
var b := Style.button(self, tr("%s wählen") % tr(e), Rect2(x + 30, 590, 200, 50), choose.bind(e), Style.PRIMARY)
|
||||||
|
b.add_theme_font_size_override("font_size", 18)
|
||||||
|
|
||||||
|
|
||||||
|
func choose(element: String) -> void:
|
||||||
|
app.profile.choose_start(element)
|
||||||
|
app.save()
|
||||||
|
app.sfx.play("combo")
|
||||||
|
app.show_screen(MenuScreen.new())
|
||||||
1
scripts/ui/start_deck_screen.gd.uid
Normal file
1
scripts/ui/start_deck_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bxl6isdrgqb2b
|
||||||
101
tests/net_bot.gd
101
tests/net_bot.gd
@@ -1,101 +0,0 @@
|
|||||||
extends Node
|
|
||||||
## Test-Mitspieler: tritt einem Host bei und spielt einfache Züge; mit --host eröffnet er
|
|
||||||
## selbst und startet, sobald ein zweiter Mensch am Tisch sitzt (ohne KI).
|
|
||||||
## godot --path . tests/net_bot.tscn -- --ip=127.0.0.1 --name=Bot
|
|
||||||
## godot --path . tests/net_bot.tscn -- --host --name=BotHost
|
|
||||||
|
|
||||||
var net: Net
|
|
||||||
var seat := -1
|
|
||||||
var game: Game
|
|
||||||
var t := 0.0
|
|
||||||
var hosting := false
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
var ip := "127.0.0.1"
|
|
||||||
var bot_name := "Bot"
|
|
||||||
for a in OS.get_cmdline_user_args():
|
|
||||||
if a.begins_with("--ip="):
|
|
||||||
ip = a.substr(5)
|
|
||||||
if a.begins_with("--name="):
|
|
||||||
bot_name = a.substr(7)
|
|
||||||
if a == "--host":
|
|
||||||
hosting = true
|
|
||||||
net = Net.new()
|
|
||||||
net.name = "Net"
|
|
||||||
get_tree().root.add_child.call_deferred(net)
|
|
||||||
net.my_name = bot_name
|
|
||||||
net.my_pick = {"elements": ["Feuer", "Erde"], "looks": {}}
|
|
||||||
net.started.connect(func(s):
|
|
||||||
seat = s
|
|
||||||
if hosting:
|
|
||||||
net.broadcast() # erster Zustand für alle, wie es sonst der Kampfbildschirm tut
|
|
||||||
_act_host.call_deferred())
|
|
||||||
net.snapshot_received.connect(_on_snapshot)
|
|
||||||
net.ended.connect(func(_r): get_tree().quit())
|
|
||||||
await get_tree().process_frame
|
|
||||||
if hosting:
|
|
||||||
net.host()
|
|
||||||
net.lobby_changed.connect(func():
|
|
||||||
if net.game == null and net.names.size() >= 2:
|
|
||||||
net.start_battle())
|
|
||||||
net.action_applied.connect(func():
|
|
||||||
net.broadcast()
|
|
||||||
_act_host.call_deferred())
|
|
||||||
else:
|
|
||||||
net.join(ip)
|
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
|
||||||
t += delta
|
|
||||||
if hosting and net.game != null:
|
|
||||||
net.tick()
|
|
||||||
if t > 180.0 or FileAccess.file_exists("user://bot_quit"):
|
|
||||||
net.leave()
|
|
||||||
get_tree().quit()
|
|
||||||
|
|
||||||
|
|
||||||
func _on_snapshot(snap: Dictionary) -> void:
|
|
||||||
game = Game.from_snapshot(snap)
|
|
||||||
if game.winner != "":
|
|
||||||
await get_tree().create_timer(1.0).timeout
|
|
||||||
get_tree().quit()
|
|
||||||
return
|
|
||||||
if game.current_idx != seat:
|
|
||||||
return
|
|
||||||
_decide(game.players[seat])
|
|
||||||
|
|
||||||
|
|
||||||
func _act_host() -> void:
|
|
||||||
if not hosting or net.game == null or net.game.winner != "" or seat < 0 or net.game.current_idx != seat:
|
|
||||||
return
|
|
||||||
game = net.game
|
|
||||||
_decide(game.players[seat])
|
|
||||||
|
|
||||||
|
|
||||||
## Einfache Züge mit kurzer Bedenkzeit: das erste spielbare Wesen legen, dann angreifen,
|
|
||||||
## dann ziehen, sonst Zug beenden.
|
|
||||||
func _decide(me: Game.Player) -> void:
|
|
||||||
var g := game
|
|
||||||
await get_tree().create_timer(0.25).timeout
|
|
||||||
if game != g or (hosting and (net.game == null or net.game.current_idx != seat)):
|
|
||||||
return # inzwischen ein neuer Zustand
|
|
||||||
for i in me.hand.size():
|
|
||||||
var card: Cards.Card = me.hand[i]
|
|
||||||
if game.can_play(me, card)[0]:
|
|
||||||
net.send_action({"type": "play", "hand": i})
|
|
||||||
return
|
|
||||||
for cr in me.board:
|
|
||||||
if cr.can_attack():
|
|
||||||
for e in game.enemies_of(me):
|
|
||||||
if game.can_attack(cr, e)[0]:
|
|
||||||
net.send_action({"type": "attack", "uid": cr.uid, "target": {"kind": "player", "index": game.players.find(e)}})
|
|
||||||
return
|
|
||||||
for c in e.board:
|
|
||||||
if game.can_attack(cr, c)[0]:
|
|
||||||
net.send_action({"type": "attack", "uid": cr.uid, "target": {"kind": "creature", "uid": c.uid}})
|
|
||||||
return
|
|
||||||
if game.can_draw(me)[0]:
|
|
||||||
net.send_action({"type": "draw"})
|
|
||||||
return
|
|
||||||
net.send_action({"type": "end"})
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://dcbqt8ybdkhrb
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[gd_scene load_steps=2 format=3]
|
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://tests/net_bot.gd" id="1"]
|
|
||||||
|
|
||||||
[node name="NetBot" type="Node"]
|
|
||||||
script = ExtResource("1")
|
|
||||||
@@ -14,7 +14,7 @@ func _initialize() -> void:
|
|||||||
print("Kartenpool laden: %.1f ms" % t)
|
print("Kartenpool laden: %.1f ms" % t)
|
||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
r.seed = 3
|
r.seed = 3
|
||||||
print("Stapel mischen: %.2f ms" % ms(func(): DeckGen.deck(r), 20))
|
print("Stapel würfeln: %.2f ms" % ms(func(): DeckGen.random_deck(r), 20))
|
||||||
print("Gegner erzeugen: %.2f ms" % ms(func(): DeckGen.random_opponent(), 20))
|
print("Gegner erzeugen: %.2f ms" % ms(func(): DeckGen.random_opponent(), 20))
|
||||||
for n in [1]:
|
for n in [1]:
|
||||||
var g := Game.new_battle(r)
|
var g := Game.new_battle(r)
|
||||||
@@ -22,7 +22,7 @@ func _initialize() -> void:
|
|||||||
var uid := 5000
|
var uid := 5000
|
||||||
for p in g.players:
|
for p in g.players:
|
||||||
for k in 4:
|
for k in 4:
|
||||||
var c := Game.Creature.new(Cards.by_id("f_wolf"), p, uid)
|
var c := Game.Creature.new(Cards.by_id("f_3_2"), p, uid)
|
||||||
uid += 1
|
uid += 1
|
||||||
c.ready = true
|
c.ready = true
|
||||||
p.board.append(c)
|
p.board.append(c)
|
||||||
@@ -30,7 +30,7 @@ func _initialize() -> void:
|
|||||||
h.energy = 10
|
h.energy = 10
|
||||||
h.max_energy = 10
|
h.max_energy = 10
|
||||||
for k in 6:
|
for k in 6:
|
||||||
h.hand.append(Cards.by_id("f_ritter"))
|
h.hand.append(Cards.by_id("f_4_3"))
|
||||||
AI.time_budget_ms = 100000
|
AI.time_budget_ms = 100000
|
||||||
var t_full := ms(func(): AI.choose_action(g, h))
|
var t_full := ms(func(): AI.choose_action(g, h))
|
||||||
AI.time_budget_ms = 150
|
AI.time_budget_ms = 150
|
||||||
@@ -39,5 +39,4 @@ func _initialize() -> void:
|
|||||||
print("KI-Entscheidung, %d Gegner, %d Kandidaten: ohne Budget %.0f ms, mit Budget %.0f ms" % [n, cands, t_full, t_budget])
|
print("KI-Entscheidung, %d Gegner, %d Kandidaten: ohne Budget %.0f ms, mit Budget %.0f ms" % [n, cands, t_full, t_budget])
|
||||||
print("Spielkopie: %.3f ms" % ms(func(): Game.new_battle(r).clone(), 20))
|
print("Spielkopie: %.3f ms" % ms(func(): Game.new_battle(r).clone(), 20))
|
||||||
print("Musik erzeugen (läuft im Hintergrund-Thread): %.0f ms" % ms(func(): Sfx.new()._music()))
|
print("Musik erzeugen (läuft im Hintergrund-Thread): %.0f ms" % ms(func(): Sfx.new()._music()))
|
||||||
print("Snapshot: %.2f ms" % ms(func(): Game.new_battle(r).snapshot(0), 10))
|
|
||||||
quit()
|
quit()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
418
tests/ui_test.gd
418
tests/ui_test.gd
@@ -117,10 +117,11 @@ func screen():
|
|||||||
return main.screen
|
return main.screen
|
||||||
|
|
||||||
|
|
||||||
## Jeder Abschnitt startet im Menü, auch wenn ein vorheriger abgebrochen ist.
|
## Jeder Abschnitt startet im Menü, auch wenn ein vorheriger abgebrochen ist; ohne Startdeck wird
|
||||||
|
## Feuer gewählt.
|
||||||
func ensure_menu() -> void:
|
func ensure_menu() -> void:
|
||||||
if main.net.connected:
|
if screen() is StartDeckScreen:
|
||||||
main.net.leave()
|
await click(find_button("Feuer wählen"))
|
||||||
if TranslationServer.get_locale().begins_with("en"):
|
if TranslationServer.get_locale().begins_with("en"):
|
||||||
main.set_language("de")
|
main.set_language("de")
|
||||||
if not (screen() is MenuScreen):
|
if not (screen() is MenuScreen):
|
||||||
@@ -169,15 +170,11 @@ func _ready() -> void:
|
|||||||
add_child(main)
|
add_child(main)
|
||||||
main.sfx.muted = true # Tests bleiben still
|
main.sfx.muted = true # Tests bleiben still
|
||||||
await frames(3)
|
await frames(3)
|
||||||
var suite := [test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen,
|
var suite := [test_start, test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen,
|
||||||
test_kampagne, test_sammlung, test_sprache, test_kampf_steuerung, test_kampf_elemente_und_verschmelzen, test_kampf_grenzen]
|
test_kampagne, test_sammlung, test_deck, test_sprache, test_kampf_steuerung, test_kampf_elemente_und_verschmelzen, test_kampf_grenzen]
|
||||||
suite += [test_kampf.bind(3), test_kampf_abbrechen]
|
suite += [test_kampf.bind(3), test_kampf_abbrechen]
|
||||||
# Die Netzwerktests starten Bot-Prozesse und warten auf Verbindungen; das dauert Minuten.
|
|
||||||
# Sie laufen nur auf Wunsch: DINGER_NETZ=1 (oder gezielt über DINGER_ONLY).
|
|
||||||
if OS.get_environment("DINGER_NETZ") != "":
|
|
||||||
suite += [test_netzwerk, test_host_wechsel, test_zuschauer]
|
|
||||||
suite.append(test_profil_zuruecksetzen)
|
suite.append(test_profil_zuruecksetzen)
|
||||||
var only := OS.get_environment("DINGER_ONLY") # z. B. DINGER_ONLY=test_host_wechsel,test_zuschauer
|
var only := OS.get_environment("DINGER_ONLY") # z. B. DINGER_ONLY=test_sammlung,test_kampf
|
||||||
if only != "":
|
if only != "":
|
||||||
suite = suite.filter(func(t: Callable): return t.get_method() in only.split(","))
|
suite = suite.filter(func(t: Callable): return t.get_method() in only.split(","))
|
||||||
for t in suite:
|
for t in suite:
|
||||||
@@ -190,6 +187,18 @@ func _ready() -> void:
|
|||||||
get_tree().quit(1 if failed > 0 else 0)
|
get_tree().quit(1 if failed > 0 else 0)
|
||||||
|
|
||||||
|
|
||||||
|
## Erster Start: eines von vier Startdecks wählen (ensure_menu hat schon eins gewählt, darum frisch).
|
||||||
|
func test_start() -> void:
|
||||||
|
main.reset_profile()
|
||||||
|
await frames(2)
|
||||||
|
check(screen() is StartDeckScreen and not main.profile.started(), "neues Profil: Startdeck wählen")
|
||||||
|
await shot("00_startdeck")
|
||||||
|
await click(find_button("Feuer wählen"))
|
||||||
|
var p: Profile = main.profile
|
||||||
|
check(screen() is MenuScreen and p.started() and p.deck.size() == Game.DECK_MIN and p.deck.filter(func(id): return Cards.by_id(id).elements[0] == "Feuer").size() == 25, "Feuer gewählt: 40 Karten, 25 davon Feuer")
|
||||||
|
check(Profile.load_or_new(SAVE).started(), "gespeichert")
|
||||||
|
|
||||||
|
|
||||||
func test_menu() -> void:
|
func test_menu() -> void:
|
||||||
check(screen() is MenuScreen, "Menü sichtbar")
|
check(screen() is MenuScreen, "Menü sichtbar")
|
||||||
check(find_button("Einzelspieler") != null and find_button("Beenden") != null, "Menüknöpfe vorhanden")
|
check(find_button("Einzelspieler") != null and find_button("Beenden") != null, "Menüknöpfe vorhanden")
|
||||||
@@ -213,8 +222,8 @@ func test_booster() -> void:
|
|||||||
await shot("04_booster_offen")
|
await shot("04_booster_offen")
|
||||||
check(Profile.load_or_new(SAVE).coins == main.profile.coins, "Kauf gespeichert")
|
check(Profile.load_or_new(SAVE).coins == main.profile.coins, "Kauf gespeichert")
|
||||||
for res in scr.cards:
|
for res in scr.cards:
|
||||||
if res.upgraded:
|
if res.new or res.upgraded:
|
||||||
check(main.profile.level(res.card.id) == Cards.RARITIES.find(res.rarity), "aufgewertete Karte trägt ihre Stufe")
|
check(main.profile.owns(res.card.id) and main.profile.level(res.card.id) == Cards.RARITIES.find(res.rarity), "neue oder aufgewertete Karte trägt ihre Stufe")
|
||||||
else:
|
else:
|
||||||
check(res.dust > 0, "sonst gibt es Staub")
|
check(res.dust > 0, "sonst gibt es Staub")
|
||||||
check(scr.b_buy.disabled == (main.profile.coins < Profile.BOOSTER_COST), "Kaufknopf folgt dem Münzstand")
|
check(scr.b_buy.disabled == (main.profile.coins < Profile.BOOSTER_COST), "Kaufknopf folgt dem Münzstand")
|
||||||
@@ -255,6 +264,11 @@ func drag_fuse(scr, from: Control, to: Control) -> void:
|
|||||||
m.button_mask = MOUSE_BUTTON_MASK_LEFT
|
m.button_mask = MOUSE_BUTTON_MASK_LEFT
|
||||||
get_viewport().push_input(m)
|
get_viewport().push_input(m)
|
||||||
await frames(1)
|
await frames(1)
|
||||||
|
if scr.drag_ghost == null or scr.drag_target == null:
|
||||||
|
var fo = from.card if from is CardView else from.creature
|
||||||
|
var to_o = to.card if to is CardView else to.creature
|
||||||
|
print("DEBUG drag: ghost=", scr.drag_ghost, " target=", scr.drag_target, " press=", scr.press_target, " dran=", scr.my_turn(),
|
||||||
|
" overlay=", scr.any_overlay(), " mana=", scr.game.human.energy, " kann=", scr.game.can_fuse(scr.game.human, fo, to_o), " from=", from.get_global_rect(), " to=", to.get_global_rect())
|
||||||
check(scr.drag_ghost != null and scr.drag_target != null, "Karte schwebt über einem Verschmelzungsziel")
|
check(scr.drag_ghost != null and scr.drag_target != null, "Karte schwebt über einem Verschmelzungsziel")
|
||||||
var waited := 0
|
var waited := 0
|
||||||
while scr.drag_ghost != null and waited < 240:
|
while scr.drag_ghost != null and waited < 240:
|
||||||
@@ -297,7 +311,7 @@ func human_turn(scr) -> void:
|
|||||||
var mana: int = g.human.energy
|
var mana: int = g.human.energy
|
||||||
var hand: int = g.human.hand.size()
|
var hand: int = g.human.hand.size()
|
||||||
var cost: int = g.draw_cost(g.human)
|
var cost: int = g.draw_cost(g.human)
|
||||||
await click(scr.offer_views[0])
|
await click(scr.deck)
|
||||||
check(g.human.energy == mana - cost and g.human.hand.size() == hand + 1, "gezogen: eine Karte")
|
check(g.human.energy == mana - cost and g.human.hand.size() == hand + 1, "gezogen: eine Karte")
|
||||||
var tried := [] # Wesen ohne lohnendes Ziel (0 Angriff ohne Vorteil) nicht wieder versuchen
|
var tried := [] # Wesen ohne lohnendes Ziel (0 Angriff ohne Vorteil) nicht wieder versuchen
|
||||||
for i in 12:
|
for i in 12:
|
||||||
@@ -329,7 +343,7 @@ func human_turn(scr) -> void:
|
|||||||
await click(target_view(scr, targets))
|
await click(target_view(scr, targets))
|
||||||
check(scr.selected == null and cr.attacks_done > 0, "Angriff ausgeführt")
|
check(scr.selected == null and cr.attacks_done > 0, "Angriff ausgeführt")
|
||||||
if g.winner == "":
|
if g.winner == "":
|
||||||
await click(scr.b_end)
|
await key(KEY_SPACE)
|
||||||
|
|
||||||
|
|
||||||
func start_battle() -> void:
|
func start_battle() -> void:
|
||||||
@@ -385,11 +399,14 @@ func leave_battle() -> void:
|
|||||||
|
|
||||||
func give_up() -> void:
|
func give_up() -> void:
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
await click(find_button("Aufgeben"))
|
await key(KEY_ESCAPE)
|
||||||
check(scr.confirm.visible, "Aufgeben fragt nach")
|
check(scr.options.visible, "Esc öffnet die Optionen")
|
||||||
|
await click(find_button("Aufgeben", scr.options))
|
||||||
|
check(scr.confirm.visible and not scr.options.visible, "Aufgeben fragt nach")
|
||||||
await click(find_button("Weiterspielen"))
|
await click(find_button("Weiterspielen"))
|
||||||
check(not scr.confirm.visible and screen() == scr, "Weiterspielen bleibt im Kampf")
|
check(not scr.confirm.visible and screen() == scr, "Weiterspielen bleibt im Kampf")
|
||||||
await click(find_button("Aufgeben"))
|
await key(KEY_ESCAPE)
|
||||||
|
await click(find_button("Aufgeben", scr.options))
|
||||||
await click(find_button("Ja, aufgeben"))
|
await click(find_button("Ja, aufgeben"))
|
||||||
check(screen() is MenuScreen, "Aufgeben führt ins Menü")
|
check(screen() is MenuScreen, "Aufgeben führt ins Menü")
|
||||||
|
|
||||||
@@ -419,33 +436,75 @@ func test_einstellungen() -> void:
|
|||||||
check(screen() is MenuScreen, "zurück im Menü")
|
check(screen() is MenuScreen, "zurück im Menü")
|
||||||
|
|
||||||
|
|
||||||
|
## Deck: eigene Karten rein und raus, Auffüllen nach Filter, Speichern. Beginnt mit dem frischen Startdeck.
|
||||||
|
func test_deck() -> void:
|
||||||
|
main.profile.choose_start("Feuer")
|
||||||
|
main.save()
|
||||||
|
await click(find_button("Deck"))
|
||||||
|
var scr = screen()
|
||||||
|
check(scr is DeckScreen, "Deck geöffnet")
|
||||||
|
var p: Profile = main.profile
|
||||||
|
check(scr.grid.views().size() == p.collection.size() and scr.count_label.text == "Stapel: %d Karten (%d bis %d)" % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX], "alle eigenen Karten, Zähler stimmt")
|
||||||
|
var views: Array = scr.grid.views()
|
||||||
|
check(views.all(func(v): return not v.dim) and p.deck.size() == Game.DECK_MIN, "nach dem Start sind alle eigenen Karten im Stapel")
|
||||||
|
var first_id: String = views[0].card.id
|
||||||
|
await click(views[0])
|
||||||
|
check(not (first_id in p.deck) and p.deck.size() == Game.DECK_MIN - 1 and scr.grid.views().any(func(v): return v.card.id == first_id and v.dim), "Klick nimmt die Karte aus dem Stapel, sie wird dunkel")
|
||||||
|
var back_view = scr.grid.views().filter(func(v): return v.card.id == first_id)[0]
|
||||||
|
scr.grid.ensure_control_visible(back_view) # sie liegt jetzt hinter den Stapelkarten, außerhalb des Sichtbereichs
|
||||||
|
await frames(2)
|
||||||
|
await click(back_view)
|
||||||
|
check(first_id in p.deck and p.deck.size() == Game.DECK_MIN, "Klick nimmt sie wieder hinein")
|
||||||
|
await shot("19_deck")
|
||||||
|
await click(find_button("Leeren"))
|
||||||
|
check(p.deck.is_empty(), "geleert")
|
||||||
|
await click(find_button("Wasser"))
|
||||||
|
await click(find_button("Auffüllen"))
|
||||||
|
check(p.deck.size() == 5 and p.deck.all(func(id): return Cards.by_id(id).elements[0] == "Wasser"), "Auffüllen hält sich an den Filter: nur die fünf Wasserkarten")
|
||||||
|
await click(find_button("Alle"))
|
||||||
|
await click(find_button("Auffüllen"))
|
||||||
|
check(p.deck.size() == Game.DECK_MIN and Profile.load_or_new(SAVE).deck == p.deck, "ohne Filter alles, gespeichert")
|
||||||
|
await click(find_button("Zurück"))
|
||||||
|
check(screen() is MenuScreen, "zurück im Menü")
|
||||||
|
|
||||||
|
|
||||||
func test_sammlung() -> void:
|
func test_sammlung() -> void:
|
||||||
await click(find_button("Sammlung"))
|
await click(find_button("Sammlung"))
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
check(scr is CollectionScreen, "Sammlung geöffnet")
|
check(scr is CollectionScreen, "Sammlung geöffnet")
|
||||||
var p: Profile = main.profile
|
var p: Profile = main.profile
|
||||||
check(scr.grid.views().size() == Cards.all().size(), "alle Karten gehören dir")
|
check(scr.grid.views().size() == Cards.all().size(), "alle Karten des Spiels sind zu sehen")
|
||||||
|
check(scr.grid.views().slice(0, p.collection.size()).all(func(v): return not v.dim) and scr.grid.views().back().dim, "eigene Karten vorn, fehlende abgedunkelt")
|
||||||
p.dust = 5000
|
p.dust = 5000
|
||||||
scr.search_box.text = "Phönix"
|
scr.search_box.text = "Lodernder Funke"
|
||||||
scr.search_box.text_changed.emit("Phönix")
|
scr.search_box.text_changed.emit("Lodernder Funke")
|
||||||
await frames(1)
|
await frames(1)
|
||||||
var phoenix = scr.grid.views().filter(func(v): return v.card.id == "ff_phoenix")[0]
|
var funke = scr.grid.views().filter(func(v): return v.card.id == "f_2_1")[0]
|
||||||
await click(phoenix)
|
await click(funke)
|
||||||
check(scr.selected.id == "ff_phoenix" and p.rarity("ff_phoenix") == "Common", "Karte gewählt, noch Common")
|
check(scr.selected.id == "f_2_1" and p.rarity("f_2_1") == "Common" and scr.b_upgrade.visible, "eigene Karte gewählt, noch Common")
|
||||||
await click(scr.b_upgrade)
|
await click(scr.b_upgrade)
|
||||||
check(p.level("ff_phoenix") == 1 and p.dust == 5000 - Profile.DUST_UPGRADE["Rare"], "auf Rare aufgewertet")
|
check(p.level("f_2_1") == 1 and p.dust == 5000 - Profile.DUST_UPGRADE["Rare"], "auf Rare aufgewertet")
|
||||||
scr.search_box.text = ""
|
scr.search_box.text = ""
|
||||||
scr.search_box.text_changed.emit("")
|
scr.search_box.text_changed.emit("")
|
||||||
await frames(1)
|
await frames(1)
|
||||||
var front: Array = scr.grid.views().slice(0, p.upgraded_count())
|
var front = scr.grid.views().filter(func(v): return v.card.id == "f_2_1")[0]
|
||||||
check(front.all(func(v): return v.rarity != "Common") and front.any(func(v): return v.card.id == "ff_phoenix" and v.rarity == "Rare"), "aufgewertete Karten stehen vorn in der Sammlung, der Phönix als Rare dabei")
|
check(front.rarity == "Rare" and front.card.attack == 3 and front.card.health == 2 and scr.grid.views()[0].rarity != "Common", "die Rare zeigt +1/+1, aufgewertete stehen vorn: %s %d/%d" % [front.rarity, front.card.attack, front.card.health])
|
||||||
scr.search_box.text = "Phönix"
|
check(Profile.load_or_new(SAVE).level("f_2_1") == 1, "Sammlung gespeichert")
|
||||||
scr.search_box.text_changed.emit("Phönix")
|
scr.search_box.text = "Lodernder Funke"
|
||||||
|
scr.search_box.text_changed.emit("Lodernder Funke")
|
||||||
await frames(1)
|
await frames(1)
|
||||||
check(Profile.load_or_new(SAVE).level("ff_phoenix") == 1, "Sammlung gespeichert")
|
|
||||||
var dust: int = p.dust
|
var dust: int = p.dust
|
||||||
await click(scr.b_dismantle)
|
await click(scr.b_dismantle)
|
||||||
check(p.level("ff_phoenix") == 0 and p.dust == dust + Profile.DUST_DISMANTLE["Rare"], "wieder zerlegt")
|
check(p.level("f_2_1") == 0 and p.dust == dust + Profile.DUST_DISMANTLE["Rare"], "wieder zerlegt")
|
||||||
|
# Herstellen einer fehlenden Karte
|
||||||
|
scr.search_box.text = "Infernalischer Drache"
|
||||||
|
scr.search_box.text_changed.emit("Infernalischer Drache")
|
||||||
|
await frames(1)
|
||||||
|
await click(scr.grid.views().filter(func(v): return v.card.id == "f_5_5")[0])
|
||||||
|
check(scr.b_craft.visible and not scr.b_upgrade.visible, "fehlende Karte: nur Herstellen")
|
||||||
|
dust = p.dust
|
||||||
|
await click(scr.b_craft)
|
||||||
|
check(p.owns("f_5_5") and p.dust == dust - Profile.CRAFT_COST, "hergestellt")
|
||||||
await shot("20_sammlung")
|
await shot("20_sammlung")
|
||||||
await click(find_button("Zurück"))
|
await click(find_button("Zurück"))
|
||||||
check(screen() is MenuScreen, "zurück im Menü")
|
check(screen() is MenuScreen, "zurück im Menü")
|
||||||
@@ -489,7 +548,9 @@ func test_profil_zuruecksetzen() -> void:
|
|||||||
check(not scr.confirm.visible, "abgebrochen")
|
check(not scr.confirm.visible, "abgebrochen")
|
||||||
await click(find_button("Profil zurücksetzen"))
|
await click(find_button("Profil zurücksetzen"))
|
||||||
await click(find_button("Ja, zurücksetzen"))
|
await click(find_button("Ja, zurücksetzen"))
|
||||||
check(screen() is MenuScreen and main.profile.coins == Profile.START_COINS and main.profile.collection.is_empty(), "Profil zurückgesetzt")
|
check(screen() is StartDeckScreen and main.profile.coins == Profile.START_COINS and not main.profile.started(), "Profil zurückgesetzt: Startdeck neu wählen")
|
||||||
|
await click(find_button("Luft wählen"))
|
||||||
|
check(screen() is MenuScreen and main.profile.deck.filter(func(id): return Cards.by_id(id).elements[0] == "Luft").size() == 25, "Luft gewählt")
|
||||||
check(Profile.load_or_new(SAVE).coins == Profile.START_COINS, "zurückgesetzt gespeichert")
|
check(Profile.load_or_new(SAVE).coins == Profile.START_COINS, "zurückgesetzt gespeichert")
|
||||||
|
|
||||||
|
|
||||||
@@ -526,26 +587,26 @@ func test_kampf_steuerung() -> void:
|
|||||||
await wait_for_human(scr)
|
await wait_for_human(scr)
|
||||||
var g: Game = scr.game
|
var g: Game = scr.game
|
||||||
g.human.energy = 20 # die Starthand ist zufällig, hier soll nur die Steuerung geprüft werden
|
g.human.energy = 20 # die Starthand ist zufällig, hier soll nur die Steuerung geprüft werden
|
||||||
g.human.hand = [Cards.by_id("f_wolf").copy()]
|
g.human.hand = [Cards.by_id("f_3_2").copy()]
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
check(scr.hand_views.size() == 1 and scr.draw_enabled and scr.offer_views.size() == Game.OFFER, "eine Handkarte, drei offene auf dem Stapel")
|
check(scr.hand_views.size() == 1 and scr.deck.enabled, "eine Handkarte, Stapel bereit")
|
||||||
# Ziehen: die gewählte offene Karte landet auf der Hand, das erste Ziehen ist frei
|
# Ziehen: eine Karte vom eigenen Stapel, das erste Ziehen ist frei
|
||||||
var mana: int = g.human.energy
|
var mana: int = g.human.energy
|
||||||
var wanted: Cards.Card = g.human.offer[1]
|
var deck_before: int = g.human.deck.size()
|
||||||
await click(scr.offer_views[1])
|
await click(scr.deck)
|
||||||
check(g.human.energy == mana and g.human.hand.size() == 2 and scr.hand_views.size() == 2 and g.human.hand[1].id == wanted.id, "gezogen: die gewählte Karte, frei")
|
check(g.human.energy == mana and g.human.hand.size() == 2 and scr.hand_views.size() == 2 and g.human.deck.size() == deck_before - 1, "gezogen: eine Karte, frei")
|
||||||
check(g.human.offer.size() == Game.OFFER and g.human.offer.all(func(c): return c.cost <= g.human.max_energy) and g.draw_cost(g.human) == Game.DRAW_COST, "eine bezahlbare neue liegt nach, das nächste Ziehen kostet 2")
|
check(g.draw_cost(g.human) == 1 and scr.deck.cost == 1, "das nächste Ziehen kostet 1")
|
||||||
# Ausspielen kostet die Kartenkosten; das Wesen greift noch nicht an
|
# Ausspielen kostet die Kartenkosten; das Wesen greift noch nicht an
|
||||||
var board_before: int = g.human.board.size()
|
var board_before: int = g.human.board.size()
|
||||||
mana = g.human.energy
|
mana = g.human.energy
|
||||||
await key(KEY_1)
|
await key(KEY_1)
|
||||||
check(g.human.board.size() == board_before + 1 and g.human.energy == mana - Cards.by_id("f_wolf").cost, "Wesen per Taste gespielt, Kosten bezahlt")
|
check(g.human.board.size() == board_before + 1 and g.human.energy == mana - Cards.by_id("f_3_2").cost, "Wesen per Taste gespielt, Kosten bezahlt")
|
||||||
check(not g.human.board.back().can_attack(), "frisch gelegt greift es noch nicht an")
|
check(not g.human.board.back().can_attack(), "frisch gelegt greift es noch nicht an")
|
||||||
# Rückgängig: Wesen zurück auf die Hand, Mana zurück
|
# Rückgängig: Wesen zurück auf die Hand, Mana zurück
|
||||||
check(scr.b_undo.visible, "Rückgängig angeboten")
|
check(scr.b_undo.visible, "Rückgängig angeboten")
|
||||||
await click(scr.b_undo)
|
await click(scr.b_undo)
|
||||||
g = scr.game
|
g = scr.game
|
||||||
check(g.human.board.size() == board_before and g.human.hand.any(func(c): return c.id == "f_wolf") and g.human.energy == mana, "Rückgängig stellt Hand und Mana her")
|
check(g.human.board.size() == board_before and g.human.hand.any(func(c): return c.id == "f_3_2") and g.human.energy == mana, "Rückgängig stellt Hand und Mana her")
|
||||||
check(not scr.b_undo.visible and g.log.back() == "Rückgängig.", "Rückgängig verbraucht")
|
check(not scr.b_undo.visible and g.log.back() == "Rückgängig.", "Rückgängig verbraucht")
|
||||||
await key(KEY_1)
|
await key(KEY_1)
|
||||||
check(g.human.board.size() == board_before + 1, "erneut gespielt")
|
check(g.human.board.size() == board_before + 1, "erneut gespielt")
|
||||||
@@ -553,7 +614,7 @@ func test_kampf_steuerung() -> void:
|
|||||||
mana = g.human.energy
|
mana = g.human.energy
|
||||||
var hand_before: int = g.human.hand.size()
|
var hand_before: int = g.human.hand.size()
|
||||||
await key(KEY_Z)
|
await key(KEY_Z)
|
||||||
check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == hand_before + 1, "Z zieht die erste offene Karte für 2")
|
check(g.human.energy == mana - 1 and g.human.hand.size() == hand_before + 1, "Z zieht, das zweite Ziehen für 1")
|
||||||
await shot("22_kampf_steuerung")
|
await shot("22_kampf_steuerung")
|
||||||
# Gamepad: A auf dem Stapel zieht
|
# Gamepad: A auf dem Stapel zieht
|
||||||
await pad(JOY_BUTTON_DPAD_LEFT)
|
await pad(JOY_BUTTON_DPAD_LEFT)
|
||||||
@@ -565,7 +626,7 @@ func test_kampf_steuerung() -> void:
|
|||||||
await key(KEY_SPACE)
|
await key(KEY_SPACE)
|
||||||
check(g.current() != g.human or g.turn >= 2, "Zug per Leertaste beendet")
|
check(g.current() != g.human or g.turn >= 2, "Zug per Leertaste beendet")
|
||||||
await frames(2)
|
await frames(2)
|
||||||
check(scr.b_skip.visible and not scr.b_end.visible, "Überspringen-Knopf während des Gegnerzugs")
|
check(scr.b_skip.visible, "Überspringen-Knopf während des Gegnerzugs")
|
||||||
await click(scr.b_skip)
|
await click(scr.b_skip)
|
||||||
var w := 0
|
var w := 0
|
||||||
while g.current() != g.human and g.winner == "" and w < 120:
|
while g.current() != g.human and g.winner == "" and w < 120:
|
||||||
@@ -574,9 +635,15 @@ func test_kampf_steuerung() -> void:
|
|||||||
check(g.current() == g.human and w < 60, "Gegnerzug übersprungen (%d Bilder)" % w)
|
check(g.current() == g.human and w < 60, "Gegnerzug übersprungen (%d Bilder)" % w)
|
||||||
scr.ai_delay = 0.0
|
scr.ai_delay = 0.0
|
||||||
await key(KEY_ESCAPE)
|
await key(KEY_ESCAPE)
|
||||||
check(scr.confirm.visible, "Esc öffnet Aufgeben")
|
check(scr.options.visible and find_button("Hauptmenü", scr.options) != null and find_button("Beenden", scr.options) != null, "Esc öffnet die Optionen")
|
||||||
await key(KEY_ESCAPE)
|
await key(KEY_ESCAPE)
|
||||||
check(not scr.confirm.visible, "Esc schließt den Dialog")
|
check(not scr.options.visible, "Esc schließt die Optionen")
|
||||||
|
# Einstellungen aus dem Kampf und zurück
|
||||||
|
await key(KEY_ESCAPE)
|
||||||
|
await click(find_button("Einstellungen", scr.options))
|
||||||
|
check(screen() is SettingsScreen and is_instance_valid(scr), "Einstellungen geöffnet, der Kampf bleibt erhalten")
|
||||||
|
await click(find_button("Zurück"))
|
||||||
|
check(screen() == scr and scr.game == g and not scr.options.visible, "zurück im selben Kampf")
|
||||||
await give_up()
|
await give_up()
|
||||||
|
|
||||||
|
|
||||||
@@ -594,21 +661,21 @@ func test_kampf_grenzen() -> void:
|
|||||||
var guard := 0
|
var guard := 0
|
||||||
while g.human.hand.size() < Game.MAX_HAND and guard < 30:
|
while g.human.hand.size() < Game.MAX_HAND and guard < 30:
|
||||||
guard += 1
|
guard += 1
|
||||||
await click(scr.offer_views[0])
|
await click(scr.deck)
|
||||||
check(g.human.hand.size() == Game.MAX_HAND, "Hand auf %d gefüllt" % Game.MAX_HAND)
|
check(g.human.hand.size() == Game.MAX_HAND, "Hand auf %d gefüllt" % Game.MAX_HAND)
|
||||||
check(not scr.draw_enabled, "bei voller Hand lässt sich nichts ziehen")
|
check(not scr.deck.enabled, "bei voller Hand lässt sich nichts ziehen")
|
||||||
await click(scr.offer_views[0])
|
await click(scr.deck)
|
||||||
check(scr.msg_label.text == tr("Deine Hand ist voll"), "Meldung bei voller Hand: " + scr.msg_label.text)
|
check(scr.msg_label.text == tr("Deine Hand ist voll"), "Meldung bei voller Hand: " + scr.msg_label.text)
|
||||||
# Feld voll spielen
|
# Feld voll spielen
|
||||||
guard = 0
|
guard = 0
|
||||||
while g.human.board.size() < Game.MAX_BOARD and guard < 30:
|
while g.human.board.size() < Game.MAX_BOARD and guard < 30:
|
||||||
guard += 1
|
guard += 1
|
||||||
if g.human.hand.is_empty():
|
if g.human.hand.is_empty():
|
||||||
await click(scr.offer_views[0])
|
await click(scr.deck)
|
||||||
await key(KEY_1)
|
await key(KEY_1)
|
||||||
check(g.human.board.size() == Game.MAX_BOARD, "Feld mit %d Wesen voll" % Game.MAX_BOARD)
|
check(g.human.board.size() == Game.MAX_BOARD, "Feld mit %d Wesen voll" % Game.MAX_BOARD)
|
||||||
if g.human.hand.is_empty():
|
if g.human.hand.is_empty():
|
||||||
await click(scr.offer_views[0])
|
await click(scr.deck)
|
||||||
await key(KEY_1)
|
await key(KEY_1)
|
||||||
check(scr.msg_label.text == tr("Dein Feld ist voll"), "Meldung bei vollem Feld: " + scr.msg_label.text)
|
check(scr.msg_label.text == tr("Dein Feld ist voll"), "Meldung bei vollem Feld: " + scr.msg_label.text)
|
||||||
await shot("25_kampf_voll")
|
await shot("25_kampf_voll")
|
||||||
@@ -619,10 +686,10 @@ func test_kampf_grenzen() -> void:
|
|||||||
check(g.human.board.size() == before - 1, "Verschmelzen macht einen Platz frei")
|
check(g.human.board.size() == before - 1, "Verschmelzen macht einen Platz frei")
|
||||||
# Ohne Mana kein Ziehen
|
# Ohne Mana kein Ziehen
|
||||||
g.human.energy = 0
|
g.human.energy = 0
|
||||||
g.human.drew = true
|
g.human.draws = 1
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
await frames(1)
|
await frames(1)
|
||||||
check(not scr.draw_enabled, "ohne Mana kein zweites Ziehen")
|
check(not scr.deck.enabled, "ohne Mana kein zweites Ziehen")
|
||||||
await give_up()
|
await give_up()
|
||||||
|
|
||||||
|
|
||||||
@@ -646,27 +713,27 @@ func test_kampf_elemente_und_verschmelzen() -> void:
|
|||||||
cr.hidden = hidden
|
cr.hidden = hidden
|
||||||
p.board.append(cr)
|
p.board.append(cr)
|
||||||
return cr
|
return cr
|
||||||
var wolf_cr: Game.Creature = put.call(g.human, "f_wolf")
|
var wolf_cr: Game.Creature = put.call(g.human, "f_3_2")
|
||||||
wolf_cr.health = 4 # übersteht die Konter von Falke und Kobold, damit der Test ihn weiter nutzen kann
|
wolf_cr.health = 4 # übersteht die Konter von Falke und Funke, damit der Test ihn weiter nutzen kann
|
||||||
wolf_cr.max_health = 4
|
wolf_cr.max_health = 4
|
||||||
put.call(g.human, "f_asche")
|
put.call(g.human, "f_4_3")
|
||||||
put.call(g.human, "w_reiter")
|
put.call(g.human, "w_3_3")
|
||||||
var falke: Game.Creature = put.call(scr.opponent(), "l_falke")
|
var falke: Game.Creature = put.call(scr.opponent(), "l_3_1")
|
||||||
var secret: Game.Creature = put.call(scr.opponent(), "f_kobold", true)
|
var secret: Game.Creature = put.call(scr.opponent(), "f_2_1", true)
|
||||||
g.human.hand = [Cards.by_id("f_kobold").copy(), Cards.by_id("l_wind").copy()]
|
g.human.hand = [Cards.by_id("f_2_1").copy(), Cards.by_id("l_1_1").copy()]
|
||||||
g.human.energy = 20
|
g.human.energy = 20
|
||||||
g.human.looks = {"w_reiter": 3}
|
g.human.looks = {"w_3_3": 3}
|
||||||
g.apply_auras()
|
g.apply_auras()
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
await frames(2)
|
await frames(2)
|
||||||
check(scr.element_view.text() == "Feuer 2/0 (Doppelschlag) · Wasser 1/0 · Erde 0/0 · Luft 0/0", "Zähler: offene eigene Wesen und ausgespielte Karten, Feuerpaar: " + scr.element_view.text())
|
check(scr.element_view.text() == "Feuer 2/0 (Doppelschlag) · Wasser 1/0", "Zähler: nur Elemente mit Wesen oder gespielten Karten, Feuerpaar: " + scr.element_view.text())
|
||||||
check(scr.opp_element_view.text() == "Feuer 0/0 · Wasser 0/0 · Erde 0/0 · Luft 1/0", "die Leiste des Gegners zählt nur seine offenen Wesen: " + scr.opp_element_view.text())
|
check(scr.opp_element_view.text() == "Luft 1/0", "die Liste des Gegners zählt nur seine offenen Wesen: " + scr.opp_element_view.text())
|
||||||
check(g.human.board[0].keywords() == ["Doppelschlag"] and scr.tiles[secret.uid].secret and scr.hand_views[0].bonus == 0, "Feuerpaar gibt Doppelschlag; fremdes Verdecktes zeigt den Rücken; noch kein Bonus in Sicht")
|
check(g.human.board[0].keywords() == ["Doppelschlag"] and scr.tiles[secret.uid].secret and scr.hand_views[0].bonus == 0, "Feuerpaar gibt Doppelschlag; fremdes Verdecktes zeigt den Rücken; noch kein Bonus in Sicht")
|
||||||
# Rechtsklick auf die Handkarte: Menü mit Verdeckt legen, ohne Verschmelzen
|
# Rechtsklick auf die Handkarte: Menü mit Verdeckt legen, ohne Verschmelzen
|
||||||
await hover(scr.hand_views[0])
|
await hover(scr.hand_views[0])
|
||||||
await right_click_at(scr.hand_views[0].get_global_rect().get_center())
|
await right_click_at(scr.hand_views[0].get_global_rect().get_center())
|
||||||
check(scr.menu_layer.visible and find_button("Verdeckt legen ◆ %d" % g.human.hand[0].cost, scr.menu_layer) != null and find_button("Abwerfen +1 ◆", scr.menu_layer) != null, "Rechtsklick öffnet das Kartenmenü mit Verdeckt legen und Abwerfen")
|
check(scr.menu_layer.visible and find_button("Verdeckt legen ◆ %d" % g.human.hand[0].cost, scr.menu_layer) != null and find_button("Abwerfen +1 ◆", scr.menu_layer) != null, "Rechtsklick öffnet das Kartenmenü mit Verdeckt legen und Abwerfen")
|
||||||
check(find_button("Verschmelzen mit … ◆ 1", scr.menu_layer) == null, "kein Verschmelzen im Rechtsklick-Menü")
|
check(find_button("Verschmelzen mit …", scr.menu_layer) == null, "kein Verschmelzen im Rechtsklick-Menü")
|
||||||
await shot("23_kartenmenue")
|
await shot("23_kartenmenue")
|
||||||
await right_click()
|
await right_click()
|
||||||
check(not scr.menu_layer.visible, "Rechtsklick schließt das Menü")
|
check(not scr.menu_layer.visible, "Rechtsklick schließt das Menü")
|
||||||
@@ -685,16 +752,16 @@ func test_kampf_elemente_und_verschmelzen() -> void:
|
|||||||
while scr.own_views[3].busy:
|
while scr.own_views[3].busy:
|
||||||
await frames(1)
|
await frames(1)
|
||||||
await click(scr.own_views[3])
|
await click(scr.own_views[3])
|
||||||
check(mine_hidden.hidden and scr.msg_label.text == "Feuerkobold kann erst im nächsten Zug aufgedeckt werden", "im Legezug nicht aufzudecken: " + scr.msg_label.text)
|
check(mine_hidden.hidden and scr.msg_label.text == "Lodernder Funke kann erst im nächsten Zug aufgedeckt werden", "im Legezug nicht aufzudecken: " + scr.msg_label.text)
|
||||||
mine_hidden.ready = true
|
mine_hidden.ready = true
|
||||||
await click(scr.own_views[3])
|
await click(scr.own_views[3])
|
||||||
check(not mine_hidden.hidden and scr.element_view.text().begins_with("Feuer 3/1 (Doppelschlag)"), "aufgedeckt: drei offene Feuer, eine ausgespielt: " + scr.element_view.text())
|
check(not mine_hidden.hidden and scr.element_view.text().begins_with("Feuer 3/1 (Doppelschlag)"), "aufgedeckt: drei offene Feuer, eine ausgespielt: " + scr.element_view.text())
|
||||||
check(wolf.power() == 3 and falke.power() == 3 and mine_hidden.can_attack(), "unter 10 Mana kein Bonus, aufgedeckt greift sofort an")
|
check(wolf.power() == 3 and falke.power() == 3 and mine_hidden.can_attack(), "unter fünf Karten kein Bonus, aufgedeckt greift sofort an")
|
||||||
g.human.played["Feuer"] = 10
|
g.human.played["Feuer"] = 5
|
||||||
g.apply_auras()
|
g.apply_auras()
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
await frames(1)
|
await frames(1)
|
||||||
check(scr.element_view.text().begins_with("Feuer 3/10 (Doppelschlag) (+1)") and wolf.power() == 4 and falke.power() == 3, "10 Mana Feuer: +1 Angriff für die eigenen Feuerwesen: " + scr.element_view.text())
|
check(scr.element_view.text().begins_with("Feuer 3/5 (Doppelschlag) (+1/+1)") and wolf.power() == 4 and falke.power() == 3, "fünf Feuerkarten: +1/+1 für die eigenen Feuerwesen: " + scr.element_view.text())
|
||||||
check(scr.hand_views[0].bonus == 0, "die Luftkarte auf der Hand zeigt keinen Bonus")
|
check(scr.hand_views[0].bonus == 0, "die Luftkarte auf der Hand zeigt keinen Bonus")
|
||||||
await shot("23b_vier_feuer")
|
await shot("23b_vier_feuer")
|
||||||
# Angriffsziel: Elementvorteil, verdeckte ohne Anzeige, Held gesperrt
|
# Angriffsziel: Elementvorteil, verdeckte ohne Anzeige, Held gesperrt
|
||||||
@@ -714,220 +781,26 @@ func test_kampf_elemente_und_verschmelzen() -> void:
|
|||||||
mana = g.human.energy
|
mana = g.human.energy
|
||||||
var before: int = g.human.board.size()
|
var before: int = g.human.board.size()
|
||||||
await drag_fuse(scr, scr.own_views[1], scr.own_views[2])
|
await drag_fuse(scr, scr.own_views[1], scr.own_views[2])
|
||||||
check(g.human.board.size() == before - 1 and g.human.board.any(func(c): return c.name == "Dampf") and g.human.energy == mana - 1, "zu Dampf verschmolzen, 1 Mana bezahlt")
|
check(g.human.board.size() == before - 1 and g.human.board.any(func(c): return c.name == "Dampf") and g.human.energy == mana - 3, "zu Dampf verschmolzen, 3 Mana bezahlt (halbe Manasumme)")
|
||||||
var dampf: Game.Creature = g.human.board.filter(func(c): return c.name == "Dampf")[0]
|
var dampf: Game.Creature = g.human.board.filter(func(c): return c.name == "Dampf")[0]
|
||||||
check(scr.tiles[dampf.uid].rarity() == "Ultra Rare" and dampf.card.look == 3, "die Fusion trägt die höhere Stufe (Wellenreiter war Ultra Rare)")
|
check(scr.tiles[dampf.uid].rarity() == "Ultra Rare" and dampf.card.look == 3, "die Fusion trägt die höhere Stufe (der Reiter war Ultra Rare)")
|
||||||
await shot("24_verschmelzen")
|
await shot("24_verschmelzen")
|
||||||
# Verschmelzen auf der Hand per Ziehen
|
# Verschmelzen auf der Hand per Ziehen
|
||||||
g.human.hand = [Cards.by_id("f_kobold").copy(), Cards.by_id("f_kobold").copy(), Cards.by_id("l_wind").copy()]
|
g.human.hand = [Cards.by_id("f_2_1").copy(), Cards.by_id("f_2_1").copy(), Cards.by_id("l_1_1").copy()]
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
await frames(1)
|
await frames(1)
|
||||||
await drag_fuse(scr, scr.hand_views[0], scr.hand_views[1])
|
await drag_fuse(scr, scr.hand_views[0], scr.hand_views[1])
|
||||||
check(g.human.hand.size() == 2 and g.human.hand[0].name == "Inferno" and g.human.hand[0].attack == 3 and g.human.hand[0].health == 2, "zwei Kobolde auf der Hand zu Inferno 3/2")
|
check(g.human.hand.size() == 2 and g.human.hand[0].name == "Inferno" and g.human.hand[0].attack == 3 and g.human.hand[0].health == 2, "zwei Funken auf der Hand zu Inferno 3/2")
|
||||||
# Gamepad-Menü (Y) bietet Verdeckt legen und Verschmelzen mit …
|
# Gamepad-Menü (Y) bietet Verdeckt legen und Verschmelzen mit …
|
||||||
scr.open_menu(g.human.hand[0], Rect2(scr.hand_views[0].position, scr.hand_views[0].size), true)
|
scr.open_menu(g.human.hand[0], Rect2(scr.hand_views[0].position, scr.hand_views[0].size), true)
|
||||||
check(find_button("Verdeckt legen ◆ %d" % g.human.hand[0].cost, scr.menu_layer) != null and g.human.hand[0].cost == 2, "Gamepad-Menü: Verdeckt legen, Inferno kostet 2")
|
check(find_button("Verdeckt legen ◆ %d" % g.human.hand[0].cost, scr.menu_layer) != null and g.human.hand[0].cost == 2, "Gamepad-Menü: Verdeckt legen, Inferno kostet 2")
|
||||||
await click(find_button("Verschmelzen mit … ◆ 2", scr.menu_layer))
|
await click(find_button("Verschmelzen mit …", scr.menu_layer))
|
||||||
check(scr.fuse_first == g.human.hand[0], "Partner wählen")
|
check(scr.fuse_first == g.human.hand[0], "Partner wählen")
|
||||||
await click(scr.hand_views[1])
|
await click(scr.hand_views[1])
|
||||||
check(g.human.hand.size() == 1 and g.human.hand[0].name == "Blitz", "Inferno und Windgeist zu Blitz")
|
check(g.human.hand.size() == 1 and g.human.hand[0].name == "Blitz", "Inferno und Windgeist zu Blitz")
|
||||||
await give_up()
|
await give_up()
|
||||||
|
|
||||||
|
|
||||||
func test_netzwerk() -> void:
|
|
||||||
await click(find_button("Netzwerkspiel"))
|
|
||||||
var scr = screen()
|
|
||||||
check(scr is LobbyScreen, "Lobby geöffnet")
|
|
||||||
await click(find_button("Spiel eröffnen"))
|
|
||||||
check(main.net.is_host and main.net.connected, "Spiel eröffnet")
|
|
||||||
if FileAccess.file_exists("user://bot_quit"):
|
|
||||||
DirAccess.remove_absolute(ProjectSettings.globalize_path("user://bot_quit"))
|
|
||||||
var args := ["--headless", "--path", ProjectSettings.globalize_path("res://"), "tests/net_bot.tscn", "--", "--ip=127.0.0.1", "--name=Bot"]
|
|
||||||
var pid := OS.create_process(OS.get_executable_path(), args)
|
|
||||||
check(pid > 0, "Bot-Prozess gestartet")
|
|
||||||
var wait := 0
|
|
||||||
while main.net.names.size() < 2 and wait < 600:
|
|
||||||
await frames(1)
|
|
||||||
wait += 1
|
|
||||||
if main.net.names.size() < 2:
|
|
||||||
print("HINWEIS: Bot kam nicht, zweiter Versuch")
|
|
||||||
OS.kill(pid)
|
|
||||||
pid = OS.create_process(OS.get_executable_path(), args)
|
|
||||||
while main.net.names.size() < 2 and wait < 1500:
|
|
||||||
await frames(1)
|
|
||||||
wait += 1
|
|
||||||
check(main.net.names.size() == 2, "Bot ist beigetreten (Pfad %s)" % main.net.get_path())
|
|
||||||
await shot("23_lobby")
|
|
||||||
await click(find_button("Start"))
|
|
||||||
var b = screen()
|
|
||||||
check(b is BattleScreen and b.is_online() and not b.is_client(), "Netzwerkkampf als Host")
|
|
||||||
b.ai_delay = 0.0
|
|
||||||
var rounds := 0
|
|
||||||
while b.game != null and b.game.winner == "" and rounds < 2:
|
|
||||||
if b.game.current() == b.perspective:
|
|
||||||
if b.game.current() == b.perspective:
|
|
||||||
await click(b.b_end)
|
|
||||||
var w2 := 0
|
|
||||||
while b.game.current() != b.perspective and b.game.winner == "" and w2 < 900:
|
|
||||||
await frames(1)
|
|
||||||
w2 += 1
|
|
||||||
rounds += 1
|
|
||||||
if b.game.winner != "":
|
|
||||||
print("DEBUG Netz: Sieger=", b.game.winner, " Verlauf=", b.game.log.slice(maxi(0, b.game.log.size() - 12)))
|
|
||||||
check(b.game.turn >= 2 and b.game.winner == "", "Runden gespielt, Partie läuft (Zug %d)" % b.game.turn)
|
|
||||||
check(b.game.log.any(func(l): return l.begins_with("Bot kauft") or l.begins_with("Bot spielt")), "Bot hat gehandelt")
|
|
||||||
await shot("24_netzwerk_kampf")
|
|
||||||
# Bot verlässt geordnet: Platz bleibt frei, Züge werden übersprungen
|
|
||||||
var quit_file := "user://bot_quit"
|
|
||||||
var f := FileAccess.open(quit_file, FileAccess.WRITE)
|
|
||||||
f.store_string("x")
|
|
||||||
f.close()
|
|
||||||
var w3 := 0
|
|
||||||
while main.net.waiting.is_empty() and w3 < 600:
|
|
||||||
await frames(1)
|
|
||||||
w3 += 1
|
|
||||||
check(main.net.waiting.has("Bot") and b.game.log.any(func(l): return "Verbindung verloren" in l), "Platz wird freigehalten")
|
|
||||||
var turn_before: int = b.game.turn
|
|
||||||
for i in 2:
|
|
||||||
if b.game.current() == b.perspective and b.game.winner == "":
|
|
||||||
await click(b.b_end)
|
|
||||||
var w4 := 0
|
|
||||||
while b.game.current() != b.perspective and b.game.winner == "" and w4 < 300:
|
|
||||||
await frames(1)
|
|
||||||
w4 += 1
|
|
||||||
check(b.game.turn >= turn_before + 1 and b.game.log.any(func(l): return "übersprungen" in l), "Züge des fehlenden Spielers werden übersprungen")
|
|
||||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(quit_file))
|
|
||||||
await frames(5)
|
|
||||||
var pid2 := OS.create_process(OS.get_executable_path(), args)
|
|
||||||
var w5 := 0
|
|
||||||
while not main.net.waiting.is_empty() and w5 < 900:
|
|
||||||
await frames(1)
|
|
||||||
w5 += 1
|
|
||||||
check(main.net.waiting.is_empty() and main.net.seats.size() == 2 and b.game.log.any(func(l): return l == "Bot ist zurück."), "Bot wieder verbunden, Platz zurück")
|
|
||||||
var turn2: int = b.game.turn
|
|
||||||
for i in 2:
|
|
||||||
if b.game.current() == b.perspective and b.game.winner == "":
|
|
||||||
await click(b.b_end)
|
|
||||||
var w6 := 0
|
|
||||||
while b.game.current() != b.perspective and b.game.winner == "" and w6 < 900:
|
|
||||||
await frames(1)
|
|
||||||
w6 += 1
|
|
||||||
check(b.game.turn >= turn2 + 1, "Partie läuft nach der Rückkehr weiter")
|
|
||||||
OS.kill(pid2)
|
|
||||||
await leave_battle()
|
|
||||||
check(screen() is MenuScreen and not main.net.connected, "Netzwerk getrennt, zurück im Menü")
|
|
||||||
|
|
||||||
|
|
||||||
func _bot_args(extra: Array) -> Array:
|
|
||||||
return ["--headless", "--path", ProjectSettings.globalize_path("res://"), "tests/net_bot.tscn", "--"] + extra
|
|
||||||
|
|
||||||
|
|
||||||
func _bot_quit(on: bool) -> void:
|
|
||||||
var path := "user://bot_quit"
|
|
||||||
if on:
|
|
||||||
var f := FileAccess.open(path, FileAccess.WRITE)
|
|
||||||
f.store_string("x")
|
|
||||||
f.close()
|
|
||||||
elif FileAccess.file_exists(path):
|
|
||||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
|
||||||
|
|
||||||
|
|
||||||
## Verbindet sich mit dem Bot-Host (mehrere Versuche, bis er lauscht).
|
|
||||||
func _join_bot_host(watch: bool) -> void:
|
|
||||||
await click(find_button("Netzwerkspiel"))
|
|
||||||
var lobby = screen()
|
|
||||||
lobby.ip_box.text = "127.0.0.1"
|
|
||||||
for attempt in 8:
|
|
||||||
await click(find_button("Zuschauen" if watch else "Beitreten"))
|
|
||||||
var w := 0
|
|
||||||
while not main.net.connected and w < 180:
|
|
||||||
await frames(1)
|
|
||||||
w += 1
|
|
||||||
if main.net.connected:
|
|
||||||
return
|
|
||||||
main.net.leave()
|
|
||||||
await frames(30)
|
|
||||||
|
|
||||||
|
|
||||||
## Der Host (ein Bot) verschwindet: der einzige Mitspieler übernimmt und spielt zu Ende.
|
|
||||||
func test_host_wechsel() -> void:
|
|
||||||
_bot_quit(false)
|
|
||||||
var pid := OS.create_process(OS.get_executable_path(), _bot_args(["--host", "--name=BotHost"]))
|
|
||||||
check(pid > 0, "Bot-Host gestartet")
|
|
||||||
await frames(30)
|
|
||||||
await _join_bot_host(false)
|
|
||||||
var w := 0
|
|
||||||
while not (screen() is BattleScreen) and w < 900:
|
|
||||||
await frames(1)
|
|
||||||
w += 1
|
|
||||||
var b = screen()
|
|
||||||
check(b is BattleScreen and b.is_client(), "als Mitspieler beim Bot-Host im Kampf")
|
|
||||||
w = 0
|
|
||||||
while (b.game == null or main.net.backup.is_empty()) and w < 600:
|
|
||||||
await frames(1)
|
|
||||||
w += 1
|
|
||||||
check(b.game != null and main.net.i_am_successor and not main.net.backup.is_empty(), "Nachfolger kennt den vollen Spielstand")
|
|
||||||
main.net.rejoin_seconds = 3.0
|
|
||||||
_bot_quit(true)
|
|
||||||
w = 0
|
|
||||||
while not main.net.is_host and w < 900:
|
|
||||||
await frames(1)
|
|
||||||
w += 1
|
|
||||||
check(main.net.is_host and b.game == main.net.game and screen() == b and b.game.log.any(func(l): return "übernimmt als Host" in l),
|
|
||||||
"Host-Wechsel: Mitspieler führt das Spiel weiter")
|
|
||||||
check(main.net.waiting.has("BotHost"), "Platz des alten Hosts bleibt zunächst frei")
|
|
||||||
await shot("34_host_wechsel")
|
|
||||||
# Der alte Host kehrt nicht zurück: ein Zug wird übersprungen, nach der Frist scheidet er aus
|
|
||||||
b.ai_delay = 0.0
|
|
||||||
if b.game.current() == b.perspective:
|
|
||||||
await click(b.b_end)
|
|
||||||
w = 0
|
|
||||||
while b.game.winner == "" and w < 900:
|
|
||||||
await frames(1)
|
|
||||||
w += 1
|
|
||||||
check(b.game.log.any(func(l): return "übersprungen" in l), "Zug des fehlenden Hosts übersprungen")
|
|
||||||
check(b.game.winner != "" and b.game.winner_player == b.perspective and b.game.log.any(func(l): return "kehrt nicht zurück" in l),
|
|
||||||
"nach der Frist scheidet der alte Host aus, Partie gewonnen (Sieger '%s', Zug %d)" % [b.game.winner, b.game.turn])
|
|
||||||
await leave_battle()
|
|
||||||
_bot_quit(false)
|
|
||||||
check(screen() is MenuScreen and not main.net.connected, "nach dem Host-Wechsel zurück im Menü")
|
|
||||||
|
|
||||||
|
|
||||||
## Zuschauer: sieht die Partie zweier Bots ohne Hände und ohne eigene Züge.
|
|
||||||
func test_zuschauer() -> void:
|
|
||||||
_bot_quit(false)
|
|
||||||
var pid_h := OS.create_process(OS.get_executable_path(), _bot_args(["--host", "--name=BotHost"]))
|
|
||||||
await frames(30)
|
|
||||||
var pid_c := OS.create_process(OS.get_executable_path(), _bot_args(["--ip=127.0.0.1", "--name=Bot"]))
|
|
||||||
check(pid_h > 0 and pid_c > 0, "zwei Bots gestartet")
|
|
||||||
await _join_bot_host(true)
|
|
||||||
var w := 0
|
|
||||||
while not (screen() is BattleScreen) and w < 1200:
|
|
||||||
await frames(1)
|
|
||||||
w += 1
|
|
||||||
var b = screen()
|
|
||||||
check(b is BattleScreen and b.spectating(), "als Zuschauer im Kampf")
|
|
||||||
w = 0
|
|
||||||
while (b.game == null or b.game.turn < 2) and w < 900:
|
|
||||||
await frames(1)
|
|
||||||
w += 1
|
|
||||||
check(b.game != null and b.game.turn >= 2 and b.hand_views.is_empty() and not b.b_end.visible,
|
|
||||||
"Zuschauer sieht die Partie ohne Hände und ohne Züge (Zug %d)" % (b.game.turn if b.game != null else 0))
|
|
||||||
await shot("35_zuschauer")
|
|
||||||
check(not b.b_end.visible and not b.b_undo.visible, "Zuschauer hat keine Zugknöpfe")
|
|
||||||
if screen() is BattleScreen: # die Bots spielen schnell, die Partie kann schon vorbei sein
|
|
||||||
await leave_battle()
|
|
||||||
else:
|
|
||||||
var w2 := 0
|
|
||||||
while not (screen() is MenuScreen) and w2 < 300:
|
|
||||||
await frames(1)
|
|
||||||
w2 += 1
|
|
||||||
check(screen() is MenuScreen and not main.net.connected, "Zuschauer zurück im Menü")
|
|
||||||
_bot_quit(true)
|
|
||||||
await frames(60)
|
|
||||||
_bot_quit(false)
|
|
||||||
|
|
||||||
|
|
||||||
func test_kampagne() -> void:
|
func test_kampagne() -> void:
|
||||||
await click(find_button("Kampagne"))
|
await click(find_button("Kampagne"))
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
@@ -978,7 +851,7 @@ func test_kampf(shot_turn: int) -> void:
|
|||||||
scr.smart_ai = false
|
scr.smart_ai = false
|
||||||
g.opponent.life = 1
|
g.opponent.life = 1
|
||||||
for i in 3:
|
for i in 3:
|
||||||
var drache := Game.Creature.new(Cards.by_id("f_drache").copy(), g.human, 9100 + i)
|
var drache := Game.Creature.new(Cards.by_id("f_5_5").copy(), g.human, 9100 + i)
|
||||||
drache.ready = true
|
drache.ready = true
|
||||||
g.human.board.append(drache)
|
g.human.board.append(drache)
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
@@ -1015,13 +888,13 @@ func test_kampf_abbrechen() -> void:
|
|||||||
check(g.opponent != null and g.human.max_energy == Game.START_MANA, "Duell läuft")
|
check(g.opponent != null and g.human.max_energy == Game.START_MANA, "Duell läuft")
|
||||||
# Kartenvorschau: aufgewertete Karten zeigen ihre Seltenheit
|
# Kartenvorschau: aufgewertete Karten zeigen ihre Seltenheit
|
||||||
g.human.energy = 10
|
g.human.energy = 10
|
||||||
var wolf := Game.Creature.new(Cards.by_id("f_wolf"), g.human, 9000)
|
var wolf := Game.Creature.new(Cards.by_id("f_3_2"), g.human, 9000)
|
||||||
wolf.ready = true
|
wolf.ready = true
|
||||||
g.human.board.append(wolf)
|
g.human.board.append(wolf)
|
||||||
g.human.hand.append(Cards.by_id("w_reiter"))
|
g.human.hand.append(Cards.by_id("w_3_3"))
|
||||||
g.human.looks["w_reiter"] = Cards.RARITIES.find("Super Rare")
|
g.human.looks["w_3_3"] = Cards.RARITIES.find("Super Rare")
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
var reiter_view = scr.hand_views.filter(func(v): return v.card.id == "w_reiter")[0]
|
var reiter_view = scr.hand_views.filter(func(v): return v.card.id == "w_3_3")[0]
|
||||||
check(reiter_view.rarity == "Super Rare", "Handkarte trägt die eigene Stufe")
|
check(reiter_view.rarity == "Super Rare", "Handkarte trägt die eigene Stufe")
|
||||||
await hover(scr.element_view) # erst weg von der Hand, damit das Betreten sicher ausgelöst wird
|
await hover(scr.element_view) # erst weg von der Hand, damit das Betreten sicher ausgelöst wird
|
||||||
await hover(reiter_view)
|
await hover(reiter_view)
|
||||||
@@ -1037,13 +910,14 @@ func test_kampf_abbrechen() -> void:
|
|||||||
print("DEBUG Hover: overlay=", scr.any_overlay(), " hand=", scr.hand_views.size(),
|
print("DEBUG Hover: overlay=", scr.any_overlay(), " hand=", scr.hand_views.size(),
|
||||||
" dran=", g.current().name, " Sieger=", g.winner, " hovered=", get_viewport().gui_get_hovered_control())
|
" dran=", g.current().name, " Sieger=", g.winner, " hovered=", get_viewport().gui_get_hovered_control())
|
||||||
check(scr.preview_holder.get_child_count() > 0, "Vorschau bei Hover")
|
check(scr.preview_holder.get_child_count() > 0, "Vorschau bei Hover")
|
||||||
await click(scr.b_log)
|
await key(KEY_ESCAPE)
|
||||||
|
await click(find_button("Verlauf", scr.options))
|
||||||
check(scr.log_overlay.visible and scr.any_overlay(), "Verlauf öffnet als Überlagerung")
|
check(scr.log_overlay.visible and scr.any_overlay(), "Verlauf öffnet als Überlagerung")
|
||||||
await key(KEY_ESCAPE)
|
await key(KEY_ESCAPE)
|
||||||
check(not scr.log_overlay.visible, "Esc schließt den Verlauf")
|
check(not scr.log_overlay.visible, "Esc schließt den Verlauf")
|
||||||
# Runden laufen lassen, damit die Gegner ihre Züge machen
|
# Runden laufen lassen, damit die Gegner ihre Züge machen
|
||||||
for round in 4:
|
for round in 4:
|
||||||
await click(scr.b_end)
|
await key(KEY_SPACE)
|
||||||
var wait := 0
|
var wait := 0
|
||||||
while g.current() != g.human and g.winner == "" and wait < 600:
|
while g.current() != g.human and g.winner == "" and wait < 600:
|
||||||
await frames(1)
|
await frames(1)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Dinger,Dinger
|
|||||||
Ein Kartenspiel: Duell um das Feld,A card game: duel for the field
|
Ein Kartenspiel: Duell um das Feld,A card game: duel for the field
|
||||||
Einzelspieler,Single player
|
Einzelspieler,Single player
|
||||||
Kampagne,Campaign
|
Kampagne,Campaign
|
||||||
Netzwerkspiel,Network game
|
Deck,Deck
|
||||||
Booster öffnen,Open boosters
|
Booster öffnen,Open boosters
|
||||||
Einstellungen,Settings
|
Einstellungen,Settings
|
||||||
Beenden,Quit
|
Beenden,Quit
|
||||||
@@ -81,7 +81,7 @@ Starten,Start
|
|||||||
Sammlung,Collection
|
Sammlung,Collection
|
||||||
Verschmelzen,Fuse
|
Verschmelzen,Fuse
|
||||||
Nur eigene Wesen,Only your own creatures
|
Nur eigene Wesen,Only your own creatures
|
||||||
Aufgewertet: %d von %d Karten · Kampagne: %d/%d,Upgraded: %d of %d cards · Campaign: %d/%d
|
Sammlung: %d von %d Karten · Kampagne: %d/%d,Collection: %d of %d cards · Campaign: %d/%d
|
||||||
%s ist jetzt %s,%s is now %s
|
%s ist jetzt %s,%s is now %s
|
||||||
Alle Kapitel abgeschlossen,All chapters completed
|
Alle Kapitel abgeschlossen,All chapters completed
|
||||||
Elemente,Elements
|
Elemente,Elements
|
||||||
@@ -141,7 +141,7 @@ Muss zuerst angegriffen werden,Must be attacked first
|
|||||||
Greift sofort an,Attacks right away
|
Greift sofort an,Attacks right away
|
||||||
Heilt sich am Zugende ganz,Heals fully at the end of the turn
|
Heilt sich am Zugende ganz,Heals fully at the end of the turn
|
||||||
Wesen greifen sofort an,Creatures attack right away
|
Wesen greifen sofort an,Creatures attack right away
|
||||||
Verschmelzen mit … ◆ %d,Fuse with … ◆ %d
|
Verschmelzen mit …,Fuse with …
|
||||||
Ziel wählen,Pick the partner
|
Ziel wählen,Pick the partner
|
||||||
Erst die Wesen mit Spott,Taunting creatures first
|
Erst die Wesen mit Spott,Taunting creatures first
|
||||||
%s kann erst im nächsten Zug aufgedeckt werden,%s can be revealed next turn
|
%s kann erst im nächsten Zug aufgedeckt werden,%s can be revealed next turn
|
||||||
@@ -153,3 +153,17 @@ stirbt,dies
|
|||||||
Abwerfen +%d ◆,Discard +%d ◆
|
Abwerfen +%d ◆,Discard +%d ◆
|
||||||
Überspringen,Skip
|
Überspringen,Skip
|
||||||
Zug %d,Turn %d
|
Zug %d,Turn %d
|
||||||
|
Stapel: %d Karten (%d bis %d),Deck: %d cards (%d to %d)
|
||||||
|
Herstellen (%d Staub),Craft (%d dust)
|
||||||
|
Nicht in der Sammlung,Not in the collection
|
||||||
|
%s hergestellt,%s crafted
|
||||||
|
Neu!,New!
|
||||||
|
Auffüllen,Fill up
|
||||||
|
Leeren,Clear
|
||||||
|
Dein Stapel ist leer,Your deck is empty
|
||||||
|
Klick nimmt eine Karte in den Stapel oder heraus. Auffüllen nimmt die teuersten Karten des Filters.,Click adds a card to the deck or removes it. Fill up takes the most expensive cards of the filter.
|
||||||
|
Wähle dein Startdeck,Choose your starting deck
|
||||||
|
"25 Karten des Elements, dazu je fünf der anderen. Booster und Werkstatt bringen später den Rest.","25 cards of the element plus five of each other one. Boosters and the workshop bring the rest later."
|
||||||
|
%s wählen,Choose %s
|
||||||
|
Optionen,Options
|
||||||
|
Hauptmenü,Main menu
|
||||||
|
|||||||
|
Binary file not shown.
Reference in New Issue
Block a user