update
Tests / headless (push) Waiting to run
Tests / fenster-schnell (push) Waiting to run
Tests / fenster-voll (push) Skipped
Tests / balance (push) Skipped

This commit is contained in:
team3 committed 2026-10-15 17:16:01 +02:00
1 parent 1d563b9eec
commit 04fe9716e8
22 files changed
+763 -150

No files matched your search

+6 -5
View File
@@ -21,9 +21,9 @@ Der Spielstand liegt unter `user://spielstand.json`
## Spielsysteme ## Spielsysteme
- 550 Karten in `data/karten.json`: je Element und neutral alle Werte von 0/1 bis 10/10, die ID verrät sie (`f_3_2` = Feuer 3/2, `n_…` neutral), Namen aus Eigenschaft und Wesen; Kombos sind die Namen verschmolzener Wesen. Neutrale Karten sind nie im Vorteil oder Nachteil, bilden kein Paar, haben aber ihren eigenen Zähler (+1/+1 je drei); verschmolzen mit einem Element (Feuerritter …) bleibt dessen Vorteil, die Schwäche fällt weg - 550 Karten in `data/karten.json`: je Element und neutral alle Werte von 0/1 bis 10/10, die ID verrät sie (`f_3_2` = Feuer 3/2, `n_…` neutral), Namen aus Eigenschaft und Wesen; Kombos sind die Namen verschmolzener Wesen. Neutrale Karten sind nie im Vorteil oder Nachteil, bilden kein Paar, haben aber ihren eigenen Zähler (+1/+1 je drei); verschmolzen mit einem Element (Feuerritter …) bleibt dessen Vorteil, die Schwäche fällt weg
- 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) - Karten haben nur Element, Angriff und Leben; die Kosten sind zwei Drittel Angriff plus ein Drittel Leben, abgerundet (0/2 kostet nichts)
- Mana: 1 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen und Legen - Mana: 1 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen und Legen
- Jeder spielt mit seinem eigenen Stapel aus 30 bis 60 Karten (Menü „Deck“: eigene Karten rein oder raus, mit Element- und Manafilter, Manakurve und Elementanteilen; „Auffüllen“ nimmt die wertvollsten des gewählten Filters, bei gleichem Wert die teuersten; „Stärke“ ist der mittlere Kartenwert: ⌊(2·Angriff + Leben)/3⌋ − Kosten + 1, mal 100, also 100 für Grundkarten bis 500 für Ultimate Rare). Zu Beginn die obersten fünf Karten des gemischten Stapels. 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) - Jeder spielt mit seinem eigenen Stapel aus 30 bis 60 Karten (Menü „Deck“: eigene Karten rein oder raus, mit Element- und Manafilter, Manakurve und Elementanteilen; „Auffüllen“ nimmt die wertvollsten des gewählten Filters, bei gleichem Wert die teuersten; „Stärke“ ist der mittlere Kartenwert: faire Kosten (⌊(2·Angriff + Leben)/3⌋ + Eigenschaften + weitere Elemente wie in der Werkstatt) − Kosten + 1, mal 100, plus 100 je Eigenschaft; also 100 für Grundkarten bis 500 für Ultimate Rare, Eigenbauten 100 je Eigenschaft mehr, Fusionen 100 für das zweite Element). Zu Beginn die obersten fünf Karten des gemischten Stapels. 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; 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; aufdecken ist kostenlos, frühestens im nächsten Zug, danach greift es sofort an; ein Angriff deckt es auf
- Man startet mit allen 110 neutralen Karten in der Sammlung, der Stapel sind 30 davon nach Manakurve (2 zu 0 Mana, 5 zu 1, 6 zu 2, 6 zu 3, 5 zu 4, 3 zu 5, 2 zu 6, 1 zu 7); die Elemente schaltet die Kampagne frei: mit jedem offenen Kapitel gibt es den Booster seines Elements, die Werkstatt bringt den Rest; Sammlung, Deckfilter und Booster zeigen nur Elemente offener Kapitel. 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 - Man startet mit allen 110 neutralen Karten in der Sammlung, der Stapel sind 30 davon nach Manakurve (2 zu 0 Mana, 5 zu 1, 6 zu 2, 6 zu 3, 5 zu 4, 3 zu 5, 2 zu 6, 1 zu 7); die Elemente schaltet die Kampagne frei: mit jedem offenen Kapitel gibt es den Booster seines Elements, die Werkstatt bringt den Rest; Sammlung, Deckfilter und Booster zeigen nur Elemente offener Kapitel. 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
@@ -34,11 +34,12 @@ Der Spielstand liegt unter `user://spielstand.json`
- Verschmelzen: eine Handkarte oder ein eigenes Wesen auf eine andere ziehen und loslassen, auch gemischt; das Ergebnis erscheint über dem Ziel, darunter „Verschmelzen“ oder „Abbrechen“ (Gamepad: Y, „Verschmelzen mit …“). Es kostet nichts; Wesen verlassen das Brett, die neue Karte kommt unverletzt auf die Hand und wird wie jede andere ausgespielt. Je Wert die aufgerundete Hälfte der Grundwertsumme plus eins, Kosten daraus wie bei jeder Karte, dann die höhere Stufe der Eltern mit ihrem +1/+1; Elemente und Eigenschaften beider; drei oder mehr Elemente ergeben eine Chimäre - Verschmelzen: eine Handkarte oder ein eigenes Wesen auf eine andere ziehen und loslassen, auch gemischt; das Ergebnis erscheint über dem Ziel, darunter „Verschmelzen“ oder „Abbrechen“ (Gamepad: Y, „Verschmelzen mit …“). Es kostet nichts; Wesen verlassen das Brett, die neue Karte kommt unverletzt auf die Hand und wird wie jede andere ausgespielt. Je Wert die aufgerundete Hälfte der Grundwertsumme plus eins, Kosten daraus wie bei jeder Karte, dann die höhere Stufe der Eltern mit ihrem +1/+1; Elemente und Eigenschaften beider; drei oder mehr Elemente ergeben eine Chimäre
- Duell mit ausgeloster Zugreihenfolge, beide beginnen mit 1 Mana; keine Grenze für Handkarten, das Feld fasst 5 Wesen - Duell mit ausgeloster Zugreihenfolge, beide beginnen mit 1 Mana; keine Grenze für Handkarten, das Feld fasst 5 Wesen
- Münzen nach jedem Kampf: 5 je eigenem Zug, 5 je ausgespielter Karte, 5 je Zug unter 15 Sekunden; ein Sieg verdoppelt alles; das Ergebnisbild schlüsselt es auf. In der Kampagne ohne Schwierigkeitsfaktor, der erste Sieg einer Stufe bringt ihre Belohnung obendrauf - Münzen nach jedem Kampf: 5 je eigenem Zug, 5 je ausgespielter Karte, 5 je Zug unter 15 Sekunden; ein Sieg verdoppelt alles; das Ergebnisbild schlüsselt es auf. In der Kampagne ohne Schwierigkeitsfaktor, der erste Sieg einer Stufe bringt ihre Belohnung obendrauf
- Erfahrung und Skilltree (Menü „Skilltree“): jede verdiente Münze zählt als Erfahrung; Level n braucht 50·n·(n+1) EP (100, 300, 600 …), jedes Level bringt einen Skillpunkt, Münzen kaufen keine Punkte. Drei Äste: Sammeln (Packglück: Common −8, die höheren Stufen +3/+2/+2/+1 Prozentpunkte je Stufe, bis 3; Staubsammler: +25 % Staub je Stufe, bis 3), Wirtschaft (Münzsegen: +10 % Münzen je Stufe, bis 3; Händler: Booster 10 % billiger je Stufe, bis 2), Kampf (Freies Ziehen: auch das zweite Ziehen je Zug gratis; Starker Start: 3 statt 1 Mana im ersten Zug; beide wirken wie die gleichnamigen Merkmale). Jede Skillkarte zeigt die Wirkung jetzt und auf der nächsten Stufe in Zahlen - Erfahrung und Skilltree (Menü „Skilltree“): jede verdiente Münze zählt als Erfahrung; Level n braucht 50·n·(n+1) EP (100, 300, 600 …), jedes Level bringt einen Skillpunkt, Münzen kaufen keine Punkte. Vier Äste als Pfade von oben nach unten, der nächste Skill öffnet, sobald der davor eine Stufe hat: Glück (Packglück: Common −8, die höheren Stufen +3/+2/+2/+1 Prozentpunkte je Stufe, bis 3; Garantie: jeder Booster bringt mindestens eine Super Rare statt Rare), Wirtschaft (Händler: Booster 10 % billiger je Stufe, bis 3; Münzsegen: +10 % Münzen je Stufe, bis 3; Staubsammler: +25 % Staub je Stufe, bis 3), Kraft (Zäh: +1 Leben je Stufe auf jeder eigenen Karte im Kampf, bis 3; Stark: +1 Angriff je Stufe, bis 3; beides liegt wie die Stufe auf den Grundwerten, Fusionen rechnen ohne und bekommen es danach wieder), Weisheit (Freies Ziehen: auch das zweite Ziehen je Zug gratis; Starker Start: 3 statt 1 Mana im ersten Zug; Sparsam: Karten ab 7/5/3 Mana kosten 1 weniger, auch Fusionen). 24 Stufen insgesamt; jeder Knoten zeigt die Wirkung jetzt und auf der nächsten Stufe in Zahlen, „Lernen“ nur wenn lernbar, fertige zeigen „Gelernt“
- Cheats (kleiner Knopf unten rechts im Hauptmenü): Münzen, Staub, Level, Skills, Kapitel und Sammlung direkt setzen, zum Ausprobieren - Cheats (kleiner Knopf unten rechts im Hauptmenü): Münzen, Staub, Level, Skills, Kapitel, Werkstatt und Sammlung direkt setzen, zum Ausprobieren
- Werkstatt (Menü, nach dem Boss von Kapitel 5): eigene Karten aus Staub bauen: beliebig viele Elemente, Angriff 0 bis 10, Leben 1 bis 10, beliebig viele Eigenschaften, eigener Name; Kosten = ⌊(2·Angriff + Leben)/3⌋ + je Eigenschaft (Spott 1, Flink ⌊Angriff/3⌋, Regeneration ⌊Leben/3⌋, Doppelschlag ⌊2·Angriff/3⌋, mindestens 1) + weitere Elemente (das zweite 1, das dritte 2, das vierte 3 …), ohne Obergrenze; Preis = doppelter Kartenwert + 100 je weiterem Element in Staub; die Karte kommt als Common in Sammlung und Stapel (IDs `c_n`) und lässt sich wie jede andere aufwerten
- Schwierigkeit Leicht/Normal/Schwer (Einzelspieler): der Gegner spielt 30 Karten eines Elements aus den offenen Kapiteln nach Manakurve, seine Karten eine Stufe unter deiner Deckstärke, auf ihr oder eine darüber (Stärke 100 → Common … 500 → Ultimate Rare); Leicht mit einfacher KI, Schwer mit einem zufälligen Merkmal; 30 Leben wie du; Münzen 1/1,5/2-fach - Schwierigkeit Leicht/Normal/Schwer (Einzelspieler): der Gegner spielt 30 Karten eines Elements aus den offenen Kapiteln nach Manakurve, seine Karten eine Stufe unter deiner Deckstärke, auf ihr oder eine darüber (Stärke 100 → Common … 500 → Ultimate Rare); Leicht mit einfacher KI, Schwer mit einem zufälligen Merkmal; 30 Leben wie du; Münzen 1/1,5/2-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: fünf Kapitel mit je zehn Magiern, der zehnte ist der Boss; Kapitel 1 spielt neutrale Karten, Kapitel 2 bis 5 Feuer, Wasser, Luft, Erde; jeder Gegner spielt 30 Karten seines Elements nach Manakurve, der Kostendeckel steigt je Stufe von 3 auf 10 (Boss), in Kapitel 1 und 2 Common, dann je Kapitel eine Stufe höher (Kapitel 5 Ultra Rare); ein Merkmal je Gegner, ab Kapitel 3 zwei, in Kapitel 5 drei; eigenes Merkmal nach Kapitel 1 - Kampagne: zehn Kapitel mit je zehn Magiern, der zehnte ist der Boss; Kapitel 1 spielt neutrale Karten, Kapitel 2 bis 5 Feuer, Wasser, Luft, Erde; Kapitel 6 bis 10 wiederholen die Elemente mit Effektkarten (Titel „II“): jede Gegnerkarte trägt in Kapitel 6 und 7 eine, ab 8 zwei Eigenschaften, Kosten wie in der Werkstatt; jeder Gegner spielt 30 Karten seines Elements nach Manakurve, der Kostendeckel steigt je Stufe von 3 auf 10 (Boss), Kapitel 1 und 2 Common, dann je Kapitel eine Stufe höher bis Ultra Rare (Kapitel 5 bis 9), Kapitel 10 Ultimate Rare; ein Merkmal je Gegner, ab Kapitel 3 zwei, ab Kapitel 5 drei; eigenes Merkmal nach Kapitel 1, Werkstatt nach Kapitel 5
- 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
+5 -4
View File
@@ -8,16 +8,17 @@
- Duell: Spieler gegen einen KI-Gegner, beide mit gleichen Regeln - Duell: Spieler gegen einen KI-Gegner, beide mit gleichen Regeln
- Einzelspieler-Knopf führt zur Wahl der Schwierigkeit: der Gegner spielt ein Element aus den offenen Kapiteln, Karten eine Stufe unter deiner Deckstärke / auf ihr / eine darüber; Leicht einfache KI, Schwer ein zufälliges Merkmal; Münzen ×1/×1,5/×2 - Einzelspieler-Knopf führt zur Wahl der Schwierigkeit: der Gegner spielt ein Element aus den offenen Kapiteln, Karten eine Stufe unter deiner Deckstärke / auf ihr / eine darüber; Leicht einfache KI, Schwer ein zufälliges Merkmal; Münzen ×1/×1,5/×2
- Münzen nach jedem Kampf: 5 je eigenem Zug, 5 je ausgespielter Karte, 5 je Zug unter 15 s; ein Sieg verdoppelt; Kampagne ohne Faktor, der erste Sieg einer Stufe bringt ihre Belohnung obendrauf - Münzen nach jedem Kampf: 5 je eigenem Zug, 5 je ausgespielter Karte, 5 je Zug unter 15 s; ein Sieg verdoppelt; Kampagne ohne Faktor, der erste Sieg einer Stufe bringt ihre Belohnung obendrauf
- Erfahrung = verdiente Münzen; Level n braucht 50·n·(n+1) EP, je Level ein Skillpunkt (nicht kaufbar); Skilltree mit drei Ästen: Sammeln (Packglück, Staubsammler), Wirtschaft (Münzsegen, Händler), Kampf (Freies Ziehen, Starker Start als Merkmale) - Erfahrung = verdiente Münzen; Level n braucht 50·n·(n+1) EP, je Level ein Skillpunkt (nicht kaufbar); Skilltree mit vier Ästen als Pfade (der nächste Skill braucht eine Stufe im vorherigen): Glück (Packglück, Garantie: Super Rare je Booster), Wirtschaft (Händler, Münzsegen, Staubsammler), Kraft (Zäh, Stark: +1 Leben/Angriff je Stufe auf eigenen Karten im Kampf), Weisheit (Freies Ziehen, Starker Start, Sparsam: Karten ab 7/5/3 Mana 1 billiger)
- Kampagne: fünf Kapitel mit je zehn Magiern, der zehnte ist der Boss; Kapitel 1 neutral, dann Feuer, Wasser, Luft, Erde; Gegner spielen 30 Karten ihres Elements nach Manakurve, Kostendeckel je Stufe 3 bis 10, Kapitel 1 und 2 Common, dann je Kapitel eine Stufe höher (Kapitel 5 Ultra Rare); ein offenes Kapitel schaltet den Booster seines Elements frei (neutral 100 Münzen, Element 200) - Kampagne: zehn Kapitel mit je zehn Magiern, der zehnte ist der Boss; Kapitel 1 neutral, dann Feuer, Wasser, Luft, Erde; Kapitel 6 bis 10 dieselben Elemente mit Effektkarten (6 und 7 eine Eigenschaft, ab 8 zwei); Gegner spielen 30 Karten ihres Elements nach Manakurve, Kostendeckel je Stufe 3 bis 10, Kapitel 1 und 2 Common, dann je Kapitel eine Stufe höher bis Ultra Rare, Kapitel 10 Ultimate Rare; ein offenes Kapitel schaltet den Booster seines Elements frei (neutral 100 Münzen, Element 200); der Boss von Kapitel 5 öffnet die Werkstatt
- Werkstatt: eigene Karten aus Staub: beliebig viele Elemente, Angriff 0 bis 10, Leben 1 bis 10, beliebig viele Eigenschaften, Name; Kosten = Grundkosten + je Eigenschaft (Spott 1, Flink Angriff/3, Regeneration Leben/3, Doppelschlag 2·Angriff/3, abgerundet, mindestens 1) + weitere Elemente (1, 2, 3 …), ohne Obergrenze; Preis = doppelter Kartenwert + 100 je weiterem Element; die Karte kommt als Common in Sammlung und Stapel
## Sammeln ## Sammeln
- Man startet mit allen 110 neutralen Karten in der Sammlung, 30 davon nach Manakurve im Stapel; die Elemente kommen über die Kampagne und ihre Booster; alle Karten sammeln ist das lange Ziel - Man startet mit allen 110 neutralen Karten in der Sammlung, 30 davon nach Manakurve im Stapel; die Elemente kommen über die Kampagne und ihre Booster; alle Karten sammeln ist das lange Ziel
- 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 - 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 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) - 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)
- 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 - 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
- Jeder hat sein Deck: 30 bis 60 eigene Karten, im Menü „Deck“ zusammengestellt (Auffüllen folgt dem Filter, wertvollste zuerst; Stärke = mittlerer Kartenwert ⌊(2·Angriff + Leben)/3⌋ − Kosten + 1, mal 100: 100 bis 500); zu Beginn die obersten fünf 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 - Jeder hat sein Deck: 30 bis 60 eigene Karten, im Menü „Deck“ zusammengestellt (Auffüllen folgt dem Filter, wertvollste zuerst; Stärke = mittlerer Kartenwert: faire Kosten − Kosten + 1, mal 100, plus 100 je Eigenschaft; Grundkarten 100 bis 500, Eigenbauten und Fusionen entsprechend mehr); zu Beginn die obersten fünf 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 und Legen (Kosten = 2/3 Angriff + 1/3 Leben, abgerundet, höchstens 10; 0/2 kostet 0), Abwerfen bringt 1; 1 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; keine Grenze für Handkarten - Mana für Ziehen und Legen (Kosten = 2/3 Angriff + 1/3 Leben, abgerundet, ohne Obergrenze; 0/2 kostet 0), Abwerfen bringt 1; 1 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; keine Grenze für 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
+14 -9
View File
@@ -24,7 +24,8 @@ Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entw
- Oben links der Kostenkristall, daneben der Name (immer eine Zeile, nie abgeschnitten: die - Oben links der Kostenkristall, daneben der Name (immer eine Zeile, nie abgeschnitten: die
Schrift schrumpft etwas, dann wird der Text schmaler gezeichnet; das Feld ist so hoch wie der Schrift schrumpft etwas, dann wird der Text schmaler gezeichnet; das Feld ist so hoch wie der
Kristall), in der Mitte das Bild als Elementsymbol im dunklen Kreis, so groß wie Kristall), in der Mitte das Bild als Elementsymbol im dunklen Kreis, so groß wie
der Platz erlaubt, unten Angriff (gelb, links) und Leben (blau, rechts) als Kreise. Karten der Platz erlaubt (bei mehreren Elementen nur das stärkste: das die meisten anderen auf der
Karte schlägt, bei Gleichstand ein festes zufälliges), unten Angriff (gelb, links) und Leben (blau, rechts) als Kreise. Karten
haben keinen Text: nur Element, Angriff, Leben. Auf dem Feld stehen unter dem Namen links die haben keinen Text: nur Element, Angriff, Leben. Auf dem Feld stehen unter dem Namen links die
Eigenschaften als Symbole (Ausrufezeichen = Spott, Doppelpfeil = Flink, Kreuz = Regeneration, Eigenschaften als Symbole (Ausrufezeichen = Spott, Doppelpfeil = Flink, Kreuz = Regeneration,
gekreuzte Klingen = Doppelschlag, in der Farbe des Elements, das sie gibt). gekreuzte Klingen = Doppelschlag, in der Farbe des Elements, das sie gibt).
@@ -82,27 +83,31 @@ Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
## Menüs ## Menüs
- Hauptmenü: Titel, Einzelspieler, Kampagne und Deck links, darunter Booster, Sammlung, Skilltree, - Hauptmenü: Titel, Einzelspieler, Kampagne und Deck links, darunter Booster, Sammlung, Skilltree
in der Zeile darunter Einstellungen und Beenden, unten rechts der kleine Cheats-Knopf; rechts drei Karten der Sammlung (höchste Stufen zuerst) als Schaufenster; Münzen und (nach Kapitel 5 auch Werkstatt), in der Zeile darunter Einstellungen und Beenden, unten rechts der kleine Cheats-Knopf; 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 (fünf Kapitel: Neutral, Feuer, Wasser, Luft, - Kampagne als Pfad mit zehn Stationen je Kapitel (zehn Kapitel: Neutral, Feuer, Wasser, Luft,
Erde, das Element steht über dem Pfad), die nächste pulsiert; darunter die gewählte Stufe mit Erde, dann dieselben „mit Effektkarten“; das Element steht über dem Pfad), die nächste pulsiert; darunter die gewählte Stufe mit
Gegner, Leben, Belohnung und Merkmalen. Die Gegner werden über Leben, Merkmale und die Gegner, Leben, Belohnung und Merkmalen. Die Gegner werden über Leben, Merkmale und die
Seltenheit ihrer Karten stärker, nicht über Begleiter. Seltenheit ihrer Karten stärker, nicht über Begleiter.
- Werkstatt: links Elemente (Symbolknöpfe, beliebig viele), Schieber für Angriff und Leben, vier
Schalter für Eigenschaften, Name, darunter Kosten · Wert · Preis und „Bauen“; rechts
die große Vorschau der Karte mit ihren Eigenschaften.
- Booster: je offenem Kapitel eine Pack-Kachel (Symbol, Name, Preis, ×1 ×5 ×10), mittig; gesperrte - Booster: je offenem Kapitel eine Pack-Kachel (Symbol, Name, Preis, ×1 ×5 ×10), mittig; gesperrte
gibt es nicht zu sehen. Die Karten fliegen aus dem Pack ins Raster (ein Pack fünf große, mehrere gibt es nicht zu sehen. Die Karten fliegen aus dem Pack ins Raster (ein Pack fünf große, mehrere
zehn kleine je Reihe, beste Stufe zuerst) und liegen verdeckt; Aufdecken gestaffelt, seltene zehn kleine je Reihe, beste Stufe zuerst) und liegen verdeckt; Aufdecken gestaffelt, seltene
blitzen auf, ab Ultra Rare mit Pause. Danach das Fazit „n neu · n aufgewertet · +n Staub“, das blitzen auf, ab Ultra Rare mit Pause. Danach das Fazit „n neu · n aufgewertet · +n Staub“, das
gekaufte Pack pulsiert. gekaufte Pack pulsiert.
- Skilltree: oben Level, Erfahrung bis zum nächsten Level und freie Punkte, darunter drei Spalten - Skilltree: oben Level, Erfahrungsbalken bis zum nächsten Level und freie Punkte, darunter vier
(Sammeln, Wirtschaft, Kampf) mit Skillkarten: Name, Wirkung, „Jetzt“ und „Nächste Stufe“ in Spalten (Glück, Wirtschaft, Kraft, Weisheit) als senkrechte Pfade: Knoten mit Name, Wirkung,
Zahlen, Stufe x / y und „Lernen“. Rangpunkten, „Jetzt“ und „Nächste“ in Zahlen, durch Linien verbunden; gesperrte Knoten sind
abgedunkelt („Erst … lernen“), „Lernen“ steht nur, wenn lernbar, fertige zeigen grün „Gelernt“.
- Cheats (kleiner Knopf unten rechts im Hauptmenü): Stand oben, darunter Knopfreihen für Währung, - Cheats (kleiner Knopf unten rechts im Hauptmenü): Stand oben, darunter Knopfreihen für Währung,
Erfahrung, Kampagne und Sammlung; jeder Knopf speichert sofort. Erfahrung, Kampagne und Sammlung; jeder Knopf speichert sofort.
- Sammlung: sechs Spalten, nur Elemente offener Kapitel, eigene Karten zuerst (höchste Stufen vorn), fehlende abgedunkelt, rechts - Sammlung: sechs Spalten, nur Elemente offener Kapitel, eigene Karten zuerst (höchste Stufen vorn), fehlende abgedunkelt, rechts
die große Vorschau mit Aufwerten, Zerlegen oder Herstellen. Deck: gleiches Raster, Filter nach Element (Symbolknöpfe) die große Vorschau mit Aufwerten, Zerlegen oder Herstellen. Deck: gleiches Raster, Filter nach Element (Symbolknöpfe)
und Mana 0 bis 10, Stapelkarten zuerst und höchste Stufen vorn, Stapelkarten mit grünem Haken, die und Mana 0 bis 9 oder 10+, Stapelkarten zuerst und höchste Stufen vorn, Stapelkarten mit grünem Haken, die
anderen abgedunkelt; Klick wechselt. Rechts Kartenzahl, Stärke (mittlerer Kartenwert, 100 für Grundkarten bis 500 für anderen abgedunkelt; Klick wechselt. Rechts Kartenzahl, Stärke (mittlerer Kartenwert, 100 für Grundkarten bis 500 für
Ultimate Rare), Manakurve, Elementanteile und die Vorschau der gezeigten Karte. Ultimate Rare), Manakurve, Elementanteile und die Vorschau der gezeigten Karte.
+34 -14
View File
@@ -1,11 +1,13 @@
class_name Campaign class_name Campaign
## Kampagne: fünf Kapitel mit je zehn benannten Magiern, der zehnte ist der Boss. Kapitel 1 spielt ## Kampagne: zehn Kapitel mit je zehn benannten Magiern, der zehnte ist der Boss. Kapitel 1 spielt
## neutrale Karten, Kapitel 2 bis 5 je ein Element; jeder Gegner spielt 30 Karten seines Elements ## neutrale Karten, Kapitel 2 bis 5 je ein Element; Kapitel 6 bis 10 wiederholen die Elemente mit
## nach Manakurve (DeckGen.curve_deck) mit steigendem Kostendeckel (COST_CAP), in der Stufe seines Kapitels (Kapitel 1 und 2 Common, dann je Kapitel eine höher, Kapitel 5 ## Effektkarten (Eigenschaften, DeckGen.effect_deck). Jeder Gegner spielt 30 Karten seines Elements
## Ultra Rare). Mit jedem offenen ## nach Manakurve mit steigendem Kostendeckel (COST_CAP), in der Stufe seines Kapitels (rarity_of).
## Kapitel gibt es den Booster seines Elements zu kaufen (Profile.booster_unlocked). ## Mit jedem offenen Kapitel der ersten Runde gibt es den Booster seines Elements
## (Profile.booster_unlocked); nach Kapitel 5 öffnet die Werkstatt (Profile.workshop_unlocked).
const STAGES_PER_CHAPTER := 10 const STAGES_PER_CHAPTER := 10
const ROUNDS := 2 # Runde 1: Grundkarten (Kapitel 1 bis 5), Runde 2: Effektkarten (6 bis 10)
const CHAPTER_ELEMENTS: Array[String] = [Cards.NEUTRAL, "Feuer", "Wasser", "Luft", "Erde"] const CHAPTER_ELEMENTS: Array[String] = [Cards.NEUTRAL, "Feuer", "Wasser", "Luft", "Erde"]
const NAMES: Array[String] = ["Fenn", "Mara", "Borin", "Liska", "Vulkar", "Nyx", "Ignis", "Thalassa", "Zephyra", "Archimagier"] const NAMES: Array[String] = ["Fenn", "Mara", "Borin", "Liska", "Vulkar", "Nyx", "Ignis", "Thalassa", "Zephyra", "Archimagier"]
const TITLES := { const TITLES := {
@@ -22,8 +24,12 @@ const COST_CAP: Array[int] = [3, 4, 4, 5, 5, 6, 7, 8, 9, 10]
const PERK_ORDER: Array[String] = ["feuer_angriff", "leben_35", "wesen_angriff", "ziehen_gratis", "wesen_leben", "extra_mana", "kein_nachteil", "alle_flink", "alle_flink", "start_energie"] const PERK_ORDER: Array[String] = ["feuer_angriff", "leben_35", "wesen_angriff", "ziehen_gratis", "wesen_leben", "extra_mana", "kein_nachteil", "alle_flink", "alle_flink", "start_energie"]
static func chapters() -> int:
return CHAPTER_ELEMENTS.size() * ROUNDS
static func total() -> int: static func total() -> int:
return STAGES_PER_CHAPTER * CHAPTER_ELEMENTS.size() return STAGES_PER_CHAPTER * chapters()
static func chapter(stage: int) -> int: static func chapter(stage: int) -> int:
@@ -31,12 +37,19 @@ static func chapter(stage: int) -> int:
static func element_of(chapter_no: int) -> String: static func element_of(chapter_no: int) -> String:
return CHAPTER_ELEMENTS[chapter_no - 1] return CHAPTER_ELEMENTS[(chapter_no - 1) % CHAPTER_ELEMENTS.size()]
## Merkmale je Gegner: eins, ab Kapitel 3 zwei, in Kapitel 5 drei. ## Merkmale je Gegner: eins, ab Kapitel 3 zwei, ab Kapitel 5 drei.
static func perk_count(chapter_no: int) -> int: static func perk_count(chapter_no: int) -> int:
return (chapter_no + 1) / 2 return mini(3, (chapter_no + 1) / 2)
## Eigenschaften je Gegnerkarte: Kapitel 1 bis 5 keine, 6 und 7 eine, ab 8 zwei.
static func keyword_count(chapter_no: int) -> int:
if chapter_no <= CHAPTER_ELEMENTS.size():
return 0
return 1 if chapter_no <= CHAPTER_ELEMENTS.size() + 2 else 2
## Stufe i: Name, Titel, Leben, Belohnung (Kapitel 1: 100 bis 300, Boss 500; je Kapitel 250 mehr), ## Stufe i: Name, Titel, Leben, Belohnung (Kapitel 1: 100 bis 300, Boss 500; je Kapitel 250 mehr),
@@ -48,25 +61,32 @@ static func stage(i: int) -> Dictionary:
for n in perk_count(c): for n in perk_count(c):
perks.append(PERK_ORDER[(k + 3 * n) % STAGES_PER_CHAPTER]) perks.append(PERK_ORDER[(k + 3 * n) % STAGES_PER_CHAPTER])
var base := 100 + 250 * (c - 1) var base := 100 + 250 * (c - 1)
return {"name": NAMES[k], "title": TITLES[element_of(c)][k], "life": LIFE[k], var title: String = TITLES[element_of(c)][k] + (" II" if c > CHAPTER_ELEMENTS.size() else "")
return {"name": NAMES[k], "title": title, "life": LIFE[k],
"reward": base + (400 if k == STAGES_PER_CHAPTER - 1 else 50 * (k / 2)), "reward": base + (400 if k == STAGES_PER_CHAPTER - 1 else 50 * (k / 2)),
"perks": perks, "element": element_of(c), "max_cost": COST_CAP[k]} "perks": perks, "element": element_of(c), "max_cost": COST_CAP[k], "keywords": keyword_count(c)}
## Gegner einer Stufe: Leben und Merkmale der Stufe, 30 Karten seines Elements nach Manakurve bis ## Gegner einer Stufe: Leben und Merkmale der Stufe, 30 Karten seines Elements nach Manakurve bis
## max_cost, in der Stufe des Kapitels (rarity_of). ## max_cost (ab Kapitel 6 Effektkarten), in der Stufe des Kapitels (rarity_of).
static func opponent(stage_index: int, r: RandomNumberGenerator = null) -> Game.Player: 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: if r == null:
r = Game.make_rng() r = Game.make_rng()
if int(st.keywords) > 0:
boss.deck = DeckGen.effect_deck(st.element, r, int(st.max_cost), int(st.keywords))
else:
boss.deck = DeckGen.curve_deck(st.element, r, int(st.max_cost)) boss.deck = DeckGen.curve_deck(st.element, r, int(st.max_cost))
for id in boss.deck: for id in boss.deck:
boss.looks[id] = rarity_of(chapter(stage_index)) boss.looks[id] = rarity_of(chapter(stage_index))
return boss return boss
## Seltenheitsstufe der Gegnerkarten: Kapitel 1 und 2 Common, dann je Kapitel eine höher. ## Seltenheitsstufe der Gegnerkarten: Kapitel 1 und 2 Common, dann je Kapitel eine höher bis Ultra
## Rare; Kapitel 6 bis 9 Ultra Rare, Kapitel 10 Ultimate Rare.
static func rarity_of(chapter_no: int) -> int: static func rarity_of(chapter_no: int) -> int:
return maxi(0, chapter_no - 2) if chapter_no >= chapters():
return Cards.RARITIES.size() - 1
return mini(3, maxi(0, chapter_no - 2))
+81 -8
View File
@@ -109,9 +109,74 @@ static func best_element(own: Array, other: Array) -> String:
return best return best
## Kosten einer Karte: zwei Drittel Angriff plus ein Drittel Leben, abgerundet, höchstens 10 (0/2 kostet nichts). ## Das stärkste Element einer Karte (für das Bild): das, das die meisten der anderen Elemente der
## Karte schlägt; bei Gleichstand entscheidet seed (fest je Karte, damit das Bild nicht wechselt).
static func strongest_element(elements: Array, seed: int) -> String:
var best: Array = []
var best_score := -99
for e in elements:
var score := 0
for o in elements:
score += duel(e, o)
if score > best_score:
best = [e]
best_score = score
elif score == best_score:
best.append(e)
return best[absi(seed) % best.size()]
## Kosten einer Karte: zwei Drittel Angriff plus ein Drittel Leben, abgerundet, ohne Obergrenze (0/2 kostet nichts).
static func cost_of(attack: int, health: int) -> int: static func cost_of(attack: int, health: int) -> int:
return mini(10, (2 * attack + health) / 3) return (2 * attack + health) / 3
## Mehrkosten einer Eigenschaft: Doppelschlag bezahlt den Angriff noch einmal (2·Angriff/3), Flink ein
## Drittel Angriff, Regeneration ein Drittel Leben, Spott 1; abgerundet, nie unter 1.
static func keyword_cost(kw: String, attack: int, health: int) -> int:
match kw:
"Doppelschlag":
return maxi(1, 2 * attack / 3)
"Flink":
return maxi(1, attack / 3)
"Regeneration":
return maxi(1, health / 3)
return 1
## Mehrkosten weiterer Elemente: das zweite 1, das dritte 2, das vierte 3 … (zusammen 1, 3, 6, 10).
static func element_cost(count: int) -> int:
return count * (count - 1) / 2
## Kosten einer Karte mit Eigenschaften und Elementen: Grundkosten plus keyword_cost je Eigenschaft
## plus element_cost, ohne Obergrenze (Werkstatt und Effektkarten der Kapitel 6 bis 10).
static func cost_with(attack: int, health: int, keywords: Array, elements: Array) -> int:
var c := cost_of(attack, health) + element_cost(elements.size())
for k in keywords:
c += keyword_cost(k, attack, health)
return c
## Eigene oder erzeugte Karte (IDs c_… aus der Werkstatt, g_… für Gegner) mit Kosten nach cost_with.
static func custom(id: String, name: String, elements: Array, attack: int, health: int, keywords: Array) -> Card:
var c := Card.new()
c.id = id
c.name = name
c.name_en = name
c.elements = elements.duplicate()
c.attack = attack
c.health = health
c.keywords = keywords.duplicate()
c.cost = cost_with(attack, health, keywords, elements)
return c
## Nimmt eine Karte ins Register (by_id) auf, ohne sie in den Pool (all) zu legen.
static func register(c: Card) -> void:
if _all.is_empty():
_build()
_by_id[c.id] = c
## Wert eines verschmolzenen Wesens aus den Grundwerten der Eltern: die aufgerundete Hälfte der ## Wert eines verschmolzenen Wesens aus den Grundwerten der Eltern: die aufgerundete Hälfte der
@@ -172,9 +237,11 @@ 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, höchstens 10 (aus den Grundwerten) var cost: int = 0 # abgeleitet: (2 · Angriff + Leben) / 3, abgerundet, plus Eigenschaften und zweites Element
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 look := -1 # Seltenheitsstufe des Exemplars (gibt +1/+1 je Stufe); -1: Grundkarte aus dem Pool var look := -1 # Seltenheitsstufe des Exemplars (gibt +1/+1 je Stufe); -1: Grundkarte aus dem Pool
var boost_attack := 0 # Skill Stark auf diesem Exemplar, kein Grundwert
var boost_health := 0 # Skill Zäh
var art := "" # verschmolzen: Karte, deren Bild das Wesen trägt (der stärkere Elternteil) var art := "" # verschmolzen: Karte, deren Bild das Wesen trägt (der stärkere Elternteil)
func to_dict() -> Dictionary: func to_dict() -> Dictionary:
@@ -189,17 +256,23 @@ class Card extends RefCounted:
d["cost"] = cost d["cost"] = cost
return d return d
## Wert der Karte: was sie über ihre Manakosten hinaus bringt, mal 100. Grundkarten liegen ## Wert der Karte: was sie über ihre Manakosten hinaus bringt (gegen die fairen Kosten cost_with),
## bei 100, jede Seltenheitsstufe gibt 100 dazu (Ultimate Rare 500); Fusionen aus ihren Werten. ## mal 100, plus 100 je Eigenschaft. Grundkarten und Eigenbauten ohne Eigenschaften liegen bei 100,
## jede Seltenheitsstufe gibt 100 dazu (Ultimate Rare 500); Fusionen bringen ihr zweites Element
## und geerbte Eigenschaften umsonst mit und sind entsprechend mehr wert.
func value() -> int: func value() -> int:
return (Cards.cost_of(attack, health) - cost + 1) * 100 return (Cards.cost_with(attack, health, keywords, elements) - cost + 1) * 100 + 100 * keywords.size()
## Eigenbau (Werkstatt) oder erzeugte Gegnerkarte, nicht aus dem Pool.
func is_custom() -> bool:
return id.begins_with("c_") or id.begins_with("g_")
## Grundwerte ohne den Stufenbonus. ## Grundwerte ohne den Stufenbonus.
func base_attack() -> int: func base_attack() -> int:
return attack - maxi(look, 0) return attack - maxi(look, 0) - boost_attack
func base_health() -> int: func base_health() -> int:
return health - maxi(look, 0) return health - maxi(look, 0) - boost_health
## 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).
func rarity_in(looks: Dictionary) -> String: func rarity_in(looks: Dictionary) -> String:
+40 -2
View File
@@ -30,9 +30,14 @@ static func element_deck(element: String) -> Array:
## je Kosten so viele wie CURVE vorgibt, zufällig gewählt. Was über dem Deckel fehlt, füllen die ## je Kosten so viele wie CURVE vorgibt, zufällig gewählt. Was über dem Deckel fehlt, füllen die
## erlaubten Kosten reihum von billig nach teuer auf. ## erlaubten Kosten reihum von billig nach teuer auf.
static func curve_deck(element: String, r: RandomNumberGenerator, max_cost: int = 10) -> Array: static func curve_deck(element: String, r: RandomNumberGenerator, max_cost: int = 10) -> Array:
return curve_pick(Cards.all().filter(func(c): return c.elements[0] == element), r, max_cost)
## Wählt aus cards (Karten) nach der Manakurve bis max_cost, siehe curve_deck; liefert IDs.
static func curve_pick(cards: Array, r: RandomNumberGenerator, max_cost: int) -> Array:
var by_cost := {} var by_cost := {}
for c in Cards.all(): for c in cards:
if c.elements[0] == element and c.cost <= max_cost: if c.cost <= max_cost:
if not by_cost.has(c.cost): if not by_cost.has(c.cost):
by_cost[c.cost] = [] by_cost[c.cost] = []
by_cost[c.cost].append(c.id) by_cost[c.cost].append(c.id)
@@ -56,6 +61,39 @@ static func curve_deck(element: String, r: RandomNumberGenerator, max_cost: int
return out return out
## Stapel mit Effektkarten (Kapitel 6 bis 10): wie curve_deck, aber jede Karte trägt keyword_count
## Eigenschaften. Zur Wahl stehen alle Grundkarten des Elements mit jeder Eigenschaftskombination,
## deren Kosten (Cards.cost_with) unter max_cost bleiben; die gewählten werden registriert (IDs g_…).
static func effect_deck(element: String, r: RandomNumberGenerator, max_cost: int, keyword_count: int) -> Array:
var pool := {}
for b in Cards.all():
if b.elements[0] != element:
continue
for combo in combos(Array(Cards.KEYWORDS), keyword_count):
var kws := Cards.KEYWORDS.filter(func(k): return k in combo)
var short := "".join(kws.map(func(k): return k.substr(0, 1)))
var c := Cards.custom("g_%s_%s" % [b.id, short], b.name, b.elements, b.attack, b.health, kws)
c.name_en = b.name_en
pool[c.id] = c
var out := curve_pick(pool.values(), r, max_cost)
for id in out:
Cards.register(pool[id])
return out
## Alle Teilmengen von items mit k Einträgen, in der Reihenfolge von items.
static func combos(items: Array, k: int) -> Array:
if k == 0:
return [[]]
var out := []
for i in items.size():
for rest in combos(items.slice(i + 1), k - 1):
var combo := [items[i]]
combo.append_array(rest)
out.append(combo)
return out
## Füllt deck aus pool (Karten-IDs) bis limit auf, die wertvollsten zuerst (Card.value in der Stufe ## Füllt deck aus pool (Karten-IDs) bis limit auf, die wertvollsten zuerst (Card.value in der Stufe
## aus levels: ID -> Stufe); bei gleichem Wert zuerst, was der Manakurve (CURVE, auf limit ## aus levels: ID -> Stufe); bei gleichem Wert zuerst, was der Manakurve (CURVE, auf limit
## skaliert) noch fehlt, dann die billigeren, dann nach ID; nichts doppelt. ## skaliert) noch fehlt, dann die billigeren, dann nach ID; nichts doppelt.
+24 -3
View File
@@ -137,6 +137,9 @@ class Player extends RefCounted:
var cards_played := 0 # ausgespielte Karten (Münzen nach dem Kampf) var cards_played := 0 # ausgespielte Karten (Münzen nach dem Kampf)
var draws := 0 # in diesem Zug gezogene Karten; die n-te kostet n-1 Mana var draws := 0 # in diesem Zug gezogene Karten; die n-te kostet n-1 Mana
var fatigue := 0 # Züge ohne Karten im Stapel: so viel Leben kostet der nächste var fatigue := 0 # Züge ohne Karten im Stapel: so viel Leben kostet der nächste
var bonus_attack := 0 # Skill Stark: so viel mehr Angriff auf jeder gezogenen Karte
var bonus_health := 0 # Skill Zäh: so viel mehr Leben
var cheap_from := 99 # Skill Sparsam: Karten ab diesen Kosten sind 1 Mana billiger
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
@@ -148,12 +151,24 @@ class Player extends RefCounted:
func alive() -> bool: func alive() -> bool:
return life > 0 return life > 0
## Die oberste Karte des eigenen Stapels als Exemplar in der eigenen Stufe; null, wenn er leer ist. ## Die oberste Karte des eigenen Stapels als Exemplar in der eigenen Stufe und mit den Skillboni
## (Stark, Zäh, Sparsam); null, wenn er leer ist.
func draw_from_deck() -> Cards.Card: func draw_from_deck() -> Cards.Card:
if deck.is_empty(): if deck.is_empty():
return null return null
var id: String = deck.pop_back() var id: String = deck.pop_back()
return Cards.by_id(id).leveled(int(looks.get(id, 0))) return boost(Cards.by_id(id).leveled(int(looks.get(id, 0))))
## Legt die Skillboni auf ein Exemplar: Stark und Zäh getrennt von den Grundwerten (Fusionen
## rechnen ohne sie), Sparsam macht teure Karten 1 Mana billiger.
func boost(c: Cards.Card) -> Cards.Card:
c.boost_attack = bonus_attack
c.boost_health = bonus_health
c.attack += bonus_attack
c.health += bonus_health
if c.cost >= cheap_from:
c.cost -= 1
return c
## Offene eigene Wesen mit diesem Element. ## Offene eigene Wesen mit diesem Element.
func element_count(e: String) -> int: func element_count(e: String) -> int:
@@ -177,6 +192,9 @@ class Player extends RefCounted:
p.deck = deck.duplicate() p.deck = deck.duplicate()
p.draws = draws p.draws = draws
p.fatigue = fatigue p.fatigue = fatigue
p.bonus_attack = bonus_attack
p.bonus_health = bonus_health
p.cheap_from = cheap_from
for c in board: for c in board:
p.board.append(c.clone(p)) p.board.append(c.clone(p))
return p return p
@@ -489,7 +507,7 @@ func can_fuse(p: Player, a, b) -> Array:
## Die Karte, die aus a und b entsteht (siehe Cards.fused), in der höheren Stufe der Eltern. ## Die Karte, die aus a und b entsteht (siehe Cards.fused), in der höheren Stufe der Eltern.
static func fusion_result(p: Player, a, b) -> Cards.Card: static func fusion_result(p: Player, a, b) -> Cards.Card:
return Cards.fused(card_of(a), card_of(b), maxi(look_of(p, card_of(a)), look_of(p, card_of(b)))) return p.boost(Cards.fused(card_of(a), card_of(b), maxi(look_of(p, card_of(a)), look_of(p, card_of(b)))))
## Verschmilzt zwei eigene Karten zu einer neuen Handkarte: Wesen verlassen das Brett, die neue ## Verschmilzt zwei eigene Karten zu einer neuen Handkarte: Wesen verlassen das Brett, die neue
@@ -633,6 +651,9 @@ static func new_battle_with(opp: Player, r: RandomNumberGenerator = null, fixed_
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() human.deck = rules.get("deck", []).duplicate()
human.bonus_attack = int(rules.get("bonus_attack", 0))
human.bonus_health = int(rules.get("bonus_health", 0))
human.cheap_from = int(rules.get("cheap_from", 99))
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:
+66 -11
View File
@@ -33,6 +33,7 @@ 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 xp := 0 # Erfahrung: alle je verdienten Münzen (Skills.level_for) var xp := 0 # Erfahrung: alle je verdienten Münzen (Skills.level_for)
var customs: Array = [] # Eigenbauten aus der Werkstatt (Card.to_dict), beim Laden registriert
var skills := {} # Skill-ID -> gelernte Stufe (Skills.SKILLS) var skills := {} # Skill-ID -> gelernte Stufe (Skills.SKILLS)
var collection := {} # eigene Karten: id -> Seltenheitsstufe (Index in Cards.RARITIES) 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 deck: Array = [] # Karten-IDs des eigenen Stapels (Game.DECK_MIN bis DECK_MAX)
@@ -84,6 +85,11 @@ static func from_dict(d: Dictionary) -> Profile:
if Skills.SKILLS.has(id): if Skills.SKILLS.has(id):
p.skills[id] = clampi(int(sk[id]), 0, int(Skills.SKILLS[id].max)) p.skills[id] = clampi(int(sk[id]), 0, int(Skills.SKILLS[id].max))
p.campaign_stage = int(d.get("campaign_stage", 0)) p.campaign_stage = int(d.get("campaign_stage", 0))
for cd in d.get("customs", []):
if cd is Dictionary and cd.has("id") and str(cd.id).begins_with("c_"):
var c := Cards.Card.from_dict(cd)
Cards.register(c)
p.customs.append(c.to_dict())
if int(d.get("version", 1)) >= SAVE_VERSION: if int(d.get("version", 1)) >= SAVE_VERSION:
var coll = d.get("collection", {}) var coll = d.get("collection", {})
for cid in coll: for cid in coll:
@@ -110,7 +116,7 @@ static func from_dict(d: Dictionary) -> Profile:
func to_dict() -> Dictionary: func to_dict() -> Dictionary:
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "xp": xp, "skills": skills, "collection": collection, return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "xp": xp, "skills": skills, "customs": customs, "collection": collection,
"deck": deck, "campaign_stage": campaign_stage, "settings": settings, "perk": perk} "deck": deck, "campaign_stage": campaign_stage, "settings": settings, "perk": perk}
@@ -225,16 +231,17 @@ func booster_unlocked(element: String) -> bool:
return campaign_stage >= Campaign.STAGES_PER_CHAPTER * Campaign.CHAPTER_ELEMENTS.find(element) return campaign_stage >= Campaign.STAGES_PER_CHAPTER * Campaign.CHAPTER_ELEMENTS.find(element)
## Fünf Karten des Elements mit gewürfelter Seltenheit; luck (Packglück) verschiebt die Chancen. ## Fünf Karten des Elements mit gewürfelter Seltenheit; luck (Packglück) verschiebt die Chancen,
static func roll_booster(r: RandomNumberGenerator, element: String, luck: int = 0) -> Array: ## mindestens eine Karte hat die Seltenheit floor (Rare, mit Garantie Super Rare).
static func roll_booster(r: RandomNumberGenerator, element: String, luck: int = 0, floor: String = "Rare") -> Array:
var rarities := RARITY_WEIGHTS.keys() var rarities := RARITY_WEIGHTS.keys()
var weights := Skills.pack_weights(luck) var weights := Skills.pack_weights(luck)
var pool := Cards.all().filter(func(c): return c.elements[0] == element) var pool := Cards.all().filter(func(c): return c.elements[0] == element)
var out := [] var out := []
for i in BOOSTER_SIZE: for i in BOOSTER_SIZE:
var rar: String = rarities[r.rand_weighted(weights)] var rar: String = rarities[r.rand_weighted(weights)]
if i == BOOSTER_SIZE - 1 and out.all(func(x): return x.rarity == "Common"): if i == BOOSTER_SIZE - 1 and out.all(func(x): return Cards.RARITIES.find(x.rarity) < Cards.RARITIES.find(floor)):
rar = "Rare" # mindestens eine Rare pro Booster rar = floor
out.append({"card": pool[r.randi_range(0, pool.size() - 1)], "rarity": rar}) out.append({"card": pool[r.randi_range(0, pool.size() - 1)], "rarity": rar})
return out return out
@@ -261,7 +268,7 @@ func open_boosters(element: String, n: int, r: RandomNumberGenerator = null) ->
func _open_one(element: String, r: RandomNumberGenerator) -> Array: func _open_one(element: String, r: RandomNumberGenerator) -> Array:
coins -= pack_price(element) coins -= pack_price(element)
var out := roll_booster(r, element, skill("packglueck")) var out := roll_booster(r, element, skill("packglueck"), Skills.pack_floor(skill("garantie")))
for res in out: for res in out:
var lvl: int = Cards.RARITIES.find(res.rarity) var lvl: int = Cards.RARITIES.find(res.rarity)
res["new"] = not owns(res.card.id) res["new"] = not owns(res.card.id)
@@ -330,10 +337,14 @@ func skill_points() -> int:
return player_level() - spent return player_level() - spent
## Lernt die nächste Stufe eines Skills für einen Punkt. Gibt "" zurück oder eine Fehlermeldung. ## Lernt die nächste Stufe eines Skills für einen Punkt; der Skill davor im Pfad braucht eine Stufe.
## Gibt "" zurück oder eine Fehlermeldung.
func learn(id: String) -> String: func learn(id: String) -> String:
if skill(id) >= int(Skills.SKILLS[id].max): if skill(id) >= int(Skills.SKILLS[id].max):
return "Schon auf der höchsten Stufe" return "Schon auf der höchsten Stufe"
var prev := Skills.previous(id)
if prev != "" and skill(prev) == 0:
return "Erst den Skill davor lernen"
if skill_points() <= 0: if skill_points() <= 0:
return "Kein Skillpunkt frei" return "Kein Skillpunkt frei"
skills[id] = skill(id) + 1 skills[id] = skill(id) + 1
@@ -379,15 +390,59 @@ func opponent_level() -> int:
return clampi(deck_level() + int(difficulty().level), 0, Cards.RARITIES.size() - 1) return clampi(deck_level() + int(difficulty().level), 0, Cards.RARITIES.size() - 1)
## Elemente der offenen Kampagnenkapitel (Neutral immer). ## Elemente der offenen Kampagnenkapitel (Neutral immer, ab Kapitel 5 alle).
func unlocked_elements() -> Array: func unlocked_elements() -> Array:
return Campaign.CHAPTER_ELEMENTS.slice(0, campaign_stage / Campaign.STAGES_PER_CHAPTER + 1) return Campaign.CHAPTER_ELEMENTS.slice(0, mini(Campaign.CHAPTER_ELEMENTS.size(), campaign_stage / Campaign.STAGES_PER_CHAPTER + 1))
## Alle Karten der offenen Elemente: was Sammlung, Deck und Booster zeigen. ## Alle Karten der offenen Elemente plus die eigenen Eigenbauten: was Sammlung und Deck zeigen.
func unlocked_cards() -> Array: func unlocked_cards() -> Array:
var open := unlocked_elements() var open := unlocked_elements()
return Cards.all().filter(func(c): return c.elements[0] in open) var out: Array = Cards.all().filter(func(c): return c.elements[0] in open)
for id in collection:
if Cards.has_id(id) and Cards.by_id(id).is_custom():
out.append(Cards.by_id(id))
return out
# --- Werkstatt ----------------------------------------------------------------------
## Die Werkstatt öffnet nach dem Boss von Kapitel 5.
func workshop_unlocked() -> bool:
return campaign_stage >= Campaign.CHAPTER_ELEMENTS.size() * Campaign.STAGES_PER_CHAPTER
## Preis eines Eigenbaus in Staub: der doppelte Kartenwert, plus 100 je weiterem Element.
func build_price(card: Cards.Card) -> int:
return 2 * card.value() + 100 * maxi(card.elements.size() - 1, 0)
## Baut eine Karte (Werkstatt): beliebig viele Elemente, Angriff 0 bis 10, Leben 1 bis 10, beliebig
## viele Eigenschaften, Kosten nach Cards.cost_with. Sie kommt als Common in Sammlung und Stapel.
## Gibt "" zurück oder eine Fehlermeldung.
func build_card(name: String, elements: Array, attack: int, health: int, keywords: Array) -> String:
if not workshop_unlocked():
return "Werkstatt noch gesperrt"
if name.strip_edges() == "":
return "Name fehlt"
if elements.is_empty():
return "Mindestens ein Element"
if attack < 0 or attack > 10 or health < 1 or health > 10:
return "Angriff 0 bis 10, Leben 1 bis 10"
var n := 1
for cd in customs:
n = maxi(n, int(str(cd.id).trim_prefix("c_")) + 1)
var c := Cards.custom("c_%d" % n, name.strip_edges(), elements, attack, health, keywords)
var price := build_price(c)
if dust < price:
return "Nicht genug Staub (%d nötig)" % price
dust -= price
Cards.register(c)
customs.append(c.to_dict())
collection[c.id] = 0
if deck.size() < Game.DECK_MAX:
deck.append(c.id)
return ""
## Einzelspieler-Gegner: ein Element aus den offenen Kapiteln, 30 Karten nach Manakurve in opponent_level; ## Einzelspieler-Gegner: ein Element aus den offenen Kapiteln, 30 Karten nach Manakurve in opponent_level;
+52 -13
View File
@@ -1,18 +1,30 @@
class_name Skills class_name Skills
## Skilltree: Erfahrung sind die verdienten Münzen, je Level ein Skillpunkt, Punkte kaufen Stufen ## Skilltree: Erfahrung sind die verdienten Münzen, je Level ein Skillpunkt, Punkte kaufen Stufen
## der Skills in drei Ästen. Münzen kaufen keine Punkte. ## der Skills in vier Ästen (PATHS). Jeder Ast ist ein Pfad: der nächste Skill öffnet, sobald der
## davor mindestens eine Stufe hat. Münzen kaufen keine Punkte.
const BRANCHES: Array[String] = ["Sammeln", "Wirtschaft", "Kampf"] const BRANCHES: Array[String] = ["Glück", "Wirtschaft", "Kraft", "Weisheit"]
## id -> Name, Ast, Höchststufe, Text. ## Ast -> Skills in Pfadreihenfolge (oben nach unten).
const SKILLS := { const PATHS := {
"packglueck": {"name": "Packglück", "branch": "Sammeln", "max": 3, "text": "Seltenere Karten in Boostern"}, "Glück": ["packglueck", "garantie"],
"staub": {"name": "Staubsammler", "branch": "Sammeln", "max": 3, "text": "Mehr Staub aus Doppelten und Zerlegen"}, "Wirtschaft": ["packpreis", "muenzen", "staub"],
"muenzen": {"name": "Münzsegen", "branch": "Wirtschaft", "max": 3, "text": "Mehr Münzen nach jedem Kampf"}, "Kraft": ["zaeh", "stark"],
"packpreis": {"name": "Händler", "branch": "Wirtschaft", "max": 2, "text": "Booster werden billiger"}, "Weisheit": ["gratisziehen", "startmana", "sparsam"],
"gratisziehen": {"name": "Freies Ziehen", "branch": "Kampf", "max": 1, "text": "Das zweite Ziehen je Zug ist auch gratis"},
"startmana": {"name": "Starker Start", "branch": "Kampf", "max": 1, "text": "Mehr Mana im ersten Zug"},
} }
## Kampfskills geben ein Merkmal (Game.PERKS) im Kampf. ## id -> Name, Höchststufe, Text.
const SKILLS := {
"packglueck": {"name": "Packglück", "max": 3, "text": "Seltenere Karten in Boostern"},
"garantie": {"name": "Garantie", "max": 1, "text": "Mindestens eine Super Rare je Booster"},
"packpreis": {"name": "Händler", "max": 3, "text": "Booster werden billiger"},
"muenzen": {"name": "Münzsegen", "max": 3, "text": "Mehr Münzen nach jedem Kampf"},
"staub": {"name": "Staubsammler", "max": 3, "text": "Mehr Staub aus Doppelten und Zerlegen"},
"zaeh": {"name": "Zäh", "max": 3, "text": "Eigene Karten haben mehr Leben"},
"stark": {"name": "Stark", "max": 3, "text": "Eigene Karten haben mehr Angriff"},
"gratisziehen": {"name": "Freies Ziehen", "max": 1, "text": "Zweites Ziehen je Zug auch gratis"},
"startmana": {"name": "Starker Start", "max": 1, "text": "Mehr Mana im ersten Zug"},
"sparsam": {"name": "Sparsam", "max": 3, "text": "Teure Karten kosten 1 Mana weniger"},
}
## Diese Skills geben ein Merkmal (Game.PERKS) im Kampf.
const PERK_OF := {"gratisziehen": "ziehen_gratis", "startmana": "start_energie"} const PERK_OF := {"gratisziehen": "ziehen_gratis", "startmana": "start_energie"}
## Packglück je Stufe: Common gibt ab, die höheren Stufen gewinnen. ## Packglück je Stufe: Common gibt ab, die höheren Stufen gewinnen.
const LUCK_SHIFT := {"Common": -8.0, "Rare": 3.0, "Super Rare": 2.0, "Ultra Rare": 2.0, "Ultimate Rare": 1.0} const LUCK_SHIFT := {"Common": -8.0, "Rare": 3.0, "Super Rare": 2.0, "Ultra Rare": 2.0, "Ultimate Rare": 1.0}
@@ -32,7 +44,26 @@ static func level_for(xp: int) -> int:
static func in_branch(branch: String) -> Array: static func in_branch(branch: String) -> Array:
return SKILLS.keys().filter(func(id): return SKILLS[id].branch == branch) return PATHS[branch]
## Der Skill davor im Pfad, "" für den ersten eines Astes.
static func previous(id: String) -> String:
for b in PATHS:
var i: int = PATHS[b].find(id)
if i > 0:
return PATHS[b][i - 1]
return ""
## Sparsam: ab diesen Kosten ist eine Karte 1 Mana billiger (Stufe 1 ab 7, 2 ab 5, 3 ab 3); 99 = nie.
static func cheap_from(rank: int) -> int:
return 9 - 2 * rank if rank > 0 else 99
## Mindestseltenheit je Booster: Rare, mit Garantie Super Rare.
static func pack_floor(garantie: int) -> String:
return "Super Rare" if garantie > 0 else "Rare"
## Boosterchancen je Stufe (Reihenfolge Cards.RARITIES) mit Packglück luck. ## Boosterchancen je Stufe (Reihenfolge Cards.RARITIES) mit Packglück luck.
@@ -49,14 +80,22 @@ static func effect(id: String, rank: int) -> String:
"packglueck": "packglueck":
var w := pack_weights(rank) var w := pack_weights(rank)
return TranslationServer.translate("Chancen %d/%d/%d/%d/%d %%") % [int(w[0]), int(w[1]), int(w[2]), int(w[3]), int(w[4])] return TranslationServer.translate("Chancen %d/%d/%d/%d/%d %%") % [int(w[0]), int(w[1]), int(w[2]), int(w[3]), int(w[4])]
"garantie":
return TranslationServer.translate("Super Rare je Pack sicher" if rank > 0 else "Rare je Pack sicher")
"staub": "staub":
return TranslationServer.translate("+%d %% Staub") % (25 * rank) return TranslationServer.translate("+%d %% Staub") % (25 * rank)
"muenzen": "muenzen":
return TranslationServer.translate("+%d %% Münzen") % (10 * rank) return TranslationServer.translate("+%d %% Münzen") % (10 * rank)
"packpreis": "packpreis":
return TranslationServer.translate("Neutral %d, Element %d Münzen") % [roundi(Profile.BOOSTER_COST * (1.0 - 0.1 * rank)), roundi(Profile.ELEMENT_BOOSTER_COST * (1.0 - 0.1 * rank))] return TranslationServer.translate("Neutral %d, Element %d") % [roundi(Profile.BOOSTER_COST * (1.0 - 0.1 * rank)), roundi(Profile.ELEMENT_BOOSTER_COST * (1.0 - 0.1 * rank))]
"zaeh":
return TranslationServer.translate("+%d Leben") % rank
"stark":
return TranslationServer.translate("+%d Angriff") % rank
"gratisziehen": "gratisziehen":
return TranslationServer.translate("zwei Karten je Zug gratis" if rank > 0 else "eine Karte je Zug gratis") return TranslationServer.translate("zwei Karten je Zug gratis" if rank > 0 else "eine Karte je Zug gratis")
"startmana": "startmana":
return TranslationServer.translate("erster Zug: %d Mana") % (Game.START_MANA + (2 if rank > 0 else 0)) return TranslationServer.translate("erster Zug: %d Mana") % (Game.START_MANA + (2 if rank > 0 else 0))
"sparsam":
return TranslationServer.translate("ab %d Mana 1 billiger") % cheap_from(rank) if rank > 0 else TranslationServer.translate("voller Preis")
return "" return ""
+1 -1
View File
@@ -113,7 +113,7 @@ func _ready() -> void:
## Eigener Stapel und Kartenstufen und das eigene Merkmal (nach Kapitel 1). ## Eigener Stapel und Kartenstufen 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(), "deck": p.deck_cards()} var rules := {"looks": p.looks(), "deck": p.deck_cards(), "bonus_attack": p.skill("stark"), "bonus_health": p.skill("zaeh"), "cheap_from": Skills.cheap_from(p.skill("sparsam"))}
var perks := p.battle_perks() var perks := p.battle_perks()
if not perks.is_empty(): if not perks.is_empty():
rules["human_perks"] = perks rules["human_perks"] = perks
+9 -7
View File
@@ -12,7 +12,7 @@ var node_buttons: Array = []
func _ready() -> void: func _ready() -> void:
var p: Profile = app.profile var p: Profile = app.profile
chapter = mini(Campaign.chapter(p.campaign_stage), Campaign.CHAPTER_ELEMENTS.size()) chapter = mini(Campaign.chapter(p.campaign_stage), Campaign.chapters())
selected = mini(p.campaign_stage, Campaign.total() - 1) selected = mini(p.campaign_stage, Campaign.total() - 1)
build() build()
@@ -24,16 +24,18 @@ func build() -> void:
Style.header(self, "Kampagne", func(): app.show_screen(MenuScreen.new())) Style.header(self, "Kampagne", func(): app.show_screen(MenuScreen.new()))
var n := Campaign.STAGES_PER_CHAPTER var n := Campaign.STAGES_PER_CHAPTER
var x := 40 var x := 40
for c in Campaign.CHAPTER_ELEMENTS.size(): for c in Campaign.chapters():
var b := Style.button(self, "Kapitel %d" % (c + 1), Rect2(x, 88, 130, 38), set_chapter.bind(c + 1), Style.PRIMARY if chapter == c + 1 else null) var b := Style.button(self, "Kapitel %d" % (c + 1), Rect2(x, 88, 96, 38), set_chapter.bind(c + 1), Style.PRIMARY if chapter == c + 1 else null)
b.add_theme_font_size_override("font_size", 14)
b.disabled = p.campaign_stage < c * n b.disabled = p.campaign_stage < c * n
x += 138 x += 104
var element := Campaign.element_of(chapter) var element := Campaign.element_of(chapter)
Style.label(self, tr(element), 20, Cards.ELEMENT_COLORS[element], Rect2(0, 136, 1280, 30), "center", true) var round_text := tr(element) if chapter <= Campaign.CHAPTER_ELEMENTS.size() else tr("%s mit Effektkarten") % tr(element)
Style.label(self, round_text, 20, Cards.ELEMENT_COLORS[element], Rect2(0, 136, 700, 30), "center", true)
if p.perk_unlocked(): if p.perk_unlocked():
Style.label(self, "Dein Merkmal:", 15, Style.MUTED, Rect2(740, 96, 150, 24), "right") Style.label(self, "Dein Merkmal:", 15, Style.MUTED, Rect2(740, 140, 150, 24), "right")
perk_box = OptionButton.new() perk_box = OptionButton.new()
perk_box.position = Vector2(900, 88) perk_box.position = Vector2(900, 132)
perk_box.size = Vector2(340, 38) perk_box.size = Vector2(340, 38)
perk_box.add_item("keins") perk_box.add_item("keins")
var keys: Array = Game.PERKS.keys() var keys: Array = Game.PERKS.keys()
+4 -3
View File
@@ -24,6 +24,7 @@ func _ready() -> void:
["Kapitel 3 frei", func(): app.profile.campaign_stage = maxi(app.profile.campaign_stage, 20)], ["Kapitel 3 frei", func(): app.profile.campaign_stage = maxi(app.profile.campaign_stage, 20)],
["Kapitel 4 frei", func(): app.profile.campaign_stage = maxi(app.profile.campaign_stage, 30)], ["Kapitel 4 frei", func(): app.profile.campaign_stage = maxi(app.profile.campaign_stage, 30)],
["Kapitel 5 frei", func(): app.profile.campaign_stage = maxi(app.profile.campaign_stage, 40)], ["Kapitel 5 frei", func(): app.profile.campaign_stage = maxi(app.profile.campaign_stage, 40)],
["Werkstatt frei", func(): app.profile.campaign_stage = maxi(app.profile.campaign_stage, 50)],
["Kampagne zurücksetzen", func(): app.profile.campaign_stage = 0]]], ["Kampagne zurücksetzen", func(): app.profile.campaign_stage = 0]]],
["Sammlung", [["Alle Karten (Common)", func(): _give_all(0)], ["Alle Karten Ultimate Rare", func(): _give_all(4)], ["Sammlung", [["Alle Karten (Common)", func(): _give_all(0)], ["Alle Karten Ultimate Rare", func(): _give_all(4)],
["Eigene Karten +1 Stufe", func(): ["Eigene Karten +1 Stufe", func():
@@ -36,9 +37,9 @@ func _ready() -> void:
Style.label(self, g[0], 18, Style.TEXT, Rect2(40, y, 200, 30), "left", true) Style.label(self, g[0], 18, Style.TEXT, Rect2(40, y, 200, 30), "left", true)
var x := 250 var x := 250
for entry in g[1]: for entry in g[1]:
var b := Style.button(self, entry[0], Rect2(x, y - 4, 190, 40), _apply.bind(entry[1])) var b := Style.button(self, entry[0], Rect2(x, y - 4, 160, 40), _apply.bind(entry[1]))
b.add_theme_font_size_override("font_size", 14) b.add_theme_font_size_override("font_size", 13)
x += 198 x += 168
y += 70 y += 70
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(40, y + 10, 1200, 24), "left") msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(40, y + 10, 1200, 24), "left")
refresh() refresh()
+6 -6
View File
@@ -11,7 +11,7 @@ const MAX_COST := 10
var app var app
var filter := "" # Element oder "" var filter := "" # Element oder ""
var cost_filter := -1 # Kosten 0 bis 10 oder -1 var cost_filter := -1 # Kosten 0 bis 9, MAX_COST für 10 und mehr, oder -1
var grid: CardGrid var grid: CardGrid
var all_button: Button var all_button: Button
var element_buttons := {} # Element -> Knopf mit Symbol var element_buttons := {} # Element -> Knopf mit Symbol
@@ -37,7 +37,7 @@ class ElementButton extends Button:
Style.element_icon(self, element, size / 2, size.y * 0.6, Style.lighter(Cards.ELEMENT_COLORS[element], 0.4)) Style.element_icon(self, element, size / 2, size.y * 0.6, Style.lighter(Cards.ELEMENT_COLORS[element], 0.4))
## Manakurve: ein Balken je Kosten 0 bis 10 mit der Zahl der Stapelkarten. ## Manakurve: ein Balken je Kosten 0 bis 9 und einer für 10 und mehr, mit der Zahl der Stapelkarten.
class CurveView extends Control: class CurveView extends Control:
var counts: Array = [] var counts: Array = []
@@ -67,7 +67,7 @@ class CurveView extends Control:
if v > 0: if v > 0:
draw_rect(Rect2(x, base - h, bw, h), Style.BLUE) draw_rect(Rect2(x, base - h, bw, h), Style.BLUE)
Style.text(self, str(v), 11, Style.TEXT, Vector2(x + bw / 2, base - h - 8), "center", true) Style.text(self, str(v), 11, Style.TEXT, Vector2(x + bw / 2, base - h - 8), "center", true)
Style.text(self, str(i), 11, Style.MUTED, Vector2(x + bw / 2, base + 8), "center") Style.text(self, str(i) if i < n - 1 else str(i) + "+", 11, Style.MUTED, Vector2(x + bw / 2, base + 8), "center")
## Elementanteile des Stapels: je Element eine farbige Kachel mit der Kartenzahl. ## Elementanteile des Stapels: je Element eine farbige Kachel mit der Kartenzahl.
@@ -113,7 +113,7 @@ func _ready() -> void:
add_child(fb) add_child(fb)
element_buttons[e] = fb element_buttons[e] = fb
for c in MAX_COST + 1: for c in MAX_COST + 1:
var cb := Style.button(self, str(c), Rect2(330 + c * 40, 86, 36, 34), set_cost.bind(c)) var cb := Style.button(self, str(c) if c < MAX_COST else str(c) + "+", Rect2(330 + c * 40, 86, 36, 34), set_cost.bind(c))
cb.add_theme_font_size_override("font_size", 14) cb.add_theme_font_size_override("font_size", 14)
cost_buttons.append(cb) cost_buttons.append(cb)
grid = CardGrid.new(Rect2(40, 132, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 650)) grid = CardGrid.new(Rect2(40, 132, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 650))
@@ -205,7 +205,7 @@ func cards() -> Array:
if filter != "": if filter != "":
out = out.filter(func(c): return filter in c.elements) out = out.filter(func(c): return filter in c.elements)
if cost_filter >= 0: if cost_filter >= 0:
out = out.filter(func(c): return c.cost == cost_filter) out = out.filter(func(c): return c.cost == cost_filter or (cost_filter == MAX_COST and c.cost > MAX_COST))
out.sort_custom(func(a, b): return sort_key(a) < sort_key(b)) out.sort_custom(func(a, b): return sort_key(a) < sort_key(b))
return out return out
@@ -242,7 +242,7 @@ func refresh() -> void:
var per_element := {} var per_element := {}
for id in p.deck: for id in p.deck:
var c := Cards.by_id(id) var c := Cards.by_id(id)
counts[clampi(c.cost, 0, 10)] += 1 counts[mini(c.cost, MAX_COST)] += 1
per_element[c.elements[0]] = int(per_element.get(c.elements[0], 0)) + 1 per_element[c.elements[0]] = int(per_element.get(c.elements[0], 0)) + 1
curve.set_counts(counts) curve.set_counts(counts)
share.set_counts(per_element) share.set_counts(per_element)
+4 -2
View File
@@ -1,7 +1,7 @@
class_name MenuScreen class_name MenuScreen
extends Control extends Control
## Hauptmenü: links Titel und Spielarten (darunter Booster, Sammlung, Skilltree, dann Einstellungen und ## Hauptmenü: links Titel und Spielarten (darunter Booster, Sammlung, Skilltree, nach Kapitel 5 die
## Beenden), rechts drei Karten der eigenen Sammlung als Schaufenster, unten rechts die Cheats. ## Werkstatt, dann Einstellungen und Beenden), rechts drei Karten der eigenen Sammlung als Schaufenster, unten rechts die Cheats.
var app var app
var coins_label: Label var coins_label: Label
@@ -29,6 +29,8 @@ func _ready() -> void:
["Skilltree", func(): app.show_screen(SkillScreen.new())]]: ["Skilltree", func(): app.show_screen(SkillScreen.new())]]:
Style.button(self, entry[0], Rect2(x, y, 166, 44), entry[1]) Style.button(self, entry[0], Rect2(x, y, 166, 44), entry[1])
x += 176 x += 176
if p.workshop_unlocked():
Style.button(self, "Werkstatt", Rect2(x, y, 166, 44), func(): app.show_screen(WorkshopScreen.new()), Color("5a4628"))
y += 56 y += 56
x = 80 x = 80
for entry in [["Einstellungen", func(): app.show_screen(SettingsScreen.new())], for entry in [["Einstellungen", func(): app.show_screen(SettingsScreen.new())],
+95 -26
View File
@@ -1,43 +1,92 @@
class_name SkillScreen class_name SkillScreen
extends Control extends Control
## Skilltree: oben Level, Erfahrung bis zum nächsten Level und freie Skillpunkte; darunter drei ## Skilltree: oben Level, Erfahrungsbalken bis zum nächsten Level und freie Skillpunkte; darunter
## Äste (Skills.BRANCHES) mit ihren Skills als Karten: Name, Wirkung, aktuelle und nächste Stufe ## vier Äste (Skills.BRANCHES) als senkrechte Pfade: Knoten mit Name, Wirkung, Rangpunkten, „Jetzt“
## in Zahlen (Skills.effect), Stufe und „Lernen“. ## und „Nächste“ in Zahlen (Skills.effect), durch Linien verbunden. Der nächste Knoten öffnet, sobald
## der davor eine Stufe hat; „Lernen“ steht nur, wenn lernbar, fertige Knoten zeigen „Gelernt“.
const COL_W := 285
const COL_STEP := 300
const NODE_TOP := 218
const NODE_H := 150
const NODE_STEP := 172
var app var app
var level_label: Label var level_label: Label
var xp_bar: ProgressBar
var xp_label: Label var xp_label: Label
var points_label: Label var points_label: Label
var msg_label: Label var msg_label: Label
var nodes := {} # Skill-ID -> SkillNode
var learn_buttons := {} # Skill-ID -> Knopf var learn_buttons := {} # Skill-ID -> Knopf
var rank_labels := {} # Skill-ID -> Label
var now_labels := {} # Skill-ID -> Wirkung jetzt var now_labels := {} # Skill-ID -> Wirkung jetzt
var next_labels := {} # Skill-ID -> Wirkung der nächsten Stufe var next_labels := {} # Skill-ID -> Wirkung der nächsten Stufe
var state_labels := {} # Skill-ID -> „Gelernt“, „Erst … lernen“ oder „Kein Skillpunkt frei“
var links := {} # Skill-ID -> Linie zum Knoten davor
## Ein Knoten des Baums: Rahmen und Rangpunkte zeichnet er selbst, Texte und Knopf sind Kinder.
class SkillNode extends Control:
var rank := 0
var max_rank := 1
var open := true # der Skill davor hat eine Stufe (oder es gibt keinen)
func _init(rect: Rect2, mx: int) -> void:
max_rank = mx
position = rect.position
size = rect.size
func _draw() -> void:
var done := rank >= max_rank
var bg: Color = Style.PANEL_LIGHT if open else Style.darker(Style.PANEL, 0.9)
var border: Color = Style.GREEN if done else (Style.GOLD if rank > 0 else (Style.MUTED if open else Style.darker(Style.MUTED, 0.5)))
draw_style_box(Style.box(bg, 8, border, 2), Rect2(Vector2.ZERO, size))
for i in max_rank: # Rangpunkte oben rechts
var c := Vector2(size.x - 16 - (max_rank - 1 - i) * 18, 21)
draw_circle(c, 6, Style.GOLD if i < rank else Style.darker(Style.MUTED, 0.6))
func _ready() -> void: func _ready() -> void:
Style.header(self, "Skilltree", func(): app.show_screen(MenuScreen.new())) Style.header(self, "Skilltree", func(): app.show_screen(MenuScreen.new()))
level_label = Style.chip(self, "", Rect2(40, 86, 220, 38)) level_label = Style.chip(self, "", Rect2(40, 86, 160, 38))
xp_label = Style.chip(self, "", Rect2(276, 86, 400, 38), Style.BLUE) xp_bar = ProgressBar.new()
points_label = Style.chip(self, "", Rect2(692, 86, 300, 38), Style.GREEN) xp_bar.position = Vector2(216, 86)
Style.label(self, "Erfahrung sind deine verdienten Münzen. Jedes Level bringt einen Skillpunkt.", 14, Style.MUTED, Rect2(40, 134, 1200, 22), "left") xp_bar.size = Vector2(520, 38)
xp_bar.show_percentage = false
xp_bar.add_theme_stylebox_override("background", Style.box(Style.PANEL_LIGHT, 19))
xp_bar.add_theme_stylebox_override("fill", Style.box(Style.darker(Style.BLUE, 0.7), 19))
add_child(xp_bar)
xp_label = Style.label(self, "", 16, Style.TEXT, Rect2(216, 86, 520, 38), "center", true)
points_label = Style.chip(self, "", Rect2(752, 86, 260, 38), Style.GREEN)
Style.label(self, "Erfahrung sind deine verdienten Münzen. Jedes Level bringt einen Skillpunkt. Jeder Ast öffnet sich von oben nach unten.", 14, Style.MUTED, Rect2(40, 134, 1200, 22), "left")
for b in Skills.BRANCHES.size(): for b in Skills.BRANCHES.size():
var branch: String = Skills.BRANCHES[b] var branch: String = Skills.BRANCHES[b]
var x := 40 + b * 410 var x := 40 + b * COL_STEP
Style.panel(self, Rect2(x, 170, 390, 580)) Style.panel(self, Rect2(x, 166, COL_W, 600))
Style.label(self, branch, 22, Style.GOLD, Rect2(x, 184, 390, 32), "center", true) Style.label(self, branch, 20, Style.GOLD, Rect2(x, 178, COL_W, 30), "center", true)
var y := 236 var path: Array = Skills.in_branch(branch)
for id in Skills.in_branch(branch): for i in path.size():
var id: String = path[i]
var sk: Dictionary = Skills.SKILLS[id] var sk: Dictionary = Skills.SKILLS[id]
Style.panel(self, Rect2(x + 16, y, 358, 176), Style.PANEL_LIGHT, 8) var y := NODE_TOP + i * NODE_STEP
Style.label(self, sk.name, 18, Style.TEXT, Rect2(x + 32, y + 10, 326, 26), "left", true) if i > 0:
Style.label(self, sk.text, 14, Style.MUTED, Rect2(x + 32, y + 38, 326, 22), "left") var line := ColorRect.new()
now_labels[id] = Style.label(self, "", 14, Style.TEXT, Rect2(x + 32, y + 64, 326, 22), "left") line.position = Vector2(x + COL_W / 2.0 - 2, y - NODE_STEP + NODE_H)
next_labels[id] = Style.label(self, "", 14, Style.GREEN, Rect2(x + 32, y + 86, 326, 22), "left") line.size = Vector2(4, NODE_STEP - NODE_H)
rank_labels[id] = Style.label(self, "", 15, Style.GOLD, Rect2(x + 32, y + 126, 150, 30), "left", true) add_child(line)
learn_buttons[id] = Style.button(self, "Lernen", Rect2(x + 214, y + 122, 144, 40), learn.bind(id), Style.PRIMARY) links[id] = line
y += 192 var node := SkillNode.new(Rect2(x + 14, y, COL_W - 28, NODE_H), int(sk.max))
msg_label = Style.label(self, "", 15, Style.RED, Rect2(0, 760, 1280, 24), "center") add_child(node)
nodes[id] = node
var w := COL_W - 28 - 28
Style.label(node, sk.name, 17, Style.TEXT, Rect2(14, 8, w - 60, 26), "left", true)
var text := Style.label(node, sk.text, 12, Style.MUTED, Rect2(14, 34, w, 34), "left")
text.autowrap_mode = TextServer.AUTOWRAP_WORD
now_labels[id] = Style.label(node, "", 12, Style.TEXT, Rect2(14, 70, w, 20), "left")
next_labels[id] = Style.label(node, "", 12, Style.GREEN, Rect2(14, 88, w, 20), "left")
learn_buttons[id] = Style.button(node, "Lernen", Rect2(14, 110, w, 32), learn.bind(id), Style.PRIMARY)
state_labels[id] = Style.label(node, "", 14, Style.MUTED, Rect2(14, 112, w, 28), "center", true)
msg_label = Style.label(self, "", 15, Style.RED, Rect2(0, 770, 1280, 24), "center")
refresh() refresh()
@@ -60,11 +109,31 @@ func refresh() -> void:
var lvl := p.player_level() var lvl := p.player_level()
var base := Skills.xp_for_level(lvl) var base := Skills.xp_for_level(lvl)
level_label.text = tr("Level %d") % lvl level_label.text = tr("Level %d") % lvl
xp_bar.max_value = Skills.xp_for_level(lvl + 1) - base
xp_bar.value = p.xp - base
xp_label.text = tr("%d / %d EP bis Level %d") % [p.xp - base, Skills.xp_for_level(lvl + 1) - base, lvl + 1] xp_label.text = tr("%d / %d EP bis Level %d") % [p.xp - base, Skills.xp_for_level(lvl + 1) - base, lvl + 1]
points_label.text = tr("Skillpunkte: %d") % p.skill_points() points_label.text = tr("Skillpunkte: %d") % p.skill_points()
for id in Skills.SKILLS: for id in Skills.SKILLS:
var sk: Dictionary = Skills.SKILLS[id] var sk: Dictionary = Skills.SKILLS[id]
rank_labels[id].text = tr("Stufe %d / %d") % [p.skill(id), int(sk.max)] var node: SkillNode = nodes[id]
now_labels[id].text = tr("Jetzt: %s") % Skills.effect(id, p.skill(id)) var prev := Skills.previous(id)
next_labels[id].text = tr("Nächste Stufe: %s") % Skills.effect(id, p.skill(id) + 1) if p.skill(id) < int(sk.max) else tr("Höchste Stufe erreicht") node.rank = p.skill(id)
learn_buttons[id].disabled = p.skill_points() <= 0 or p.skill(id) >= int(sk.max) node.open = prev == "" or p.skill(prev) > 0
node.modulate = Color(1, 1, 1, 1 if node.open else 0.55)
node.queue_redraw()
var done := node.rank >= int(sk.max)
now_labels[id].text = tr("Jetzt: %s") % Skills.effect(id, node.rank)
next_labels[id].text = "" if done else tr("Nächste: %s") % Skills.effect(id, node.rank + 1)
var learnable := node.open and not done and p.skill_points() > 0
learn_buttons[id].visible = learnable
var state := ""
if done:
state = tr("Gelernt")
elif not node.open:
state = tr("Erst %s lernen") % tr(Skills.SKILLS[prev].name)
elif not learnable:
state = tr("Kein Skillpunkt frei")
state_labels[id].text = state
state_labels[id].add_theme_color_override("font_color", Style.GREEN if done else Style.MUTED)
if links.has(id):
links[id].color = Style.GOLD if node.open else Style.darker(Style.MUTED, 0.5)
+4 -15
View File
@@ -534,7 +534,7 @@ static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius:
## bei Ultimate Rare golden und als Relief. ## bei Ultimate Rare golden und als Relief.
static func picture(ci: CanvasItem, card, c: Vector2, r: float, rar: String, seed: int) -> void: static func picture(ci: CanvasItem, card, c: Vector2, r: float, rar: String, seed: int) -> void:
var src = card.art_card() var src = card.art_card()
var ring := lighter(Cards.ELEMENT_COLORS[src.elements[0]], 0.3) var ring := lighter(Cards.ELEMENT_COLORS[Cards.strongest_element(src.elements, hash(src.id))], 0.3)
ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.8)) ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.8))
ci.draw_circle(c, r, Color(0.09, 0.09, 0.13, 0.9)) ci.draw_circle(c, r, Color(0.09, 0.09, 0.13, 0.9))
glyph(ci, src, c, r * 1.3, 0.95, rarity_relief(rar)) glyph(ci, src, c, r * 1.3, 0.95, rarity_relief(rar))
@@ -616,23 +616,12 @@ static func glyph(ci: CanvasItem, card, center: Vector2, size: float, alpha: flo
## Symbol in einer festen Farbe (Relief) oder, bei durchsichtigem tint, in Elementfarben. ## Symbol in einer festen Farbe (Relief) oder, bei durchsichtigem tint, in Elementfarben.
static func _glyph_plain(ci: CanvasItem, card, center: Vector2, size: float, tint: Color, alpha: float = 0.95) -> void: static func _glyph_plain(ci: CanvasItem, card, center: Vector2, size: float, tint: Color, alpha: float = 0.95) -> void:
var variant := absi(hash(card.id)) % 3 var variant := absi(hash(card.id)) % 3
var col := lighter(Cards.ELEMENT_COLORS[card.elements[0]], 0.38) var e := Cards.strongest_element(card.elements, hash(card.id)) # mehrere Elemente: nur das stärkste
var col := lighter(Cards.ELEMENT_COLORS[e], 0.38)
col.a = alpha col.a = alpha
if tint.a > 0: if tint.a > 0:
col = tint col = tint
var elems: Array = card.elements _element_glyph(ci, e, center, size, variant, col)
if elems.size() == 1:
_element_glyph(ci, elems[0], center, size, variant, col)
return
# Verschmolzen: die ersten beiden Elemente nebeneinander, ab dem dritten ein Ring
_element_glyph(ci, elems[0], center + Vector2(-size * 0.28, 0), size * 0.62, variant, col)
var col2 := lighter(Cards.ELEMENT_COLORS[elems[1]], 0.38)
col2.a = alpha
if tint.a > 0:
col2 = tint
_element_glyph(ci, elems[1], center + Vector2(size * 0.28, 0), size * 0.62, variant, col2)
if elems.size() >= 3:
ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2)
## Elementsymbol in einer Farbe (Filterknöpfe, Listen), Grundvariante. ## Elementsymbol in einer Farbe (Filterknöpfe, Listen), Grundvariante.
+158
View File
@@ -0,0 +1,158 @@
class_name WorkshopScreen
extends Control
## Werkstatt (nach Kapitel 5): eigene Karten aus Staub bauen. Links Elemente (beliebig viele),
## Angriff, Leben, Eigenschaften (beliebig viele) und der Name; rechts die Vorschau mit Kosten
## (Cards.cost_with), Wert und Preis (Profile.build_price). „Bauen“ legt die Karte als Common in
## Sammlung und Stapel.
var app
var elements: Array = []
var keywords: Array = []
var attack_slider: HSlider
var health_slider: HSlider
var attack_label: Label
var health_label: Label
var element_buttons := {}
var keyword_boxes := {}
var name_box: LineEdit
var info_label: Label
var msg_label: Label
var dust_label: Label
var preview_holder: Control
var built := 0 # für den Vorschlagsnamen
func _ready() -> void:
Style.header(self, "Werkstatt", func(): app.show_screen(MenuScreen.new()))
dust_label = Style.chip(self, "", Rect2(860, 26, 240, 38), Style.BLUE)
Style.panel(self, Rect2(40, 86, 700, 696))
Style.label(self, "Baue eigene Karten aus Staub: so viele Elemente und Eigenschaften du willst, jede kostet Mana.", 14, Style.MUTED, Rect2(60, 98, 660, 22), "left")
Style.label(self, "Elemente", 16, Style.TEXT, Rect2(60, 134, 140, 30), "left", true)
var all_elements: Array = Cards.ELEMENTS + [Cards.NEUTRAL]
for i in all_elements.size():
var e: String = all_elements[i]
var b := DeckScreen.ElementButton.new(e)
b.position = Vector2(200 + i * 48, 130)
b.size = Vector2(40, 40)
b.pressed.connect(toggle_element.bind(e))
Style.juice(b)
add_child(b)
element_buttons[e] = b
Style.label(self, "Angriff", 16, Style.TEXT, Rect2(60, 194, 140, 30), "left", true)
attack_slider = _slider(Rect2(200, 198, 400, 24), 0, 10, 3)
attack_label = Style.label(self, "", 18, Style.AMBER, Rect2(620, 192, 60, 30), "center", true)
Style.label(self, "Leben", 16, Style.TEXT, Rect2(60, 244, 140, 30), "left", true)
health_slider = _slider(Rect2(200, 248, 400, 24), 1, 10, 3)
health_label = Style.label(self, "", 18, Style.LIFE, Rect2(620, 242, 60, 30), "center", true)
Style.label(self, "Eigenschaften", 16, Style.TEXT, Rect2(60, 300, 200, 30), "left", true)
for k in Cards.KEYWORDS.size():
var kw: String = Cards.KEYWORDS[k]
var cb := CheckButton.new()
cb.text = tr(kw)
cb.position = Vector2(200, 300 + k * 44)
cb.size = Vector2(220, 36)
cb.toggled.connect(func(on: bool): toggle_keyword(kw, on))
add_child(cb)
keyword_boxes[kw] = cb
Style.label(self, tr(Cards.KEYWORD_TEXT[kw]), 13, Style.MUTED, Rect2(430, 306 + k * 44, 300, 24), "left")
Style.label(self, "Name", 16, Style.TEXT, Rect2(60, 490, 140, 30), "left", true)
name_box = LineEdit.new()
name_box.position = Vector2(200, 486)
name_box.size = Vector2(400, 36)
name_box.max_length = 24
name_box.text_changed.connect(func(_t: String): refresh())
add_child(name_box)
info_label = Style.label(self, "", 16, Style.GOLD, Rect2(60, 546, 660, 30), "left", true)
Style.button(self, "Bauen", Rect2(60, 600, 300, 50), build, Style.PRIMARY)
msg_label = Style.label(self, "", 15, Style.RED, Rect2(60, 666, 660, 48), "left")
msg_label.autowrap_mode = TextServer.AUTOWRAP_WORD
Style.panel(self, Rect2(760, 86, 480, 696))
preview_holder = Control.new()
preview_holder.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(preview_holder)
_suggest_name()
refresh()
func _slider(rect: Rect2, lo: int, hi: int, value: int) -> HSlider:
var s := HSlider.new()
s.position = rect.position
s.size = rect.size
s.min_value = lo
s.max_value = hi
s.step = 1
s.value = value
s.value_changed.connect(func(_v: float): refresh())
add_child(s)
return s
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel") and not name_box.has_focus():
app.show_screen(MenuScreen.new())
func _suggest_name() -> void:
name_box.text = tr("Eigenbau %d") % (app.profile.customs.size() + 1)
## Ein Element an- oder abwählen.
func toggle_element(e: String) -> void:
if e in elements:
elements.erase(e)
else:
elements.append(e)
say("")
refresh()
func toggle_keyword(kw: String, on: bool) -> void:
if on and not (kw in keywords):
keywords.append(kw)
elif not on:
keywords.erase(kw)
keywords = Cards.KEYWORDS.filter(func(k): return k in keywords)
refresh()
func say(m: String, ok: bool = false) -> void:
msg_label.text = m
msg_label.add_theme_color_override("font_color", Style.GREEN if ok else Style.RED)
## Die Karte, wie sie gebaut würde (für die Vorschau ohne Element neutral).
func preview_card() -> Cards.Card:
var els := elements if not elements.is_empty() else [Cards.NEUTRAL]
return Cards.custom("c_vorschau", name_box.text.strip_edges(), els, int(attack_slider.value), int(health_slider.value), keywords)
func build() -> void:
var p: Profile = app.profile
var err: String = p.build_card(name_box.text, elements, int(attack_slider.value), int(health_slider.value), keywords)
if err != "":
say(tr(err))
return
app.save()
app.sfx.play("combo")
say(tr("Gebaut: %s") % name_box.text.strip_edges(), true)
_suggest_name()
refresh()
func refresh() -> void:
var p: Profile = app.profile
dust_label.text = "Staub: %d" % p.dust
attack_label.text = str(int(attack_slider.value))
health_label.text = str(int(health_slider.value))
for e in element_buttons:
var active: bool = e in elements
var base: Color = Style.darker(Cards.ELEMENT_COLORS[e], 0.8)
element_buttons[e].add_theme_stylebox_override("normal", Style.box(Style.lighter(base) if active else base, 8, Style.GOLD if active else Style.MUTED, 2 if active else 1))
var c := preview_card()
info_label.text = tr("Kosten %d Mana · Wert %d · Preis %d Staub") % [c.cost, c.value(), p.build_price(c)]
Style.clear(preview_holder)
var v := CardView.new(c, Vector2(240, 336), false, "Common")
v.position = Vector2(880, 160)
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
preview_holder.add_child(v)
Style.label(preview_holder, c.text(), 15, Style.MUTED, Rect2(780, 520, 440, 30), "center")
+1
View File
@@ -0,0 +1 @@
uid://cwy4h3joivrfr
+70 -7
View File
@@ -91,6 +91,7 @@ func _initialize() -> void:
test_kapitel_zwei_und_merkmalwahl() test_kapitel_zwei_und_merkmalwahl()
test_einzelspieler_gegner() test_einzelspieler_gegner()
test_skilltree() test_skilltree()
test_werkstatt()
test_ki_zeitbudget() test_ki_zeitbudget()
test_englische_karten() test_englische_karten()
test_uebersetzung_vollstaendig() test_uebersetzung_vollstaendig()
@@ -115,7 +116,7 @@ func test_kartenpool() -> void:
check(C("f_10_10").cost == 10 and C("n_6_1").name == "Eiserner Knappe" and C("w_0_10").name == "Stiller Urleviathan", "10/10 kostet 10; neue Eigenschaften und Wesen ab 6") check(C("f_10_10").cost == 10 and C("n_6_1").name == "Eiserner Knappe" and C("w_0_10").name == "Stiller Urleviathan", "10/10 kostet 10; neue Eigenschaften und Wesen ab 6")
check(C("n_3_2").is_neutral() and C("n_3_2").text() == "Neutral." and not C("f_3_2").is_neutral(), "neutrale Karten tragen die Eigenschaft Neutral") check(C("n_3_2").is_neutral() and C("n_3_2").text() == "Neutral." and not C("f_3_2").is_neutral(), "neutrale Karten tragen die Eigenschaft Neutral")
check(C("f_0_2").cost == 0 and C("f_2_1").cost == 1 and C("f_3_2").cost == 2 and C("f_5_5").cost == 5 and C("e_0_4").cost == 1, "Kostenbeispiele, 0/2 kostet nichts") check(C("f_0_2").cost == 0 and C("f_2_1").cost == 1 and C("f_3_2").cost == 2 and C("f_5_5").cost == 5 and C("e_0_4").cost == 1, "Kostenbeispiele, 0/2 kostet nichts")
check(Cards.cost_of(20, 20) == 10, "Kosten enden bei 10") check(Cards.cost_of(20, 20) == 20, "Kosten ohne Obergrenze")
check(C("f_3_2").name == "Brennender Kobold" and C("w_4_5").name == "Flutender Leviathan", "Namen aus Eigenschaft und Wesen") check(C("f_3_2").name == "Brennender Kobold" and C("w_4_5").name == "Flutender Leviathan", "Namen aus Eigenschaft und Wesen")
var a := C("f_3_2") var a := C("f_3_2")
var b := C("f_3_2") var b := C("f_3_2")
@@ -201,7 +202,7 @@ func test_stapel() -> void:
# Kartenwert: Grundkarten 100, je Stufe +100; Fusionen aus ihren Werten # Kartenwert: Grundkarten 100, je Stufe +100; Fusionen aus ihren Werten
check(Cards.all().all(func(c): return c.value() == 100), "jede Grundkarte ist 100 wert") check(Cards.all().all(func(c): return c.value() == 100), "jede Grundkarte ist 100 wert")
check(C("l_1_1").leveled(4).value() == 500 and C("w_4_5").leveled(2).value() == 300, "Ultimate Rare 500, Super Rare 300") check(C("l_1_1").leveled(4).value() == 500 and C("w_4_5").leveled(2).value() == 300, "Ultimate Rare 500, Super Rare 300")
check(Cards.fused(C("f_3_2"), C("w_4_5")).value() == 100 and Cards.fused(C("f_3_2"), C("w_4_5"), 3).value() == 400, "Dampf: 100, in Stufe 3 400") check(Cards.fused(C("f_3_2"), C("w_4_5")).value() == 200 and Cards.fused(C("f_3_2"), C("w_4_5"), 3).value() == 500, "Dampf: 200 (zweites Element umsonst), in Stufe 3 500")
check(DeckGen.strength(["f_0_1", "l_1_1", "w_4_5"], {"l_1_1": 4, "w_4_5": 2}) == 300 and DeckGen.strength([], {}) == 0 and DeckGen.strength(DeckGen.element_deck(Cards.NEUTRAL), {}) == 100, "Stärke: Mittelwert 300, leer 0, Startsammlung 100") check(DeckGen.strength(["f_0_1", "l_1_1", "w_4_5"], {"l_1_1": 4, "w_4_5": 2}) == 300 and DeckGen.strength([], {}) == 0 and DeckGen.strength(DeckGen.element_deck(Cards.NEUTRAL), {}) == 100, "Stärke: Mittelwert 300, leer 0, Startsammlung 100")
@@ -826,7 +827,7 @@ func test_spielstand_migration() -> void:
func test_kampagne() -> void: func test_kampagne() -> void:
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 4 r.seed = 4
check(Campaign.STAGES_PER_CHAPTER == 10 and Campaign.total() == 50, "fünf Kapitel mit je zehn Stufen") check(Campaign.STAGES_PER_CHAPTER == 10 and Campaign.total() == 100, "zehn Kapitel mit je zehn Stufen")
var boss := Campaign.opponent(4) var boss := Campaign.opponent(4)
check(boss.name == "Vulkar" and boss.title == "Söldnerfürst" and boss.life == 24, "Stufe 5: Vulkar mit 24 Leben") check(boss.name == "Vulkar" and boss.title == "Söldnerfürst" and boss.life == 24, "Stufe 5: Vulkar mit 24 Leben")
check(boss.deck.size() == Game.DECK_MIN and boss.looks.values().all(func(l): return l == 0), "Kapitel 1: Stapel in Common") check(boss.deck.size() == Game.DECK_MIN and boss.looks.values().all(func(l): return l == 0), "Kapitel 1: Stapel in Common")
@@ -861,6 +862,7 @@ func test_zufallssamen() -> void:
Game.test_seed = 0 Game.test_seed = 0
check(a == b, "fester Samen wiederholbar") check(a == b, "fester Samen wiederholbar")
var c := Game.make_rng().randi() var c := Game.make_rng().randi()
OS.delay_usec(50) # randomize() hängt an der Uhr: zwei Aufrufe in derselben Mikrosekunde wären gleich
var d := Game.make_rng().randi() var d := Game.make_rng().randi()
check(c != d, "ohne Samen echter Zufall") check(c != d, "ohne Samen echter Zufall")
@@ -940,7 +942,7 @@ func test_ki_zwei_schritte() -> void:
func test_kapitel_zwei_und_merkmalwahl() -> void: func test_kapitel_zwei_und_merkmalwahl() -> void:
check(Campaign.total() == 50 and Campaign.chapter(9) == 1 and Campaign.chapter(10) == 2 and Campaign.chapter(49) == 5, "fünfzig Stufen in fünf Kapiteln") check(Campaign.total() == 100 and Campaign.chapters() == 10 and Campaign.chapter(9) == 1 and Campaign.chapter(10) == 2 and Campaign.chapter(49) == 5 and Campaign.chapter(99) == 10, "hundert Stufen in zehn Kapiteln")
check(Campaign.stage(25).perks.size() == 2 and Campaign.stage(25).name == "Nyx" and Campaign.stage(25).element == "Wasser", "Kapitel 3: Wasser, zwei Merkmale") check(Campaign.stage(25).perks.size() == 2 and Campaign.stage(25).name == "Nyx" and Campaign.stage(25).element == "Wasser", "Kapitel 3: Wasser, zwei Merkmale")
check(Campaign.stage(45).perks.size() == 3 and Campaign.stage(40).element == "Erde" and Campaign.stage(49).reward == 1500 and Campaign.stage(10).reward == 350 and Campaign.stage(9).reward == 500, "Kapitel 5: Erde, drei Merkmale; Belohnungen 350 bis 1500, Boss 1 gibt 500") check(Campaign.stage(45).perks.size() == 3 and Campaign.stage(40).element == "Erde" and Campaign.stage(49).reward == 1500 and Campaign.stage(10).reward == 350 and Campaign.stage(9).reward == 500, "Kapitel 5: Erde, drei Merkmale; Belohnungen 350 bis 1500, Boss 1 gibt 500")
check(Campaign.stage(0).perks == ["feuer_angriff"] and Campaign.stage(23).perks == ["ziehen_gratis", "kein_nachteil"] and Campaign.stage(49).perks == ["start_energie", "wesen_angriff", "extra_mana"], "Merkmale je drei Plätze versetzt, nichts doppelt") check(Campaign.stage(0).perks == ["feuer_angriff"] and Campaign.stage(23).perks == ["ziehen_gratis", "kein_nachteil"] and Campaign.stage(49).perks == ["start_energie", "wesen_angriff", "extra_mana"], "Merkmale je drei Plätze versetzt, nichts doppelt")
@@ -1009,6 +1011,7 @@ func test_skilltree() -> void:
check(p.player_level() == 0 and p.skill_points() == 0 and p.learn("packglueck") == "Kein Skillpunkt frei", "Start: Level 0, kein Punkt") check(p.player_level() == 0 and p.skill_points() == 0 and p.learn("packglueck") == "Kein Skillpunkt frei", "Start: Level 0, kein Punkt")
var reward := p.record_battle(true, 380) var reward := p.record_battle(true, 380)
check(reward == 380 and p.xp == 380 and p.player_level() == 2 and p.skill_points() == 2 and p.take_notices() == ["Level 2 erreicht: +2 Skillpunkte"], "380 Münzen: Level 2, zwei Punkte, Meldung") check(reward == 380 and p.xp == 380 and p.player_level() == 2 and p.skill_points() == 2 and p.take_notices() == ["Level 2 erreicht: +2 Skillpunkte"], "380 Münzen: Level 2, zwei Punkte, Meldung")
check(p.learn("garantie") == "Erst den Skill davor lernen" and Skills.previous("garantie") == "packglueck" and Skills.previous("packglueck") == "", "Garantie braucht erst Packglück")
check(p.learn("packglueck") == "" and p.skill("packglueck") == 1 and p.skill_points() == 1, "Packglück gelernt") check(p.learn("packglueck") == "" and p.skill("packglueck") == 1 and p.skill_points() == 1, "Packglück gelernt")
check(p.learn("gratisziehen") == "" and p.learn("gratisziehen") == "Schon auf der höchsten Stufe" and p.skill_points() == 0, "Freies Ziehen hat nur eine Stufe") check(p.learn("gratisziehen") == "" and p.learn("gratisziehen") == "Schon auf der höchsten Stufe" and p.skill_points() == 0, "Freies Ziehen hat nur eine Stufe")
check(p.battle_perks() == ["ziehen_gratis"], "Kampfskill wird im Kampf zum Merkmal") check(p.battle_perks() == ["ziehen_gratis"], "Kampfskill wird im Kampf zum Merkmal")
@@ -1016,11 +1019,11 @@ func test_skilltree() -> void:
p.campaign_stage = Campaign.STAGES_PER_CHAPTER p.campaign_stage = Campaign.STAGES_PER_CHAPTER
check(p.battle_perks() == ["leben_35", "ziehen_gratis"], "das gewählte Merkmal kommt dazu") check(p.battle_perks() == ["leben_35", "ziehen_gratis"], "das gewählte Merkmal kommt dazu")
p.xp = 100000 p.xp = 100000
p.learn("packpreis")
p.learn("packpreis")
for i in 3: for i in 3:
p.learn("muenzen") p.learn("muenzen")
p.learn("staub") p.learn("staub")
p.learn("packpreis")
p.learn("packpreis")
check(is_equal_approx(p.coin_factor(), 1.3) and p.pack_price(Cards.NEUTRAL) == 80 and p.pack_price("Feuer") == 160 and p.dust_gain(100) == 175, "Münzsegen ×1,3, Händler −20 %, Staubsammler +75 %") check(is_equal_approx(p.coin_factor(), 1.3) and p.pack_price(Cards.NEUTRAL) == 80 and p.pack_price("Feuer") == 160 and p.dust_gain(100) == 175, "Münzsegen ×1,3, Händler −20 %, Staubsammler +75 %")
var coins := p.coins var coins := p.coins
check(p.record_battle(false, 200) == 260 and p.coins == coins + 260, "260 statt 200 Münzen") check(p.record_battle(false, 200) == 260 and p.coins == coins + 260, "260 statt 200 Münzen")
@@ -1042,7 +1045,67 @@ func test_skilltree() -> void:
var bad := Profile.from_dict({"version": Profile.SAVE_VERSION, "xp": 100, "skills": {"gibt_es_nicht": 2, "packglueck": 9}}) var bad := Profile.from_dict({"version": Profile.SAVE_VERSION, "xp": 100, "skills": {"gibt_es_nicht": 2, "packglueck": 9}})
check(bad.skill("packglueck") == 3 and bad.skills.size() == 1 and bad.player_level() == 1, "unbekannte Skills fliegen raus, Stufen gedeckelt") check(bad.skill("packglueck") == 3 and bad.skills.size() == 1 and bad.player_level() == 1, "unbekannte Skills fliegen raus, Stufen gedeckelt")
check(Skills.pack_weights(0) == PackedFloat32Array([45, 25, 15, 10, 5]) and Skills.pack_weights(3) == PackedFloat32Array([21, 34, 21, 16, 8]), "Packglück 3: Chancen 21/34/21/16/8") check(Skills.pack_weights(0) == PackedFloat32Array([45, 25, 15, 10, 5]) and Skills.pack_weights(3) == PackedFloat32Array([21, 34, 21, 16, 8]), "Packglück 3: Chancen 21/34/21/16/8")
check(Skills.effect("packglueck", 1) == "Chancen 37/28/17/12/6 %" and Skills.effect("packpreis", 2) == "Neutral 80, Element 160 Münzen" and Skills.effect("startmana", 1) == "erster Zug: 3 Mana" and Skills.effect("gratisziehen", 0) == "eine Karte je Zug gratis", "Wirkungstexte in Zahlen") check(Skills.cheap_from(0) == 99 and Skills.cheap_from(1) == 7 and Skills.cheap_from(3) == 3 and Skills.pack_floor(0) == "Rare" and Skills.pack_floor(1) == "Super Rare", "Sparsam ab 7/5/3 Mana, Garantie Super Rare")
var floors := true
for i in 50:
floors = floors and Profile.roll_booster(rr, Cards.NEUTRAL, 0, "Super Rare").any(func(x): return Cards.RARITIES.find(x.rarity) >= 2)
check(floors, "mit Garantie hat jeder Booster eine Super Rare oder besser")
var r2 := RandomNumberGenerator.new()
r2.seed = 4
var g := Game.new_battle_with(Campaign.opponent(0, r2), r2, true, {"deck": DeckGen.element_deck(Cards.NEUTRAL).slice(0, 30), "bonus_attack": 2, "bonus_health": 1, "cheap_from": 3})
check(g.human.hand.all(func(c): return c.attack == Cards.by_id(c.id).attack + 2 and c.health == Cards.by_id(c.id).health + 1 and c.cost == Cards.by_id(c.id).cost - (1 if Cards.by_id(c.id).cost >= 3 else 0)), "Stark 2, Zäh 1, Sparsam 3: Handkarten +2/+1, ab 3 Mana 1 billiger")
check(g.opponent.hand.all(func(c): return c.attack == Cards.by_id(c.id).attack and c.cost == Cards.by_id(c.id).cost) and g.human.clone().bonus_attack == 2 and g.human.clone().cheap_from == 3, "der Gegner bleibt wie er ist; der Klon für die Vorausschau trägt die Boni")
var h := g.human
var a0 := Cards.by_id(h.hand[0].id)
var b0 := Cards.by_id(h.hand[1].id)
var f := Game.fusion_result(h, h.hand[0], h.hand[1])
var fa := Cards.fusion_value(a0.attack, b0.attack)
var fh := Cards.fusion_value(a0.health, b0.health)
check(h.hand[0].base_attack() == a0.attack and f.base_attack() == fa and f.attack == fa + 2 and f.health == fh + 1 and f.cost == Cards.cost_of(fa, fh) - (1 if Cards.cost_of(fa, fh) >= 3 else 0), "Fusion rechnet ohne Kraft und bekommt Kraft und Sparsam danach")
check(Skills.effect("packglueck", 1) == "Chancen 37/28/17/12/6 %" and Skills.effect("packpreis", 2) == "Neutral 80, Element 160" and Skills.effect("startmana", 1) == "erster Zug: 3 Mana" and Skills.effect("gratisziehen", 0) == "eine Karte je Zug gratis", "Wirkungstexte in Zahlen")
## Werkstatt und Kapitel 6 bis 10: Eigenbauten mit Eigenschaften, Effektkarten der Gegner.
func test_werkstatt() -> void:
check(Cards.cost_with(3, 2, [], ["Feuer"]) == 2 and Cards.cost_with(3, 2, ["Spott"], ["Feuer"]) == 3 and Cards.cost_with(3, 2, ["Spott", "Flink"], ["Feuer", "Wasser"]) == 5 and Cards.cost_with(10, 10, ["Spott", "Flink"], ["Feuer", "Wasser"]) == 15, "Kosten: Spott 1, Flink Angriff/3, zweites Element 1, über 10 hinaus")
check(Cards.keyword_cost("Doppelschlag", 10, 10) == 6 and Cards.keyword_cost("Doppelschlag", 1, 1) == 1 and Cards.keyword_cost("Regeneration", 3, 9) == 3 and Cards.element_cost(1) == 0 and Cards.element_cost(3) == 3 and Cards.cost_with(10, 10, ["Doppelschlag", "Flink"], Cards.ELEMENTS + [Cards.NEUTRAL]) == 10 + 6 + 3 + 10, "Doppelschlag 2·Angriff/3, Regeneration Leben/3, Elemente 1+2+3+4; zusammen 29")
check(Cards.strongest_element(["Feuer", "Wasser"], 0) == "Wasser" and Cards.strongest_element(["Luft", "Feuer"], 5) == "Feuer" and Cards.strongest_element(["Feuer", "Erde"], 0) == "Feuer" and Cards.strongest_element(["Feuer", "Erde"], 1) == "Erde" and Cards.strongest_element([Cards.NEUTRAL], 3) == Cards.NEUTRAL, "stärkstes Element: schlägt die meisten anderen, Gleichstand nach seed")
var c := Cards.custom("c_test", "Testdrache", ["Feuer"], 3, 2, ["Spott"])
check(c.cost == 3 and c.value() == 200 and c.is_custom() and c.text() == "Spott." and not C("f_3_2").is_custom(), "Eigenbau: Kosten 3, Wert 200, Eigenschaft im Text")
Cards.register(c)
check(Cards.by_id("c_test") == c and Cards.has_id("c_test") and Cards.all().size() == 550, "registriert, aber nicht im Pool")
check(Cards.custom("c_y", "Y", ["Feuer", "Wasser"], 3, 2, []).value() == 100 and c.leveled(2).value() == 400, "zweites Element fair bezahlt: 100; Stufe 2 mit Eigenschaft: 400")
var p := Profile.starter()
check(not p.workshop_unlocked() and p.build_card("X", ["Feuer"], 3, 2, ["Spott"]) == "Werkstatt noch gesperrt", "Werkstatt erst nach Kapitel 5")
p.campaign_stage = 5 * Campaign.STAGES_PER_CHAPTER
check(p.workshop_unlocked() and p.unlocked_elements().size() == 5, "nach Kapitel 5 offen, alle Elemente")
check(p.build_card("", ["Feuer"], 3, 2, []) == "Name fehlt" and p.build_card("X", [], 3, 2, []) == "Mindestens ein Element" and p.build_card("X", ["Feuer"], 11, 2, []) == "Angriff 0 bis 10, Leben 1 bis 10", "Prüfungen")
var price := p.build_price(Cards.custom("c_x", "X", ["Feuer", "Wasser"], 3, 2, ["Spott"]))
check(price == 2 * 200 + 100, "Preis: doppelter Wert plus 100 fürs zweite Element")
p.dust = price - 1
check(p.build_card("X", ["Feuer", "Wasser"], 3, 2, ["Spott"]) == "Nicht genug Staub (%d nötig)" % price, "zu wenig Staub")
p.dust = price
check(p.build_card("Dampfwächter", ["Feuer", "Wasser"], 3, 2, ["Spott"]) == "" and p.dust == 0 and p.owns("c_1") and "c_1" in p.deck and Cards.by_id("c_1").name == "Dampfwächter" and Cards.by_id("c_1").cost == 4, "gebaut: in Sammlung und Stapel, Kosten 2 + 1 + 1")
check(p.unlocked_cards().any(func(x): return x.id == "c_1") and p.customs.size() == 1, "Eigenbau in der Sammlung sichtbar")
p.dust = 200
check(p.build_card("Kiesel", ["Erde"], 1, 1, []) == "" and p.owns("c_2") and p.dust == 0, "zweiter Eigenbau: c_2 für 200")
p.dust = 1400
check(p.build_card("Alles", Cards.ELEMENTS + [Cards.NEUTRAL], 1, 1, Cards.KEYWORDS) == "" and p.dust == 0 and Cards.by_id("c_3").cost == 1 + 10 + 4 and Cards.by_id("c_3").elements.size() == 5, "alle Elemente und Eigenschaften erlaubt: Kosten 15, Preis 1400")
var d := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
check(d.owns("c_1") and d.customs.size() == 3 and Cards.by_id("c_1").keywords == ["Spott"] and Cards.by_id("c_1").elements == ["Feuer", "Wasser"], "Eigenbauten gespeichert und wieder registriert")
check(DeckGen.strength(["c_1", "c_2"], {}) == 150, "Deckstärke zählt Eigenschaften: (200 + 100) / 2")
# Kapitel 6 bis 10
check(Campaign.element_of(6) == Cards.NEUTRAL and Campaign.element_of(10) == "Erde" and Campaign.keyword_count(5) == 0 and Campaign.keyword_count(6) == 1 and Campaign.keyword_count(8) == 2, "Elemente wiederholen sich; ab Kapitel 6 eine, ab 8 zwei Eigenschaften")
check(Campaign.rarity_of(5) == 3 and Campaign.rarity_of(6) == 3 and Campaign.rarity_of(9) == 3 and Campaign.rarity_of(10) == 4 and Campaign.perk_count(9) == 3, "Ultra Rare ab Kapitel 5, Kapitel 10 Ultimate; höchstens drei Merkmale")
check(Campaign.stage(50).title.ends_with(" II") and Campaign.stage(50).keywords == 1 and Campaign.stage(99).reward == 100 + 250 * 9 + 400 and Campaign.stage(99).name == "Archimagier", "zweite Runde: Titel II, Belohnung 2750")
var r := RandomNumberGenerator.new()
r.seed = 33
var o := Campaign.opponent(75, r)
check(o.deck.size() == Game.DECK_MIN and o.deck.all(func(id): return Cards.by_id(id).id.begins_with("g_") and Cards.by_id(id).keywords.size() == 2 and Cards.by_id(id).elements == ["Wasser"] and Cards.by_id(id).cost <= 6) and o.looks.values().all(func(l): return l == 3), "Kapitel 8, Stufe 6: Wasser-Effektkarten mit zwei Eigenschaften bis Kosten 6, Ultra Rare")
var g := Game.new_battle_with(o, r, false, {"deck": DeckGen.element_deck(Cards.NEUTRAL).slice(0, 30)})
check(g.opponent.hand.all(func(k): return k.keywords.size() == 2 and k.look == 3) and g.opponent.hand[0].attack == g.opponent.hand[0].base_attack() + 3, "im Kampf tragen die Karten ihre Eigenschaften und die Stufe")
var cr := Game.Creature.new(g.opponent.hand[0], g.opponent, 7777)
check(g.opponent.hand[0].keywords.all(func(k): return cr.has(k)), "das Wesen hat die Eigenschaften der Karte")
func test_ki_zeitbudget() -> void: func test_ki_zeitbudget() -> void:
+49 -6
View File
@@ -185,7 +185,7 @@ func _ready() -> void:
main.sfx.muted = true # Tests bleiben still main.sfx.muted = true # Tests bleiben still
await frames(3) await frames(3)
var suite := [test_start, 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_skilltree, test_cheats, test_deck, test_sprache, test_kampf_steuerung, test_kampf_elemente_und_verschmelzen, test_kampf_grenzen] test_kampagne, test_sammlung, test_skilltree, test_cheats, test_werkstatt, 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]
suite.append(test_profil_zuruecksetzen) suite.append(test_profil_zuruecksetzen)
var only := OS.get_environment("DINGER_ONLY") # z. B. DINGER_ONLY=test_sammlung,test_kampf var only := OS.get_environment("DINGER_ONLY") # z. B. DINGER_ONLY=test_sammlung,test_kampf
@@ -501,13 +501,15 @@ func test_skilltree() -> void:
main.save() main.save()
await click(find_button("Skilltree")) await click(find_button("Skilltree"))
var scr = screen() var scr = screen()
check(scr is SkillScreen and scr.level_label.text == "Level 3" and scr.points_label.text == "Skillpunkte: 3", "Skilltree: Level 3, drei Punkte") check(scr is SkillScreen and scr.level_label.text == "Level 3" and scr.points_label.text == "Skillpunkte: 3" and scr.xp_label.text == "0 / 400 EP bis Level 4" and scr.xp_bar.value == 0 and scr.xp_bar.max_value == 400, "Skilltree: Level 3, drei Punkte, Balken leer")
check(scr.learn_buttons.size() == Skills.SKILLS.size() and not scr.learn_buttons["packglueck"].disabled and scr.xp_label.text == "0 / 400 EP bis Level 4", "alle Skills mit Lernen-Knopf, Fortschritt bis Level 4") check(scr.nodes.size() == Skills.SKILLS.size() and scr.learn_buttons["packglueck"].visible and not scr.learn_buttons["garantie"].visible and scr.state_labels["garantie"].text == "Erst Packglück lernen" and not scr.nodes["garantie"].open, "erste Knoten lernbar, Garantie gesperrt")
check(scr.now_labels["packglueck"].text == "Jetzt: Chancen 45/25/15/10/5 %" and scr.next_labels["packglueck"].text == "Nächste Stufe: Chancen 37/28/17/12/6 %", "Wirkung jetzt und nächste Stufe in Zahlen") check(scr.now_labels["packglueck"].text == "Jetzt: Chancen 45/25/15/10/5 %" and scr.next_labels["packglueck"].text == "Nächste: Chancen 37/28/17/12/6 %", "Wirkung jetzt und nächste Stufe in Zahlen")
await click(scr.learn_buttons["packglueck"]) await click(scr.learn_buttons["packglueck"])
check(main.profile.skill("packglueck") == 1 and scr.points_label.text == "Skillpunkte: 2" and scr.rank_labels["packglueck"].text == "Stufe 1 / 3", "Packglück gelernt, Punkt verbraucht") check(main.profile.skill("packglueck") == 1 and scr.points_label.text == "Skillpunkte: 2" and scr.nodes["packglueck"].rank == 1 and scr.learn_buttons["garantie"].visible and scr.nodes["garantie"].open, "Packglück gelernt, Garantie offen")
await click(scr.learn_buttons["gratisziehen"]) await click(scr.learn_buttons["gratisziehen"])
check(scr.learn_buttons["gratisziehen"].disabled and scr.rank_labels["gratisziehen"].text == "Stufe 1 / 1" and scr.next_labels["gratisziehen"].text == "Höchste Stufe erreicht", "Freies Ziehen voll: Knopf aus") check(not scr.learn_buttons["gratisziehen"].visible and scr.state_labels["gratisziehen"].text == "Gelernt" and scr.next_labels["gratisziehen"].text == "", "Freies Ziehen voll: Gelernt statt Knopf")
await click(scr.learn_buttons["startmana"])
check(scr.points_label.text == "Skillpunkte: 0" and not scr.learn_buttons["garantie"].visible and scr.state_labels["garantie"].text == "Kein Skillpunkt frei", "ohne Punkte kein Knopf")
await shot("28_skilltree") await shot("28_skilltree")
check(Profile.load_or_new(SAVE).skill("packglueck") == 1, "gespeichert") check(Profile.load_or_new(SAVE).skill("packglueck") == 1, "gespeichert")
await click(find_button("Zurück")) await click(find_button("Zurück"))
@@ -541,6 +543,47 @@ func test_cheats() -> void:
check(screen() is MenuScreen, "zurück im Menü") check(screen() is MenuScreen, "zurück im Menü")
## Werkstatt: nach Kapitel 5 im Menü; Elemente, Werte, Eigenschaften und Name ergeben Vorschau und Preis.
func test_werkstatt() -> void:
check(find_button("Werkstatt") == null, "vor Kapitel 5 keine Werkstatt im Menü")
main.profile.campaign_stage = 5 * Campaign.STAGES_PER_CHAPTER
main.profile.dust = 1000
main.save()
main.show_screen(MenuScreen.new())
await frames(2)
check(find_button("Werkstatt") != null, "nach Kapitel 5 steht die Werkstatt im Menü")
await click(find_button("Werkstatt"))
var scr = screen()
check(scr is WorkshopScreen and scr.dust_label.text == "Staub: 1000" and scr.name_box.text == "Eigenbau 1", "Werkstatt geöffnet, Vorschlagsname")
await click(scr.element_buttons["Feuer"])
await click(scr.element_buttons["Wasser"])
scr.attack_slider.value = 3
scr.health_slider.value = 2
await click(scr.keyword_boxes["Spott"])
scr.name_box.text = "Dampfwächter"
scr.name_box.text_changed.emit("Dampfwächter")
await frames(1)
check(scr.elements == ["Feuer", "Wasser"] and scr.keywords == ["Spott"] and scr.info_label.text == "Kosten 4 Mana · Wert 200 · Preis 500 Staub", "Vorschau: Kosten 4, Wert 200, Preis 500: " + scr.info_label.text)
check(scr.preview_holder.get_child(0).card.name == "Dampfwächter" and scr.preview_holder.get_child(0).card.cost == 4, "Vorschaukarte zeigt Name und Kosten")
await click(scr.element_buttons["Erde"])
await click(scr.keyword_boxes["Doppelschlag"])
await click(scr.keyword_boxes["Flink"])
check(scr.elements == ["Feuer", "Wasser", "Erde"] and scr.keywords == ["Spott", "Flink", "Doppelschlag"] and scr.info_label.text == "Kosten 9 Mana · Wert 400 · Preis 1000 Staub", "drei Elemente, drei Eigenschaften in fester Reihenfolge: " + scr.info_label.text)
await shot("30_werkstatt")
await click(scr.element_buttons["Erde"])
await click(scr.keyword_boxes["Doppelschlag"])
await click(scr.keyword_boxes["Flink"])
check(scr.elements == ["Feuer", "Wasser"] and scr.keywords == ["Spott"], "wieder abgewählt")
await click(find_button("Bauen"))
var p: Profile = main.profile
check(p.owns("c_1") and p.dust == 500 and "c_1" in p.deck and scr.msg_label.text == "Gebaut: Dampfwächter" and scr.name_box.text == "Eigenbau 2", "gebaut: 500 Staub weg, Karte in Sammlung und Stapel")
check(Profile.load_or_new(SAVE).owns("c_1") and Cards.by_id("c_1").keywords == ["Spott"] and Cards.by_id("c_1").elements == ["Feuer", "Wasser"], "gespeichert")
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")
main.profile.campaign_stage = 0
main.save()
## Deck: eigene Karten rein und raus, Auffüllen nach Filter, Speichern. Beginnt mit einem frischen Profil. ## Deck: eigene Karten rein und raus, Auffüllen nach Filter, Speichern. Beginnt mit einem frischen Profil.
func test_deck() -> void: func test_deck() -> void:
main.reset_profile() main.reset_profile()
+40 -8
View File
@@ -77,6 +77,7 @@ Erde,Earth
Luft,Air Luft,Air
%s zerlegt (+%d Staub),%s dismantled (+%d dust) %s zerlegt (+%d Staub),%s dismantled (+%d dust)
Kapitel %d,Chapter %d Kapitel %d,Chapter %d
%s mit Effektkarten,%s with effect cards
Zuschauen,Watch Zuschauen,Watch
Zuschauer,Spectator Zuschauer,Spectator
"Zuschauer: Hände sind verdeckt","Spectator: hands are hidden" "Zuschauer: Hände sind verdeckt","Spectator: hands are hidden"
@@ -91,33 +92,64 @@ Level %d,Level %d
%d / %d EP bis Level %d,%d / %d XP to level %d %d / %d EP bis Level %d,%d / %d XP to level %d
Skillpunkte: %d,Skill points: %d Skillpunkte: %d,Skill points: %d
Lernen,Learn Lernen,Learn
Stufe %d / %d,Rank %d / %d Glück,Luck
Sammeln,Collecting Kraft,Power
Weisheit,Wisdom
Wirtschaft,Economy Wirtschaft,Economy
Kampf,Battle
Packglück,Pack luck Packglück,Pack luck
Staubsammler,Dust collector Staubsammler,Dust collector
Münzsegen,Coin blessing Münzsegen,Coin blessing
Händler,Merchant Händler,Merchant
Freies Ziehen,Free draw Freies Ziehen,Free draw
Starker Start,Strong start Starker Start,Strong start
Garantie,Guarantee
Zäh,Tough
Stark,Strong
Sparsam,Thrifty
Mindestens eine Super Rare je Booster,At least one Super Rare per booster
Eigene Karten haben mehr Leben,Your cards have more health
Eigene Karten haben mehr Angriff,Your cards have more attack
Teure Karten kosten 1 Mana weniger,Expensive cards cost 1 mana less
Rare je Pack sicher,Rare per pack guaranteed
Super Rare je Pack sicher,Super Rare per pack guaranteed
+%d Leben,+%d health
+%d Angriff,+%d attack
ab %d Mana 1 billiger,1 cheaper from %d mana
voller Preis,full price
Gelernt,Learned
Erst %s lernen,Learn %s first
Erst den Skill davor lernen,Learn the previous skill first
Seltenere Karten in Boostern,Rarer cards in boosters Seltenere Karten in Boostern,Rarer cards in boosters
Mehr Staub aus Doppelten und Zerlegen,More dust from duplicates and dismantling Mehr Staub aus Doppelten und Zerlegen,More dust from duplicates and dismantling
Mehr Münzen nach jedem Kampf,More coins after every battle Mehr Münzen nach jedem Kampf,More coins after every battle
Booster werden billiger,Boosters get cheaper Booster werden billiger,Boosters get cheaper
Das zweite Ziehen je Zug ist auch gratis,The second draw each turn is free as well Zweites Ziehen je Zug auch gratis,Second draw each turn free as well
Mehr Mana im ersten Zug,More mana in the first turn Mehr Mana im ersten Zug,More mana in the first turn
Chancen %d/%d/%d/%d/%d %%,Odds %d/%d/%d/%d/%d %% Chancen %d/%d/%d/%d/%d %%,Odds %d/%d/%d/%d/%d %%
+%d %% Staub,+%d %% dust +%d %% Staub,+%d %% dust
+%d %% Münzen,+%d %% coins +%d %% Münzen,+%d %% coins
"Neutral %d, Element %d Münzen","Neutral %d, element %d coins" "Neutral %d, Element %d","Neutral %d, element %d"
zwei Karten je Zug gratis,two cards per turn for free zwei Karten je Zug gratis,two cards per turn for free
eine Karte je Zug gratis,one card per turn for free eine Karte je Zug gratis,one card per turn for free
erster Zug: %d Mana,first turn: %d mana erster Zug: %d Mana,first turn: %d mana
Jetzt: %s,Now: %s Jetzt: %s,Now: %s
Nächste Stufe: %s,Next rank: %s Nächste: %s,Next: %s
Höchste Stufe erreicht,Highest rank reached
Cheats,Cheats Cheats,Cheats
Werkstatt,Workshop
Werkstatt frei,Unlock workshop
"Baue eigene Karten aus Staub: so viele Elemente und Eigenschaften du willst, jede kostet Mana.","Build your own cards from dust: as many elements and keywords as you like, each adds mana."
Angriff,Attack
Leben,Health
Eigenschaften,Keywords
Name,Name
Bauen,Build
Kosten %d Mana · Wert %d · Preis %d Staub,Cost %d mana · value %d · price %d dust
Gebaut: %s,Built: %s
Eigenbau %d,Custom %d
Werkstatt noch gesperrt,Workshop still locked
Name fehlt,Name missing
Mindestens ein Element,At least one element
"Angriff 0 bis 10, Leben 1 bis 10","Attack 0 to 10, health 1 to 10"
Währung,Currency Währung,Currency
Erfahrung,Experience Erfahrung,Experience
+1.000 Münzen,"+1,000 coins" +1.000 Münzen,"+1,000 coins"
@@ -141,7 +173,7 @@ Gespeichert,Saved
"Münzen %d · Staub %d · EP %d (Level %d, %d Skillpunkte frei) · Kampagne %d/%d · Sammlung %d Karten, Stapel %d","Coins %d · Dust %d · XP %d (level %d, %d skill points free) · Campaign %d/%d · Collection %d cards, deck %d" "Münzen %d · Staub %d · EP %d (Level %d, %d Skillpunkte frei) · Kampagne %d/%d · Sammlung %d Karten, Stapel %d","Coins %d · Dust %d · XP %d (level %d, %d skill points free) · Campaign %d/%d · Collection %d cards, deck %d"
Schon auf der höchsten Stufe,Already at the highest rank Schon auf der höchsten Stufe,Already at the highest rank
Kein Skillpunkt frei,No skill point available Kein Skillpunkt frei,No skill point available
Erfahrung sind deine verdienten Münzen. Jedes Level bringt einen Skillpunkt.,Experience is the coins you earn. Every level grants one skill point. Erfahrung sind deine verdienten Münzen. Jedes Level bringt einen Skillpunkt. Jeder Ast öffnet sich von oben nach unten.,Experience is the coins you earn. Every level grants one skill point. Each branch opens from top to bottom.
%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
1 keys en
77 Luft Air
78 %s zerlegt (+%d Staub) %s dismantled (+%d dust)
79 Kapitel %d Chapter %d
80 %s mit Effektkarten %s with effect cards
81 Zuschauen Watch
82 Zuschauer Spectator
83 Zuschauer: Hände sind verdeckt Spectator: hands are hidden
92 %d / %d EP bis Level %d %d / %d XP to level %d
93 Skillpunkte: %d Skill points: %d
94 Lernen Learn
95 Stufe %d / %d Glück Rank %d / %d Luck
96 Sammeln Kraft Collecting Power
97 Weisheit Wisdom
98 Wirtschaft Economy
Kampf Battle
99 Packglück Pack luck
100 Staubsammler Dust collector
101 Münzsegen Coin blessing
102 Händler Merchant
103 Freies Ziehen Free draw
104 Starker Start Strong start
105 Garantie Guarantee
106 Zäh Tough
107 Stark Strong
108 Sparsam Thrifty
109 Mindestens eine Super Rare je Booster At least one Super Rare per booster
110 Eigene Karten haben mehr Leben Your cards have more health
111 Eigene Karten haben mehr Angriff Your cards have more attack
112 Teure Karten kosten 1 Mana weniger Expensive cards cost 1 mana less
113 Rare je Pack sicher Rare per pack guaranteed
114 Super Rare je Pack sicher Super Rare per pack guaranteed
115 +%d Leben +%d health
116 +%d Angriff +%d attack
117 ab %d Mana 1 billiger 1 cheaper from %d mana
118 voller Preis full price
119 Gelernt Learned
120 Erst %s lernen Learn %s first
121 Erst den Skill davor lernen Learn the previous skill first
122 Seltenere Karten in Boostern Rarer cards in boosters
123 Mehr Staub aus Doppelten und Zerlegen More dust from duplicates and dismantling
124 Mehr Münzen nach jedem Kampf More coins after every battle
125 Booster werden billiger Boosters get cheaper
126 Das zweite Ziehen je Zug ist auch gratis Zweites Ziehen je Zug auch gratis The second draw each turn is free as well Second draw each turn free as well
127 Mehr Mana im ersten Zug More mana in the first turn
128 Chancen %d/%d/%d/%d/%d %% Odds %d/%d/%d/%d/%d %%
129 +%d %% Staub +%d %% dust
130 +%d %% Münzen +%d %% coins
131 Neutral %d, Element %d Münzen Neutral %d, Element %d Neutral %d, element %d coins Neutral %d, element %d
132 zwei Karten je Zug gratis two cards per turn for free
133 eine Karte je Zug gratis one card per turn for free
134 erster Zug: %d Mana first turn: %d mana
135 Jetzt: %s Now: %s
136 Nächste Stufe: %s Nächste: %s Next rank: %s Next: %s
Höchste Stufe erreicht Highest rank reached
137 Cheats Cheats
138 Werkstatt Workshop
139 Werkstatt frei Unlock workshop
140 Baue eigene Karten aus Staub: so viele Elemente und Eigenschaften du willst, jede kostet Mana. Build your own cards from dust: as many elements and keywords as you like, each adds mana.
141 Angriff Attack
142 Leben Health
143 Eigenschaften Keywords
144 Name Name
145 Bauen Build
146 Kosten %d Mana · Wert %d · Preis %d Staub Cost %d mana · value %d · price %d dust
147 Gebaut: %s Built: %s
148 Eigenbau %d Custom %d
149 Werkstatt noch gesperrt Workshop still locked
150 Name fehlt Name missing
151 Mindestens ein Element At least one element
152 Angriff 0 bis 10, Leben 1 bis 10 Attack 0 to 10, health 1 to 10
153 Währung Currency
154 Erfahrung Experience
155 +1.000 Münzen +1,000 coins
173 Münzen %d · Staub %d · EP %d (Level %d, %d Skillpunkte frei) · Kampagne %d/%d · Sammlung %d Karten, Stapel %d Coins %d · Dust %d · XP %d (level %d, %d skill points free) · Campaign %d/%d · Collection %d cards, deck %d
174 Schon auf der höchsten Stufe Already at the highest rank
175 Kein Skillpunkt frei No skill point available
176 Erfahrung sind deine verdienten Münzen. Jedes Level bringt einen Skillpunkt. Erfahrung sind deine verdienten Münzen. Jedes Level bringt einen Skillpunkt. Jeder Ast öffnet sich von oben nach unten. Experience is the coins you earn. Every level grants one skill point. Experience is the coins you earn. Every level grants one skill point. Each branch opens from top to bottom.
177 %s ist jetzt %s %s is now %s
178 Alle Kapitel abgeschlossen All chapters completed
179 Elemente Elements
Binary file not shown.