Kampf fortsetzen, Gamepad im Deckbau, Deck-Codes, Kapitel 3, Elementbooster, englische Karten, Host-Wechsel und Zuschauer
- KI-Bewertung mit Bedrohung und Führenden-Bremse (62:38 gegen die alte im Duell, gleichauf zu viert) - Balance: Sand +1/+1 statt Schild, Erde+Luft und Erde geschwächt, Wasser angehoben, Messlauf mit N=200 möglich - Einzelspieler-Kämpfe werden laufend gespeichert (voller Spielstand, viewer -1) und im Menü fortgesetzt - Karten, Deckzeilen und Kartenrücken fokussierbar: Gamepad in Deckbau, Werkstatt und Booster - Kartennamen (name_en) und Kartentexte übersetzbar, Übersetzungstest deckt cards.gd mit ab - Deck-Code DINGER1:id*anzahl mit Zwischenablage; acht neue Karten; Kampagne Kapitel 3; Elementbooster 120 Münzen - Netzwerk: Nachfolge-Host mit vollem Spielstand, Neuverbindung der anderen, Zuschauer (Sicht -2); Bot kann hosten - Große Schrift: Kartennamen, Kartentext, Knopfleisten, Gegnervorschau, Dialogtext - Anleitung 1 garantiert ein 1-Energie-Wesen; Tests: DINGER_ONLY für einzelne Oberflächentests - Oberflächentest auch auf Xephyr mit llvmpipe (wie Xvfb in der CI) grün Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
11
README.md
11
README.md
@@ -41,6 +41,15 @@ Der Spielstand liegt unter `user://spielstand.json`
|
|||||||
- Lava trifft nur noch das stärkste gegnerische Wesen; Archetypen-Werte kommen aus `data/archetypen.json` (Messlauf schreibt sie)
|
- Lava trifft nur noch das stärkste gegnerische Wesen; Archetypen-Werte kommen aus `data/archetypen.json` (Messlauf schreibt sie)
|
||||||
- Kartentext passt sich der Schriftgröße an, Gamepad auch in Menüs (Fokusrahmen), Anleitung 2 erzwingt Maras Angriff in die Falle
|
- Kartentext passt sich der Schriftgröße an, Gamepad auch in Menüs (Fokusrahmen), Anleitung 2 erzwingt Maras Angriff in die Falle
|
||||||
- KI-Vorausschau in zwei Stufen (erst alle Züge mit den besten Zielen, dann die acht besten vertieft): bei 5 Gegnern 0,1 s statt 2 s, und sie rechnet im Hintergrund-Thread, Musik ebenso
|
- KI-Vorausschau in zwei Stufen (erst alle Züge mit den besten Zielen, dann die acht besten vertieft): bei 5 Gegnern 0,1 s statt 2 s, und sie rechnet im Hintergrund-Thread, Musik ebenso
|
||||||
|
- KI-Bewertung mit Bedrohung (gegnerische Angriffskraft ohne eigenen Wächter, tödliche besonders) und Führenden-Bremse; im Duell 62:38 gegen die alte Bewertung
|
||||||
|
- Unterbrochene Einzelspieler-Kämpfe (auch Kampagne und Herausforderung) werden laufend gespeichert und lassen sich im Menü fortsetzen
|
||||||
|
- Gamepad im Deckbau, in der Werkstatt und beim Booster: Karten, Deckzeilen und Kartenrücken sind fokussierbar, A wählt, B geht zurück
|
||||||
|
- Englisch auch für Kartennamen und Kartentexte (`name_en` in `data/karten.json`, Textbausteine über `tr()`)
|
||||||
|
- Deck-Codes zum Teilen (`DINGER1:id*anzahl,…`) mit Zwischenablage im Deckbau
|
||||||
|
- Kapitel 3 der Kampagne (drei Merkmale, volle Tische), acht neue Karten (je Elementpaar eine, zwei Fallen), Elementbooster für 120 Münzen
|
||||||
|
- Netzwerk: fällt der Host aus, übernimmt der Mitspieler mit der kleinsten Kennung (er bekommt laufend den vollen Spielstand), die anderen verbinden neu; Zuschauer sehen die Partie ohne Hände
|
||||||
|
- Balance: Sand gibt +1/+1 statt Schild, Erde+Luft und Erde leicht geschwächt, Wasser angehoben; Archetypen mit 200 Partien je Kombination gemessen
|
||||||
|
- Große Schrift: Kartennamen neben dem Kostenkreis, Kartentext läuft nicht mehr über, breitere Knöpfe in Booster, Deckbau und Werkstatt
|
||||||
- Übersetzungsprüfung: jeder tr()-Text und jeder Anleitungsschritt braucht einen englischen Eintrag
|
- Übersetzungsprüfung: jeder tr()-Text und jeder Anleitungsschritt braucht einen englischen Eintrag
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
@@ -57,6 +66,8 @@ godot --headless --path . -s tests/simulate.gd # Balance: KI spielt gegen K
|
|||||||
godot --headless --path . -s tests/matchup.gd # Startvorteil und Mono gegen Paar
|
godot --headless --path . -s tests/matchup.gd # Startvorteil und Mono gegen Paar
|
||||||
godot --headless --path . -s tests/strength_probe.gd # Stärke der Elementkombinationen messen (Werte in DeckGen.ARCHETYPE_STRENGTH)
|
godot --headless --path . -s tests/strength_probe.gd # Stärke der Elementkombinationen messen (Werte in DeckGen.ARCHETYPE_STRENGTH)
|
||||||
DINGER_SHOTS=/tmp/shots godot --path . tests/ui_test.tscn # Oberfläche mit Fenster und Screenshots
|
DINGER_SHOTS=/tmp/shots godot --path . tests/ui_test.tscn # Oberfläche mit Fenster und Screenshots
|
||||||
|
DINGER_ONLY=test_host_wechsel,test_zuschauer godot --path . tests/ui_test.tscn # nur einzelne Oberflächentests
|
||||||
|
N=200 godot --headless --path . -s tests/strength_probe.gd # Messlauf mit 200 statt 60 Partien je Archetyp
|
||||||
godot --headless --path . -s tests/perf_probe.gd # Laufzeiten: KI-Entscheidung, Deckerzeugung, Musik
|
godot --headless --path . -s tests/perf_probe.gd # Laufzeiten: KI-Entscheidung, Deckerzeugung, Musik
|
||||||
godot --path . tests/frame_probe.tscn # Bildzeiten im Kampf mit 5 Gegnern
|
godot --path . tests/frame_probe.tscn # Bildzeiten im Kampf mit 5 Gegnern
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -6,14 +6,15 @@
|
|||||||
- Jeder gegen jeden: Spieler und 1 bis 5 KI-Gegner (oder Spieler), alle mit gleichen Regeln
|
- Jeder gegen jeden: Spieler und 1 bis 5 KI-Gegner (oder Spieler), alle mit gleichen Regeln
|
||||||
- Einzelspieler-Knopf führt zur Auswahl der Gegnerzahl
|
- Einzelspieler-Knopf führt zur Auswahl der Gegnerzahl
|
||||||
- Kampagne gegen benannte Magier, Erfolge und Tagesaufgaben als Fortschritt
|
- Kampagne gegen benannte Magier, Erfolge und Tagesaufgaben als Fortschritt
|
||||||
- Lokal zu mehreren am selben Rechner und im lokalen Netz möglich
|
- Lokal zu mehreren am selben Rechner und im lokalen Netz möglich, auch als Zuschauer
|
||||||
|
- Ein unterbrochener Kampf lässt sich später fortsetzen
|
||||||
- Geführte erste Partie als Einstieg
|
- Geführte erste Partie als Einstieg
|
||||||
- Wöchentliche Herausforderungen und Bestenliste als Langzeitziel
|
- Wöchentliche Herausforderungen und Bestenliste als Langzeitziel
|
||||||
|
|
||||||
## Sammeln
|
## Sammeln
|
||||||
- Booster öffnen
|
- Booster öffnen
|
||||||
- Karten haben Seltenheiten
|
- Karten haben Seltenheiten
|
||||||
- Aus der Sammlung werden Decks gebaut (mehrere Slots)
|
- Aus der Sammlung werden Decks gebaut (mehrere Slots) und als Code geteilt
|
||||||
- Überschüssige Karten lassen sich zerlegen und neue herstellen
|
- Überschüssige Karten lassen sich zerlegen und neue herstellen
|
||||||
|
|
||||||
## Elemente
|
## Elemente
|
||||||
|
|||||||
321
data/karten.json
321
data/karten.json
File diff suppressed because it is too large
Load Diff
@@ -30,13 +30,16 @@ static func board_value(p: Game.Player) -> float:
|
|||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
## Stellung aus Sicht von p: eigenes Leben, Feld und Hand zählen positiv,
|
## Stellung aus Sicht von p: eigenes Leben, Feld und Hand zählen positiv, die der Gegner
|
||||||
## die der Gegner negativ. Ein besiegter Gegner ist viel wert.
|
## negativ, der stärkste Gegner etwas mehr (jeder gegen jeden: den Führenden bremsen).
|
||||||
|
## Gegnerische Angriffskraft ohne eigenen Wächter ist eine Bedrohung, tödliche besonders.
|
||||||
static func evaluate(g: Game, p: Game.Player) -> float:
|
static func evaluate(g: Game, p: Game.Player) -> float:
|
||||||
if not p.alive():
|
if not p.alive():
|
||||||
return -10000.0
|
return -10000.0
|
||||||
var score := p.life * 1.0 + board_value(p) * 1.0 + p.hand.size() * 0.8 + p.traps.size() * 2.5
|
var score := p.life * 1.0 + board_value(p) * 1.0 + p.hand.size() * 0.8 + p.traps.size() * 2.5
|
||||||
var alive_enemies := 0
|
var alive_enemies := 0
|
||||||
|
var threat := 0.0
|
||||||
|
var leader := 0.0
|
||||||
for e in g.players:
|
for e in g.players:
|
||||||
if e == p:
|
if e == p:
|
||||||
continue
|
continue
|
||||||
@@ -44,11 +47,28 @@ static func evaluate(g: Game, p: Game.Player) -> float:
|
|||||||
score += 40.0
|
score += 40.0
|
||||||
continue
|
continue
|
||||||
alive_enemies += 1
|
alive_enemies += 1
|
||||||
score -= e.life * 0.6 + board_value(e) * 0.9 + e.hand.size() * 0.2
|
var ev: float = e.life * 0.6 + board_value(e) * 0.9 + e.hand.size() * 0.2
|
||||||
|
score -= ev
|
||||||
|
leader = maxf(leader, ev)
|
||||||
if e.life <= 6:
|
if e.life <= 6:
|
||||||
score += 3.0 # fast besiegt: dranbleiben
|
score += 3.0 # fast besiegt: dranbleiben
|
||||||
|
for c in e.board:
|
||||||
|
if not c.frozen:
|
||||||
|
threat += c.power() * c.attacks_per_turn()
|
||||||
if alive_enemies == 0:
|
if alive_enemies == 0:
|
||||||
return 10000.0
|
return 10000.0
|
||||||
|
score -= leader * 0.25
|
||||||
|
var guarded := false
|
||||||
|
for c in p.board:
|
||||||
|
if c.taunt():
|
||||||
|
guarded = true
|
||||||
|
break
|
||||||
|
if guarded:
|
||||||
|
score -= threat * 0.1
|
||||||
|
elif threat >= p.life:
|
||||||
|
score -= 25.0
|
||||||
|
else:
|
||||||
|
score -= threat * 0.3
|
||||||
return score
|
return score
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
class_name Campaign
|
class_name Campaign
|
||||||
## Kampagne: zehn benannte Magier, jede Stufe mit mehr Gegnern am Tisch.
|
## Kampagne: zehn benannte Magier in drei Kapiteln, jede Stufe mit mehr Gegnern am Tisch.
|
||||||
|
|
||||||
const STAGES := [
|
const STAGES := [
|
||||||
{"name": "Fenn", "title": "Kobold-Zähmer", "elements": ["Feuer"], "extra": 0, "reward": 100, "perk": "feuer_angriff"},
|
{"name": "Fenn", "title": "Kobold-Zähmer", "elements": ["Feuer"], "extra": 0, "reward": 100, "perk": "feuer_angriff"},
|
||||||
@@ -30,22 +30,40 @@ const STAGES2 := [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
## Kapitel 3: drei Merkmale, volle Tische, die stärksten Decks.
|
||||||
|
const STAGES3 := [
|
||||||
|
{"name": "Fenn", "title": "Aschefürst", "elements": ["Feuer"], "extra": 2, "reward": 600, "perks": ["feuer_angriff", "start_energie", "zauber_schaden"]},
|
||||||
|
{"name": "Mara", "title": "Sturmflut", "elements": ["Wasser"], "extra": 2, "reward": 600, "perks": ["leben_35", "extra_karten", "schild_wesen"]},
|
||||||
|
{"name": "Borin", "title": "Berg selbst", "elements": ["Erde"], "extra": 3, "reward": 700, "perks": ["waechter_leben", "schild_wesen", "leben_35"]},
|
||||||
|
{"name": "Liska", "title": "Windbraut", "elements": ["Luft"], "extra": 3, "reward": 700, "perks": ["flink_angriff", "alle_flink", "extra_karten"]},
|
||||||
|
{"name": "Vulkar", "title": "Magmakaiser", "elements": ["Feuer", "Erde"], "extra": 4, "reward": 800, "perks": ["zauber_schaden", "feuer_angriff", "waechter_leben"]},
|
||||||
|
{"name": "Nyx", "title": "Orkanmutter", "elements": ["Wasser", "Luft"], "extra": 4, "reward": 800, "perks": ["extra_karten", "flink_angriff", "resonanz_doppelt"]},
|
||||||
|
{"name": "Ignis", "title": "Sonnenpriester", "elements": ["Feuer"], "extra": 5, "reward": 900, "perks": ["resonanz_doppelt", "zauber_schaden", "start_energie"]},
|
||||||
|
{"name": "Thalassa", "title": "Herz der Tiefe", "elements": ["Wasser"], "extra": 5, "reward": 900, "perks": ["schild_wesen", "leben_35", "resonanz_doppelt"]},
|
||||||
|
{"name": "Zephyra", "title": "Gewitterkaiserin", "elements": ["Feuer", "Luft"], "extra": 5, "reward": 1000, "perks": ["alle_flink", "start_energie", "zauber_schaden"]},
|
||||||
|
{"name": "Archimagier", "title": "Ewiger Meister", "elements": ["Erde", "Luft"], "extra": 5, "reward": 1200, "perks": ["start_energie", "resonanz_doppelt", "extra_karten", "leben_35"]},
|
||||||
|
]
|
||||||
|
|
||||||
|
const CHAPTERS := [STAGES, STAGES2, STAGES3]
|
||||||
|
|
||||||
# Mindeststärke der Gegnerdecks je Stufe; nie unter der Stärke des eigenen Decks.
|
# Mindeststärke der Gegnerdecks je Stufe; nie unter der Stärke des eigenen Decks.
|
||||||
const MIN_RATING := [1.2, 1.3, 1.5, 1.6, 1.8, 1.9, 2.1, 2.2, 2.4, 2.6]
|
const MIN_RATING := [1.2, 1.3, 1.5, 1.6, 1.8, 1.9, 2.1, 2.2, 2.4, 2.6]
|
||||||
const MIN_RATING2 := [2.2, 2.3, 2.5, 2.6, 2.8, 2.9, 3.0, 3.1, 3.2, 3.4]
|
const MIN_RATING2 := [2.2, 2.3, 2.5, 2.6, 2.8, 2.9, 3.0, 3.1, 3.2, 3.4]
|
||||||
|
const MIN_RATING3 := [3.2, 3.3, 3.5, 3.6, 3.8, 3.9, 4.0, 4.1, 4.2, 4.4]
|
||||||
|
const MIN_RATINGS := [MIN_RATING, MIN_RATING2, MIN_RATING3]
|
||||||
|
|
||||||
|
|
||||||
static func target_rating(stage_index: int, player_rating: float) -> float:
|
static func target_rating(stage_index: int, player_rating: float) -> float:
|
||||||
var base: float = MIN_RATING[stage_index] if stage_index < STAGES.size() else MIN_RATING2[stage_index - STAGES.size()]
|
var base: float = MIN_RATINGS[chapter(stage_index) - 1][stage_index % STAGES.size()]
|
||||||
return maxf(base, player_rating)
|
return maxf(base, player_rating)
|
||||||
|
|
||||||
|
|
||||||
static func total() -> int:
|
static func total() -> int:
|
||||||
return STAGES.size() + STAGES2.size()
|
return STAGES.size() * CHAPTERS.size()
|
||||||
|
|
||||||
|
|
||||||
static func chapter(stage: int) -> int:
|
static func chapter(stage: int) -> int:
|
||||||
return 1 if stage < STAGES.size() else 2
|
return stage / STAGES.size() + 1
|
||||||
|
|
||||||
|
|
||||||
## Stufe mit einheitlichem Feld "perks".
|
## Stufe mit einheitlichem Feld "perks".
|
||||||
@@ -54,7 +72,7 @@ static func stage(i: int) -> Dictionary:
|
|||||||
var st: Dictionary = STAGES[i].duplicate()
|
var st: Dictionary = STAGES[i].duplicate()
|
||||||
st["perks"] = [st.perk]
|
st["perks"] = [st.perk]
|
||||||
return st
|
return st
|
||||||
return STAGES2[i - STAGES.size()]
|
return CHAPTERS[chapter(i) - 1][i % STAGES.size()]
|
||||||
|
|
||||||
|
|
||||||
## Gegner einer Stufe, Decks auf die Zielstärke (Stufe oder eigenes Deck, was höher ist).
|
## Gegner einer Stufe, Decks auf die Zielstärke (Stufe oder eigenes Deck, was höher ist).
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const FIELD_COMBO_TEXT := {
|
|||||||
"Blitz": "2 Schaden an den schwächsten Gegner",
|
"Blitz": "2 Schaden an den schwächsten Gegner",
|
||||||
"Schlamm": "Heile dich um 3",
|
"Schlamm": "Heile dich um 3",
|
||||||
"Sturm": "Ziehe 1 Karte",
|
"Sturm": "Ziehe 1 Karte",
|
||||||
"Sand": "Das neue Wesen erhält Schild",
|
"Sand": "Das neue Wesen erhält +1/+1",
|
||||||
}
|
}
|
||||||
const RESONANCE_TEXT := "Resonanz: Ein Wesen bekommt Angriff für eigene Wesen mit gleichem Element: +1, +2, +3, +3 bei ein bis vier Partnern."
|
const RESONANCE_TEXT := "Resonanz: Ein Wesen bekommt Angriff für eigene Wesen mit gleichem Element: +1, +2, +3, +3 bei ein bis vier Partnern."
|
||||||
const RARITIES: Array[String] = ["gewöhnlich", "selten", "episch", "legendär", "mythisch"]
|
const RARITIES: Array[String] = ["gewöhnlich", "selten", "episch", "legendär", "mythisch"]
|
||||||
@@ -82,7 +82,7 @@ static func field_combo_effect(name: String) -> Effect:
|
|||||||
"Sturm":
|
"Sturm":
|
||||||
return Effect.new("draw", "none", 1)
|
return Effect.new("draw", "none", 1)
|
||||||
"Sand":
|
"Sand":
|
||||||
return Effect.new("shield", "self_creature")
|
return Effect.new("buff", "self_creature", 1, 1)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
||||||
@@ -119,62 +119,73 @@ class Effect extends RefCounted:
|
|||||||
static func from_dict(d: Dictionary) -> Effect:
|
static func from_dict(d: Dictionary) -> Effect:
|
||||||
return Effect.new(d.kind, d.get("target", "none"), int(d.get("amount", 0)), int(d.get("amount2", 0)), str(d.get("card_id", "")))
|
return Effect.new(d.kind, d.get("target", "none"), int(d.get("amount", 0)), int(d.get("amount2", 0)), str(d.get("card_id", "")))
|
||||||
|
|
||||||
func text() -> String:
|
## Kartentext in der aktuellen Sprache. trigger: bei Fallen der Auslöser (Wortwahl).
|
||||||
|
func text(trigger: String = "") -> String:
|
||||||
var n := str(amount)
|
var n := str(amount)
|
||||||
if target == "trigger_source":
|
if target == "trigger_source":
|
||||||
|
var played := trigger == "creature_played"
|
||||||
match kind:
|
match kind:
|
||||||
"damage":
|
"damage":
|
||||||
return n + " Schaden an den Angreifer"
|
if played:
|
||||||
|
return tr("%s Schaden an dieses Wesen") % n
|
||||||
|
return tr("%s Schaden an den Angreifer") % n
|
||||||
"freeze":
|
"freeze":
|
||||||
return "Der Angreifer wird eingefroren"
|
if played:
|
||||||
|
return tr("Es wird eingefroren")
|
||||||
|
return tr("Der Angreifer wird eingefroren")
|
||||||
"bounce":
|
"bounce":
|
||||||
return "Es geht zurück auf die Hand"
|
return tr("Es geht zurück auf die Hand")
|
||||||
match kind:
|
match kind:
|
||||||
"cancel":
|
"cancel":
|
||||||
return "Der Angriff verpufft"
|
return tr("Der Angriff verpufft")
|
||||||
"summon":
|
"summon":
|
||||||
return "Beschwöre einen " + Cards.by_id(card_id).name
|
return tr("Beschwöre einen %s") % Cards.by_id(card_id).title()
|
||||||
"damage":
|
"damage":
|
||||||
match target:
|
match target:
|
||||||
"enemy_creature":
|
"enemy_creature":
|
||||||
return n + " Schaden an ein gegnerisches Wesen"
|
return tr("%s Schaden an ein gegnerisches Wesen") % n
|
||||||
"enemy_any":
|
"enemy_any":
|
||||||
return n + " Schaden an ein Ziel"
|
return tr("%s Schaden an ein Ziel") % n
|
||||||
"all_enemy_creatures":
|
"all_enemy_creatures":
|
||||||
return n + " Schaden an alle gegnerischen Wesen"
|
return tr("%s Schaden an alle gegnerischen Wesen") % n
|
||||||
"all_enemy_heroes":
|
"all_enemy_heroes":
|
||||||
return n + " Schaden an alle Gegner"
|
return tr("%s Schaden an alle Gegner") % n
|
||||||
"all_enemies":
|
"all_enemies":
|
||||||
return n + " Schaden an alle Gegner und ihre Wesen"
|
return tr("%s Schaden an alle Gegner und ihre Wesen") % n
|
||||||
"heal":
|
"heal":
|
||||||
return "Heile dich um " + n
|
return tr("Heile dich um %s") % n
|
||||||
"draw":
|
"draw":
|
||||||
return "Ziehe 1 Karte" if amount == 1 else "Ziehe " + n + " Karten"
|
if amount == 1:
|
||||||
|
return tr("Ziehe 1 Karte")
|
||||||
|
return tr("Ziehe %s Karten") % n
|
||||||
"freeze":
|
"freeze":
|
||||||
if target == "enemy_creature":
|
if target == "enemy_creature":
|
||||||
return "Friere ein gegnerisches Wesen ein"
|
return tr("Friere ein gegnerisches Wesen ein")
|
||||||
return "Friere alle gegnerischen Wesen ein"
|
if target == "strongest_enemy_creature":
|
||||||
|
return tr("Friere das stärkste gegnerische Wesen ein")
|
||||||
|
return tr("Friere alle gegnerischen Wesen ein")
|
||||||
"bounce":
|
"bounce":
|
||||||
if target == "enemy_creature":
|
if target == "enemy_creature":
|
||||||
return "Ein gegnerisches Wesen zurück auf die Hand"
|
return tr("Ein gegnerisches Wesen zurück auf die Hand")
|
||||||
return "Alle gegnerischen Wesen zurück auf die Hand"
|
return tr("Alle gegnerischen Wesen zurück auf die Hand")
|
||||||
"buff":
|
"buff":
|
||||||
var b := "+%d/+%d" % [amount, amount2]
|
var b := "+%d/+%d" % [amount, amount2]
|
||||||
if target == "friendly_creature":
|
if target == "friendly_creature":
|
||||||
return "Ein befreundetes Wesen erhält " + b
|
return tr("Ein befreundetes Wesen erhält %s") % b
|
||||||
return "Alle befreundeten Wesen erhalten " + b
|
return tr("Alle befreundeten Wesen erhalten %s") % b
|
||||||
"shield":
|
"shield":
|
||||||
if target == "friendly_creature":
|
if target == "friendly_creature":
|
||||||
return "Ein befreundetes Wesen erhält Schild"
|
return tr("Ein befreundetes Wesen erhält Schild")
|
||||||
if target == "self_creature":
|
if target == "self_creature":
|
||||||
return "Erhält Schild"
|
return tr("Erhält Schild")
|
||||||
return "Alle befreundeten Wesen erhalten Schild"
|
return tr("Alle befreundeten Wesen erhalten Schild")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
class Card extends RefCounted:
|
class Card extends RefCounted:
|
||||||
var id: String
|
var id: String
|
||||||
var name: String
|
var name: String # deutscher Name (auch im Verlauf und in Tests)
|
||||||
|
var name_en := "" # englischer Name
|
||||||
var elements: Array
|
var elements: Array
|
||||||
var cost: int
|
var cost: int
|
||||||
var rarity: String
|
var rarity: String
|
||||||
@@ -192,7 +203,7 @@ class Card extends RefCounted:
|
|||||||
return kind == "Falle"
|
return kind == "Falle"
|
||||||
|
|
||||||
func to_dict() -> Dictionary:
|
func to_dict() -> Dictionary:
|
||||||
var d := {"id": id, "name": name, "elements": elements, "cost": cost, "rarity": rarity, "kind": kind}
|
var d := {"id": id, "name": name, "name_en": name_en, "elements": elements, "cost": cost, "rarity": rarity, "kind": kind}
|
||||||
if is_creature():
|
if is_creature():
|
||||||
d["attack"] = attack
|
d["attack"] = attack
|
||||||
d["health"] = health
|
d["health"] = health
|
||||||
@@ -208,6 +219,7 @@ class Card extends RefCounted:
|
|||||||
var c := Card.new()
|
var c := Card.new()
|
||||||
c.id = d.id
|
c.id = d.id
|
||||||
c.name = d.name
|
c.name = d.name
|
||||||
|
c.name_en = str(d.get("name_en", ""))
|
||||||
c.elements = Array(d.elements)
|
c.elements = Array(d.elements)
|
||||||
c.cost = int(d.cost)
|
c.cost = int(d.cost)
|
||||||
c.rarity = d.rarity
|
c.rarity = d.rarity
|
||||||
@@ -220,6 +232,12 @@ class Card extends RefCounted:
|
|||||||
c.trigger = str(d.get("trigger", ""))
|
c.trigger = str(d.get("trigger", ""))
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
## Anzeigename in der aktuellen Sprache.
|
||||||
|
func title() -> String:
|
||||||
|
if name_en != "" and TranslationServer.get_locale().begins_with("en"):
|
||||||
|
return name_en
|
||||||
|
return name
|
||||||
|
|
||||||
func is_combo() -> bool:
|
func is_combo() -> bool:
|
||||||
return elements.size() >= 2
|
return elements.size() >= 2
|
||||||
|
|
||||||
@@ -238,19 +256,19 @@ class Card extends RefCounted:
|
|||||||
var kws: Array[String] = []
|
var kws: Array[String] = []
|
||||||
for k in Cards.KEYWORDS:
|
for k in Cards.KEYWORDS:
|
||||||
if k in keywords:
|
if k in keywords:
|
||||||
kws.append(k)
|
kws.append(tr(k))
|
||||||
parts.append(", ".join(kws) + ".")
|
parts.append(", ".join(kws) + ".")
|
||||||
if not effects.is_empty():
|
if not effects.is_empty():
|
||||||
var fx: Array[String] = []
|
var fx: Array[String] = []
|
||||||
for f in effects:
|
for f in effects:
|
||||||
fx.append(f.text())
|
fx.append(f.text(trigger))
|
||||||
var s := ", ".join(fx) + "."
|
var s := ", ".join(fx) + "."
|
||||||
if is_trap():
|
if is_trap():
|
||||||
if trigger == "creature_played":
|
parts.append(tr("Falle. %s: %s") % [tr(Cards.TRIGGER_TEXT[trigger]), s])
|
||||||
s = s.replace("an den Angreifer", "an dieses Wesen").replace("Der Angreifer wird", "Es wird")
|
elif is_creature():
|
||||||
parts.append("Falle. %s: %s" % [Cards.TRIGGER_TEXT[trigger], s])
|
parts.append(tr("Beim Ausspielen: %s") % s)
|
||||||
else:
|
else:
|
||||||
parts.append(("Beim Ausspielen: " + s) if is_creature() else s)
|
parts.append(s)
|
||||||
return " ".join(parts)
|
return " ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ func mulligan(p: Player, cards: Array) -> String:
|
|||||||
if n > 0:
|
if n > 0:
|
||||||
shuffle(p.deck, rng)
|
shuffle(p.deck, rng)
|
||||||
draw(p, n)
|
draw(p, n)
|
||||||
say("%s tauscht %d Karten." % [p.name, n] if n > 0 else "%s behält die Hand." % p.name)
|
say(("%s tauscht 1 Karte." % p.name if n == 1 else "%s tauscht %d Karten." % [p.name, n]) if n > 0 else "%s behält die Hand." % p.name)
|
||||||
emit({"type": "mulligan", "player": players.find(p), "count": n})
|
emit({"type": "mulligan", "player": players.find(p), "count": n})
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|||||||
171
scripts/net.gd
171
scripts/net.gd
@@ -8,6 +8,8 @@ signal started(seat: int)
|
|||||||
signal snapshot_received(snap: Dictionary)
|
signal snapshot_received(snap: Dictionary)
|
||||||
signal action_applied
|
signal action_applied
|
||||||
signal ended(reason: String)
|
signal ended(reason: String)
|
||||||
|
signal migrated # nach einem Host-Ausfall: dieser Rechner führt das Spiel jetzt
|
||||||
|
signal status_changed
|
||||||
|
|
||||||
const PORT := 24455
|
const PORT := 24455
|
||||||
|
|
||||||
@@ -23,15 +25,25 @@ var my_seat := -1
|
|||||||
var game: Game # Host
|
var game: Game # Host
|
||||||
var status := ""
|
var status := ""
|
||||||
var waiting := {} # Name -> {"seat": int, "since": float}: Platz nach Verbindungsabbruch freihalten
|
var waiting := {} # Name -> {"seat": int, "since": float}: Platz nach Verbindungsabbruch freihalten
|
||||||
const REJOIN_SECONDS := 60.0
|
var rejoin_seconds := 60.0
|
||||||
|
var spectator := false # eigene Rolle: nur zuschauen
|
||||||
|
var spectators := {} # Host: peer_id -> Name
|
||||||
|
var successor_ip := "" # Mitspieler: Adresse des Nachfolge-Hosts
|
||||||
|
var i_am_successor := false
|
||||||
|
var backup := {} # Nachfolger: voller Spielstand nach jeder Aktion
|
||||||
|
var backup_names := {} # Nachfolger: Platz -> Name aller Menschen
|
||||||
|
var last_winner := "" # Mitspieler: Sieger laut letztem Zustandsbild
|
||||||
|
var reconnecting := false
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
multiplayer.peer_connected.connect(_on_peer_connected)
|
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||||
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||||
multiplayer.connected_to_server.connect(_on_connected)
|
multiplayer.connected_to_server.connect(_on_connected)
|
||||||
multiplayer.connection_failed.connect(func(): _fail("Verbindung fehlgeschlagen"))
|
multiplayer.connection_failed.connect(func():
|
||||||
multiplayer.server_disconnected.connect(func(): _fail("Host hat das Spiel beendet"))
|
if not reconnecting:
|
||||||
|
_fail("Verbindung fehlgeschlagen"))
|
||||||
|
multiplayer.server_disconnected.connect(_on_server_lost)
|
||||||
|
|
||||||
|
|
||||||
func host(port: int = PORT) -> Error:
|
func host(port: int = PORT) -> Error:
|
||||||
@@ -45,6 +57,7 @@ func host(port: int = PORT) -> Error:
|
|||||||
connected = true
|
connected = true
|
||||||
names = {1: my_name}
|
names = {1: my_name}
|
||||||
decks = {1: my_deck}
|
decks = {1: my_deck}
|
||||||
|
spectators = {}
|
||||||
status = "Warte auf Mitspieler …"
|
status = "Warte auf Mitspieler …"
|
||||||
lobby_changed.emit()
|
lobby_changed.emit()
|
||||||
return OK
|
return OK
|
||||||
@@ -76,6 +89,13 @@ func leave() -> void:
|
|||||||
my_seat = -1
|
my_seat = -1
|
||||||
game = null
|
game = null
|
||||||
status = ""
|
status = ""
|
||||||
|
spectator = false
|
||||||
|
spectators = {}
|
||||||
|
successor_ip = ""
|
||||||
|
i_am_successor = false
|
||||||
|
backup = {}
|
||||||
|
backup_names = {}
|
||||||
|
last_winner = ""
|
||||||
|
|
||||||
|
|
||||||
func local_addresses() -> Array:
|
func local_addresses() -> Array:
|
||||||
@@ -91,7 +111,114 @@ func _fail(reason: String) -> void:
|
|||||||
func _on_connected() -> void:
|
func _on_connected() -> void:
|
||||||
connected = true
|
connected = true
|
||||||
status = "Verbunden"
|
status = "Verbunden"
|
||||||
register.rpc_id(1, my_name, my_deck)
|
register.rpc_id(1, my_name, my_deck, spectator)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Host-Wechsel ---------------------------------------------------------------
|
||||||
|
# Der Host schickt dem Mitspieler mit der kleinsten Kennung nach jeder Aktion den vollen
|
||||||
|
# Spielstand und allen die Adresse dieses Nachfolgers. Fällt der Host aus, eröffnet der
|
||||||
|
# Nachfolger das Spiel neu, die anderen verbinden sich mit ihm und kehren auf ihre Plätze zurück.
|
||||||
|
|
||||||
|
func _successor_id() -> int:
|
||||||
|
var ids: Array = seats.keys().filter(func(id): return id != 1)
|
||||||
|
ids.sort()
|
||||||
|
return ids[0] if not ids.is_empty() else -1
|
||||||
|
|
||||||
|
|
||||||
|
func _update_successor() -> void:
|
||||||
|
if not is_host or game == null:
|
||||||
|
return
|
||||||
|
var succ := _successor_id()
|
||||||
|
var ip := ""
|
||||||
|
if succ > 0 and peer != null and peer.get_peer(succ) != null:
|
||||||
|
ip = peer.get_peer(succ).get_remote_address()
|
||||||
|
for id in seats:
|
||||||
|
if id != 1:
|
||||||
|
set_successor.rpc_id(id, ip, id == succ)
|
||||||
|
|
||||||
|
|
||||||
|
## Platz -> Name aller Menschen (auch der gerade fehlenden), für die Rückkehr nach dem Wechsel.
|
||||||
|
func _seat_names() -> Dictionary:
|
||||||
|
var out := {}
|
||||||
|
for id in seats:
|
||||||
|
out[seats[id]] = names.get(id, "")
|
||||||
|
for pname in waiting:
|
||||||
|
out[waiting[pname].seat] = pname
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("authority", "reliable")
|
||||||
|
func set_successor(ip: String, me: bool) -> void:
|
||||||
|
successor_ip = ip
|
||||||
|
i_am_successor = me
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("authority", "reliable")
|
||||||
|
func receive_backup(snap: Dictionary, seat_names: Dictionary) -> void:
|
||||||
|
backup = snap
|
||||||
|
backup_names = seat_names
|
||||||
|
|
||||||
|
|
||||||
|
func _on_server_lost() -> void:
|
||||||
|
if is_host:
|
||||||
|
return
|
||||||
|
if last_winner != "":
|
||||||
|
_fail("Host hat das Spiel beendet")
|
||||||
|
elif i_am_successor and not backup.is_empty():
|
||||||
|
_become_host()
|
||||||
|
elif successor_ip != "":
|
||||||
|
status = "Host weg, verbinde mit dem Nachfolger …"
|
||||||
|
status_changed.emit()
|
||||||
|
_reconnect(successor_ip)
|
||||||
|
else:
|
||||||
|
_fail("Host hat das Spiel beendet")
|
||||||
|
|
||||||
|
|
||||||
|
func _become_host() -> void:
|
||||||
|
var snap := backup
|
||||||
|
var seat_names := backup_names
|
||||||
|
var seat := my_seat
|
||||||
|
if multiplayer.multiplayer_peer != null:
|
||||||
|
multiplayer.multiplayer_peer.close()
|
||||||
|
var err := FAILED
|
||||||
|
for attempt in 5:
|
||||||
|
err = host()
|
||||||
|
if err == OK:
|
||||||
|
break
|
||||||
|
await get_tree().create_timer(1.0).timeout
|
||||||
|
if err != OK:
|
||||||
|
_fail("Host weg, Übernahme fehlgeschlagen")
|
||||||
|
return
|
||||||
|
game = Game.from_snapshot(snap)
|
||||||
|
seats = {1: seat}
|
||||||
|
my_seat = seat
|
||||||
|
waiting = {}
|
||||||
|
for s in seat_names:
|
||||||
|
if int(s) != seat:
|
||||||
|
waiting[seat_names[s]] = {"seat": int(s), "since": Time.get_unix_time_from_system()}
|
||||||
|
game.say("%s übernimmt als Host." % my_name)
|
||||||
|
status = "Du bist jetzt Host, die anderen verbinden neu …"
|
||||||
|
successor_ip = ""
|
||||||
|
i_am_successor = false
|
||||||
|
backup = {}
|
||||||
|
migrated.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _reconnect(ip: String) -> void:
|
||||||
|
reconnecting = true
|
||||||
|
await get_tree().create_timer(2.0).timeout
|
||||||
|
for attempt in 5:
|
||||||
|
if join(ip) == OK:
|
||||||
|
var t := 0.0
|
||||||
|
while not connected and t < 4.0 and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTING:
|
||||||
|
await get_tree().process_frame
|
||||||
|
t += get_process_delta_time()
|
||||||
|
if connected:
|
||||||
|
reconnecting = false
|
||||||
|
return
|
||||||
|
await get_tree().create_timer(1.0).timeout
|
||||||
|
reconnecting = false
|
||||||
|
_fail("Nachfolge-Host nicht erreichbar")
|
||||||
|
|
||||||
|
|
||||||
func _on_peer_connected(_id: int) -> void:
|
func _on_peer_connected(_id: int) -> void:
|
||||||
@@ -104,14 +231,16 @@ func _on_peer_disconnected(id: int) -> void:
|
|||||||
var pname: String = names.get(id, "")
|
var pname: String = names.get(id, "")
|
||||||
names.erase(id)
|
names.erase(id)
|
||||||
decks.erase(id)
|
decks.erase(id)
|
||||||
|
spectators.erase(id)
|
||||||
if game != null and seats.has(id):
|
if game != null and seats.has(id):
|
||||||
var p: Game.Player = game.players[seats[id]]
|
var p: Game.Player = game.players[seats[id]]
|
||||||
if game.winner == "":
|
if game.winner == "":
|
||||||
waiting[pname] = {"seat": seats[id], "since": Time.get_unix_time_from_system()}
|
waiting[pname] = {"seat": seats[id], "since": Time.get_unix_time_from_system()}
|
||||||
game.say("%s hat die Verbindung verloren, der Platz bleibt %d s frei." % [p.name, int(REJOIN_SECONDS)])
|
game.say("%s hat die Verbindung verloren, der Platz bleibt %d s frei." % [p.name, int(rejoin_seconds)])
|
||||||
seats.erase(id)
|
seats.erase(id)
|
||||||
|
_update_successor()
|
||||||
action_applied.emit()
|
action_applied.emit()
|
||||||
lobby_update.rpc(names)
|
lobby_update.rpc(names, spectators)
|
||||||
lobby_changed.emit()
|
lobby_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
@@ -123,7 +252,7 @@ func tick() -> void:
|
|||||||
for pname in waiting.keys():
|
for pname in waiting.keys():
|
||||||
var w: Dictionary = waiting[pname]
|
var w: Dictionary = waiting[pname]
|
||||||
var p: Game.Player = game.players[w.seat]
|
var p: Game.Player = game.players[w.seat]
|
||||||
if now - float(w.since) > REJOIN_SECONDS:
|
if now - float(w.since) > rejoin_seconds:
|
||||||
waiting.erase(pname)
|
waiting.erase(pname)
|
||||||
game.say("%s kehrt nicht zurück und scheidet aus." % p.name)
|
game.say("%s kehrt nicht zurück und scheidet aus." % p.name)
|
||||||
game.damage(p, 999)
|
game.damage(p, 999)
|
||||||
@@ -137,10 +266,18 @@ func tick() -> void:
|
|||||||
|
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func register(name: String, deck_ids: Array) -> void:
|
func register(name: String, deck_ids: Array, as_spectator: bool = false) -> void:
|
||||||
if not is_host:
|
if not is_host:
|
||||||
return
|
return
|
||||||
var id := multiplayer.get_remote_sender_id()
|
var id := multiplayer.get_remote_sender_id()
|
||||||
|
if as_spectator:
|
||||||
|
spectators[id] = name
|
||||||
|
lobby_update.rpc(names, spectators)
|
||||||
|
lobby_changed.emit()
|
||||||
|
if game != null:
|
||||||
|
start_game.rpc_id(id, -1)
|
||||||
|
receive_snapshot.rpc_id(id, game.snapshot(-2))
|
||||||
|
return
|
||||||
if game != null:
|
if game != null:
|
||||||
# Laufendes Spiel: nur Rückkehr auf einen freigehaltenen Platz
|
# Laufendes Spiel: nur Rückkehr auf einen freigehaltenen Platz
|
||||||
if waiting.has(name):
|
if waiting.has(name):
|
||||||
@@ -150,19 +287,21 @@ func register(name: String, deck_ids: Array) -> void:
|
|||||||
game.say("%s ist zurück." % name)
|
game.say("%s ist zurück." % name)
|
||||||
start_game.rpc_id(id, seats[id])
|
start_game.rpc_id(id, seats[id])
|
||||||
receive_snapshot.rpc_id(id, game.snapshot(seats[id]))
|
receive_snapshot.rpc_id(id, game.snapshot(seats[id]))
|
||||||
|
_update_successor()
|
||||||
action_applied.emit()
|
action_applied.emit()
|
||||||
else:
|
else:
|
||||||
status = "%s wollte beitreten, das Spiel läuft schon" % name
|
status = "%s wollte beitreten, das Spiel läuft schon" % name
|
||||||
return
|
return
|
||||||
names[id] = name
|
names[id] = name
|
||||||
decks[id] = deck_ids
|
decks[id] = deck_ids
|
||||||
lobby_update.rpc(names)
|
lobby_update.rpc(names, spectators)
|
||||||
lobby_changed.emit()
|
lobby_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "reliable")
|
@rpc("authority", "reliable")
|
||||||
func lobby_update(n: Dictionary) -> void:
|
func lobby_update(n: Dictionary, sp: Dictionary = {}) -> void:
|
||||||
names = n
|
names = n
|
||||||
|
spectators = sp
|
||||||
lobby_changed.emit()
|
lobby_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
@@ -194,6 +333,9 @@ func start_battle(n_ai: int) -> void:
|
|||||||
for id in ids:
|
for id in ids:
|
||||||
if id != 1:
|
if id != 1:
|
||||||
start_game.rpc_id(id, seats[id])
|
start_game.rpc_id(id, seats[id])
|
||||||
|
for id in spectators:
|
||||||
|
start_game.rpc_id(id, -1)
|
||||||
|
_update_successor()
|
||||||
started.emit(my_seat)
|
started.emit(my_seat)
|
||||||
|
|
||||||
|
|
||||||
@@ -203,17 +345,24 @@ func start_game(seat: int) -> void:
|
|||||||
started.emit(seat)
|
started.emit(seat)
|
||||||
|
|
||||||
|
|
||||||
## Host: Zustand an alle Mitspieler senden (jeder aus seiner Sicht).
|
## Host: Zustand an alle senden (Mitspieler aus ihrer Sicht, Zuschauer ohne Verborgenes,
|
||||||
|
## der Nachfolger zusätzlich den vollen Spielstand).
|
||||||
func broadcast() -> void:
|
func broadcast() -> void:
|
||||||
if not is_host or game == null:
|
if not is_host or game == null:
|
||||||
return
|
return
|
||||||
for id in seats:
|
for id in seats:
|
||||||
if id != 1:
|
if id != 1:
|
||||||
receive_snapshot.rpc_id(id, game.snapshot(seats[id]))
|
receive_snapshot.rpc_id(id, game.snapshot(seats[id]))
|
||||||
|
for id in spectators:
|
||||||
|
receive_snapshot.rpc_id(id, game.snapshot(-2))
|
||||||
|
var succ := _successor_id()
|
||||||
|
if succ > 0:
|
||||||
|
receive_backup.rpc_id(succ, game.snapshot(-1), _seat_names())
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "reliable")
|
@rpc("authority", "reliable")
|
||||||
func receive_snapshot(snap: Dictionary) -> void:
|
func receive_snapshot(snap: Dictionary) -> void:
|
||||||
|
last_winner = str(snap.get("winner", ""))
|
||||||
snapshot_received.emit(snap)
|
snapshot_received.emit(snap)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ class_name NetState
|
|||||||
|
|
||||||
|
|
||||||
## Serialisierter Zustand aus Sicht eines Spielers: fremde Hände, Decks und Fallen nur als Anzahl.
|
## Serialisierter Zustand aus Sicht eines Spielers: fremde Hände, Decks und Fallen nur als Anzahl.
|
||||||
|
## viewer -1: alles sichtbar (Spielstand, Host-Wechsel); -2: nichts Verborgenes (Zuschauer).
|
||||||
static func snapshot(g: Game, viewer: int) -> Dictionary:
|
static func snapshot(g: Game, viewer: int) -> Dictionary:
|
||||||
var ps := []
|
var ps := []
|
||||||
for i in g.players.size():
|
for i in g.players.size():
|
||||||
var p: Game.Player = g.players[i]
|
var p: Game.Player = g.players[i]
|
||||||
var mine := i == viewer
|
var mine := i == viewer or viewer == -1
|
||||||
var board := []
|
var board := []
|
||||||
for c in p.board:
|
for c in p.board:
|
||||||
board.append({"uid": c.uid, "card": c.card.id, "attack": c.attack, "resonance": c.resonance,
|
board.append({"uid": c.uid, "card": c.card.id, "attack": c.attack, "resonance": c.resonance,
|
||||||
@@ -17,7 +18,8 @@ static func snapshot(g: Game, viewer: int) -> Dictionary:
|
|||||||
"energy": p.energy, "max_energy": p.max_energy, "fatigue": p.fatigue, "start_bonus": p.start_bonus,
|
"energy": p.energy, "max_energy": p.max_energy, "fatigue": p.fatigue, "start_bonus": p.start_bonus,
|
||||||
"order": p.order, "perks": p.perks, "turns": p.turns, "mulligan_done": p.mulligan_done,
|
"order": p.order, "perks": p.perks, "turns": p.turns, "mulligan_done": p.mulligan_done,
|
||||||
"hand": p.hand.map(func(c): return c.id) if mine else p.hand.size(),
|
"hand": p.hand.map(func(c): return c.id) if mine else p.hand.size(),
|
||||||
"deck": p.deck.size(), "traps": p.traps.map(func(c): return c.id) if mine else p.traps.size(),
|
"deck": p.deck.map(func(c): return c.id) if viewer == -1 else p.deck.size(),
|
||||||
|
"traps": p.traps.map(func(c): return c.id) if mine else p.traps.size(),
|
||||||
"board": board})
|
"board": board})
|
||||||
var evs := []
|
var evs := []
|
||||||
for ev in g.events:
|
for ev in g.events:
|
||||||
@@ -51,8 +53,12 @@ static func from_snapshot(d: Dictionary) -> Game:
|
|||||||
else:
|
else:
|
||||||
for i in int(pd.hand):
|
for i in int(pd.hand):
|
||||||
p.hand.append(placeholder)
|
p.hand.append(placeholder)
|
||||||
for i in int(pd.deck):
|
if pd.deck is Array:
|
||||||
p.deck.append(placeholder)
|
for cid in pd.deck:
|
||||||
|
p.deck.append(Cards.by_id(cid))
|
||||||
|
else:
|
||||||
|
for i in int(pd.deck):
|
||||||
|
p.deck.append(placeholder)
|
||||||
if pd.traps is Array:
|
if pd.traps is Array:
|
||||||
for cid in pd.traps:
|
for cid in pd.traps:
|
||||||
p.traps.append(Cards.by_id(cid))
|
p.traps.append(Cards.by_id(cid))
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ const SAVE_VERSION := 5
|
|||||||
const PITY_BOOSTERS := 20 # danach ist eine mythische Karte garantiert
|
const PITY_BOOSTERS := 20 # danach ist eine mythische Karte garantiert
|
||||||
const SAVE_PATH := "user://spielstand.json"
|
const SAVE_PATH := "user://spielstand.json"
|
||||||
const BOOSTER_COST := 100
|
const BOOSTER_COST := 100
|
||||||
|
const ELEMENT_BOOSTER_COST := 120 # nur Karten eines Elements (auch seine Kombos)
|
||||||
|
const CODE_PREFIX := "DINGER1:" # Deck-Code zum Teilen
|
||||||
const BOOSTER_SIZE := 5
|
const BOOSTER_SIZE := 5
|
||||||
const RARITY_WEIGHTS := {"gewöhnlich": 70.0, "selten": 20.0, "episch": 7.5, "legendär": 2.0, "mythisch": 0.5}
|
const RARITY_WEIGHTS := {"gewöhnlich": 70.0, "selten": 20.0, "episch": 7.5, "legendär": 2.0, "mythisch": 0.5}
|
||||||
const START_COINS := 150
|
const START_COINS := 150
|
||||||
@@ -34,6 +36,7 @@ const ACHIEVEMENTS := [
|
|||||||
{"id": "kombokoenig", "name": "Kombokönig", "text": "Gewinne mit mindestens 6 Kombokarten im Deck.", "reward": 100},
|
{"id": "kombokoenig", "name": "Kombokönig", "text": "Gewinne mit mindestens 6 Kombokarten im Deck.", "reward": 100},
|
||||||
{"id": "kampagnenheld", "name": "Kampagnenheld", "text": "Schließe Kapitel 1 der Kampagne ab.", "reward": 300},
|
{"id": "kampagnenheld", "name": "Kampagnenheld", "text": "Schließe Kapitel 1 der Kampagne ab.", "reward": 300},
|
||||||
{"id": "elementmeister", "name": "Meister der Elemente", "text": "Schließe Kapitel 2 der Kampagne ab.", "reward": 500},
|
{"id": "elementmeister", "name": "Meister der Elemente", "text": "Schließe Kapitel 2 der Kampagne ab.", "reward": 500},
|
||||||
|
{"id": "archimagier", "name": "Archimagier", "text": "Schließe Kapitel 3 der Kampagne ab.", "reward": 800},
|
||||||
{"id": "serie", "name": "Siegesserie", "text": "Gewinne fünf Kämpfe in Folge.", "reward": 150},
|
{"id": "serie", "name": "Siegesserie", "text": "Gewinne fünf Kämpfe in Folge.", "reward": 150},
|
||||||
{"id": "wochenheld", "name": "Wochenheld", "text": "Bestehe eine wöchentliche Herausforderung.", "reward": 100},
|
{"id": "wochenheld", "name": "Wochenheld", "text": "Bestehe eine wöchentliche Herausforderung.", "reward": 100},
|
||||||
]
|
]
|
||||||
@@ -336,7 +339,15 @@ func dismantle_extras() -> int:
|
|||||||
|
|
||||||
# --- Booster ----------------------------------------------------------------------
|
# --- Booster ----------------------------------------------------------------------
|
||||||
|
|
||||||
static func roll_booster(r: RandomNumberGenerator) -> Array:
|
## Kartenpool einer Seltenheit, bei Elementboostern nur Karten mit diesem Element.
|
||||||
|
static func booster_pool(rarity: String, element: String) -> Array:
|
||||||
|
var pool := Cards.of_rarity(rarity)
|
||||||
|
if element != "":
|
||||||
|
pool = pool.filter(func(c): return element in c.elements)
|
||||||
|
return pool
|
||||||
|
|
||||||
|
|
||||||
|
static func roll_booster(r: RandomNumberGenerator, element: String = "") -> Array:
|
||||||
var rarities := RARITY_WEIGHTS.keys()
|
var rarities := RARITY_WEIGHTS.keys()
|
||||||
var weights := PackedFloat32Array(RARITY_WEIGHTS.values())
|
var weights := PackedFloat32Array(RARITY_WEIGHTS.values())
|
||||||
var cards := []
|
var cards := []
|
||||||
@@ -345,23 +356,49 @@ static func roll_booster(r: RandomNumberGenerator) -> Array:
|
|||||||
if i == BOOSTER_SIZE - 1 and rarity == "gewöhnlich" \
|
if i == BOOSTER_SIZE - 1 and rarity == "gewöhnlich" \
|
||||||
and cards.all(func(c): return c.rarity == "gewöhnlich"):
|
and cards.all(func(c): return c.rarity == "gewöhnlich"):
|
||||||
rarity = "selten" # mindestens eine seltene Karte pro Booster
|
rarity = "selten" # mindestens eine seltene Karte pro Booster
|
||||||
var pool := Cards.of_rarity(rarity)
|
var pool := booster_pool(rarity, element)
|
||||||
cards.append(pool[r.randi_range(0, pool.size() - 1)])
|
cards.append(pool[r.randi_range(0, pool.size() - 1)])
|
||||||
return cards
|
return cards
|
||||||
|
|
||||||
|
|
||||||
## Öffnet einen Booster. Gibt die Karten zurück, leer wenn die Münzen nicht reichen.
|
## Deck-Code zum Teilen: "DINGER1:id*anzahl,…" (sortiert, daher vergleichbar).
|
||||||
func open_booster(r: RandomNumberGenerator = null) -> Array:
|
static func deck_code(card_ids: Array) -> String:
|
||||||
if coins < BOOSTER_COST:
|
var counts := {}
|
||||||
|
for cid in card_ids:
|
||||||
|
counts[cid] = counts.get(cid, 0) + 1
|
||||||
|
var keys := counts.keys()
|
||||||
|
keys.sort()
|
||||||
|
return CODE_PREFIX + ",".join(keys.map(func(k): return "%s*%d" % [k, counts[k]]))
|
||||||
|
|
||||||
|
|
||||||
|
## Karten-IDs aus einem Deck-Code; leer bei ungültigem Code.
|
||||||
|
static func parse_deck_code(code: String) -> Array:
|
||||||
|
var s := code.strip_edges()
|
||||||
|
if not s.begins_with(CODE_PREFIX):
|
||||||
|
return []
|
||||||
|
var out := []
|
||||||
|
for part in s.substr(CODE_PREFIX.length()).split(",", false):
|
||||||
|
var kv: PackedStringArray = part.strip_edges().split("*")
|
||||||
|
if kv.size() != 2 or not Cards.has_id(kv[0]) or not kv[1].is_valid_int():
|
||||||
|
return []
|
||||||
|
for i in clampi(int(kv[1]), 0, Game.MAX_COPIES):
|
||||||
|
out.append(kv[0])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Öffnet einen Booster (element: Elementbooster). Gibt die Karten zurück, leer wenn die Münzen nicht reichen.
|
||||||
|
func open_booster(r: RandomNumberGenerator = null, element: String = "") -> Array:
|
||||||
|
var cost := ELEMENT_BOOSTER_COST if element != "" else BOOSTER_COST
|
||||||
|
if coins < cost:
|
||||||
return []
|
return []
|
||||||
if r == null:
|
if r == null:
|
||||||
r = Game.make_rng()
|
r = Game.make_rng()
|
||||||
coins -= BOOSTER_COST
|
coins -= cost
|
||||||
var cards := roll_booster(r)
|
var cards := roll_booster(r, element)
|
||||||
booster_since_mythic += 1
|
booster_since_mythic += 1
|
||||||
var has_mythic: bool = cards.any(func(c): return c.rarity == "mythisch")
|
var has_mythic: bool = cards.any(func(c): return c.rarity == "mythisch")
|
||||||
if booster_since_mythic >= PITY_BOOSTERS and not has_mythic:
|
if booster_since_mythic >= PITY_BOOSTERS and not has_mythic:
|
||||||
var pool := Cards.of_rarity("mythisch")
|
var pool := booster_pool("mythisch", element)
|
||||||
cards[BOOSTER_SIZE - 1] = pool[r.randi_range(0, pool.size() - 1)]
|
cards[BOOSTER_SIZE - 1] = pool[r.randi_range(0, pool.size() - 1)]
|
||||||
has_mythic = true
|
has_mythic = true
|
||||||
if has_mythic:
|
if has_mythic:
|
||||||
@@ -478,6 +515,8 @@ func _achieved(id: String, ctx: Dictionary) -> bool:
|
|||||||
"kampagnenheld":
|
"kampagnenheld":
|
||||||
return campaign_stage >= Campaign.STAGES.size()
|
return campaign_stage >= Campaign.STAGES.size()
|
||||||
"elementmeister":
|
"elementmeister":
|
||||||
|
return campaign_stage >= 2 * Campaign.STAGES.size()
|
||||||
|
"archimagier":
|
||||||
return campaign_stage >= Campaign.total()
|
return campaign_stage >= Campaign.total()
|
||||||
"serie":
|
"serie":
|
||||||
return records.best_streak >= 5
|
return records.best_streak >= 5
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func play_events(evs: Array, centers: Dictionary) -> void:
|
|||||||
screen.preview_timer = 1.8
|
screen.preview_timer = 1.8
|
||||||
"mulligan":
|
"mulligan":
|
||||||
if ev.count > 0:
|
if ev.count > 0:
|
||||||
popup("tauscht %d Karten" % ev.count, Style.MUTED, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
|
popup(("tauscht 1 Karte" if int(ev.count) == 1 else "tauscht %d Karten" % ev.count), Style.MUTED, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
|
||||||
|
|
||||||
|
|
||||||
func center_of(ref: Dictionary, centers: Dictionary) -> Vector2:
|
func center_of(ref: Dictionary, centers: Dictionary) -> Vector2:
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ var stage := -1 # Kampagnenstufe oder -1
|
|||||||
var tutorial := false
|
var tutorial := false
|
||||||
var hotseat_decks: Array = []
|
var hotseat_decks: Array = []
|
||||||
var hotseat_ai := 0
|
var hotseat_ai := 0
|
||||||
var online_seat := -1 # Netzwerk: eigener Platz, sonst -1
|
var net_play := false # Netzwerkspiel (auch als Zuschauer)
|
||||||
|
var online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1
|
||||||
var net: Net
|
var net: Net
|
||||||
var game: Game
|
var game: Game
|
||||||
var perspective: Game.Player # der Mensch, dessen Hand unten liegt
|
var perspective: Game.Player # der Mensch, dessen Hand unten liegt
|
||||||
@@ -44,6 +45,7 @@ var preset_opponents: Array = [] # aus der Gegnervorschau übernommene Gegner
|
|||||||
var skip_ai := false # Gegnerzüge im Schnelldurchlauf
|
var skip_ai := false # Gegnerzüge im Schnelldurchlauf
|
||||||
var smart_ai := true # Vorausschau-KI (Leicht: einfache KI)
|
var smart_ai := true # Vorausschau-KI (Leicht: einfache KI)
|
||||||
var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion
|
var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion
|
||||||
|
var saved_game: Game = null # fortgesetzter Kampf (siehe resume)
|
||||||
var undo_log_len := 0
|
var undo_log_len := 0
|
||||||
var undo_t := 0.0
|
var undo_t := 0.0
|
||||||
|
|
||||||
@@ -80,6 +82,7 @@ var result_label: Label
|
|||||||
var reward_label: Label
|
var reward_label: Label
|
||||||
var notice_label: Label
|
var notice_label: Label
|
||||||
var confirm: Control
|
var confirm: Control
|
||||||
|
var b_save_leave: Button
|
||||||
var handover: Control
|
var handover: Control
|
||||||
var handover_label: Label
|
var handover_label: Label
|
||||||
var mulligan: BattleMulligan
|
var mulligan: BattleMulligan
|
||||||
@@ -135,6 +138,7 @@ static func hotseat(decks: Array, n_ai: int) -> BattleScreen:
|
|||||||
|
|
||||||
static func online(seat: int) -> BattleScreen:
|
static func online(seat: int) -> BattleScreen:
|
||||||
var b := BattleScreen.new(0, -1)
|
var b := BattleScreen.new(0, -1)
|
||||||
|
b.net_play = true
|
||||||
b.online_seat = seat
|
b.online_seat = seat
|
||||||
return b
|
return b
|
||||||
|
|
||||||
@@ -152,6 +156,15 @@ static func against(opps: Array) -> BattleScreen:
|
|||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
## Unterbrochenen Kampf aus der Datei fortsetzen (siehe save_battle).
|
||||||
|
static func resume(data: Dictionary) -> BattleScreen:
|
||||||
|
var s := BattleScreen.new(int(data.get("n_opp", 1)), int(data.get("stage", -1)))
|
||||||
|
s.challenge = data.get("challenge", {})
|
||||||
|
s.played_ids = Array(data.get("played_ids", []))
|
||||||
|
s.saved_game = Game.from_snapshot(data.game)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
static func weekly(ch: Dictionary) -> BattleScreen:
|
static func weekly(ch: Dictionary) -> BattleScreen:
|
||||||
var b := BattleScreen.new(int(ch.n_opp), -1)
|
var b := BattleScreen.new(int(ch.n_opp), -1)
|
||||||
b.challenge = ch
|
b.challenge = ch
|
||||||
@@ -159,11 +172,16 @@ static func weekly(ch: Dictionary) -> BattleScreen:
|
|||||||
|
|
||||||
|
|
||||||
func is_hotseat() -> bool:
|
func is_hotseat() -> bool:
|
||||||
return game != null and game.humans.size() > 1 and online_seat < 0
|
return game != null and game.humans.size() > 1 and not net_play
|
||||||
|
|
||||||
|
|
||||||
func is_online() -> bool:
|
func is_online() -> bool:
|
||||||
return online_seat >= 0
|
return net_play
|
||||||
|
|
||||||
|
|
||||||
|
## Zuschauer im Netzwerk: kein eigener Platz, keine Hand.
|
||||||
|
func spectating() -> bool:
|
||||||
|
return is_online() and online_seat < 0
|
||||||
|
|
||||||
|
|
||||||
func is_client() -> bool:
|
func is_client() -> bool:
|
||||||
@@ -184,6 +202,8 @@ func _ready() -> void:
|
|||||||
if is_online():
|
if is_online():
|
||||||
net = app.net
|
net = app.net
|
||||||
net.ended.connect(_on_net_ended)
|
net.ended.connect(_on_net_ended)
|
||||||
|
net.migrated.connect(_on_migrated)
|
||||||
|
net.status_changed.connect(_on_net_status)
|
||||||
if net.is_host:
|
if net.is_host:
|
||||||
game = net.game
|
game = net.game
|
||||||
net.action_applied.connect(after_action)
|
net.action_applied.connect(after_action)
|
||||||
@@ -191,6 +211,8 @@ func _ready() -> void:
|
|||||||
net.snapshot_received.connect(_on_snapshot)
|
net.snapshot_received.connect(_on_snapshot)
|
||||||
wait_label.visible = true
|
wait_label.visible = true
|
||||||
return # der erste Zustand kommt vom Host
|
return # der erste Zustand kommt vom Host
|
||||||
|
elif saved_game != null:
|
||||||
|
game = saved_game
|
||||||
elif not hotseat_decks.is_empty():
|
elif not hotseat_decks.is_empty():
|
||||||
game = Game.new_hotseat(hotseat_decks, hotseat_ai)
|
game = Game.new_hotseat(hotseat_decks, hotseat_ai)
|
||||||
elif tutorial:
|
elif tutorial:
|
||||||
@@ -227,7 +249,7 @@ func own_rules() -> Dictionary:
|
|||||||
func _begin() -> void:
|
func _begin() -> void:
|
||||||
n_opp = game.players.size() - 1
|
n_opp = game.players.size() - 1
|
||||||
if is_online():
|
if is_online():
|
||||||
perspective = game.players[online_seat]
|
perspective = game.players[maxi(online_seat, 0)]
|
||||||
else:
|
else:
|
||||||
game.events.clear()
|
game.events.clear()
|
||||||
perspective = game.current() if game.current().is_human else game.humans[0]
|
perspective = game.current() if game.current().is_human else game.humans[0]
|
||||||
@@ -235,6 +257,8 @@ func _begin() -> void:
|
|||||||
if is_online() and net.is_host:
|
if is_online() and net.is_host:
|
||||||
net.broadcast()
|
net.broadcast()
|
||||||
game.events.clear()
|
game.events.clear()
|
||||||
|
b_save_leave.visible = resumable()
|
||||||
|
save_battle()
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
@@ -304,7 +328,14 @@ func _build_static() -> void:
|
|||||||
notice_label = Style.label(overlay, "", 18, Style.GREEN, Rect2(0, 385, 1280, 80), "center")
|
notice_label = Style.label(overlay, "", 18, Style.GREEN, Rect2(0, 385, 1280, 80), "center")
|
||||||
Style.button(overlay, "Zurück zum Menü", Rect2(530, 480, 220, 50), leave, Color("285a3c"))
|
Style.button(overlay, "Zurück zum Menü", Rect2(530, 480, 220, 50), leave, Color("285a3c"))
|
||||||
# Aufgeben bestätigen
|
# Aufgeben bestätigen
|
||||||
confirm = Style.confirm(self, "Wirklich aufgeben?", "Der Kampf endet ohne Münzen.", "Ja, aufgeben", leave, "Weiterspielen")
|
confirm = Style.overlay(self)
|
||||||
|
Style.panel(confirm, Rect2(340, 290, 600, 200), Style.PANEL, 10, Style.MUTED, 2)
|
||||||
|
Style.label(confirm, "Wirklich aufgeben?", 26, Style.TEXT, Rect2(340, 312, 600, 40), "center", true)
|
||||||
|
Style.label(confirm, tr("Aufgeben beendet den Kampf ohne Münzen.") + "\n" + tr("Speichern hebt ihn für später auf."), 15, Style.MUTED, Rect2(340, 356, 600, 48), "center")
|
||||||
|
Style.button(confirm, "Ja, aufgeben", Rect2(355, 420, 180, 46), leave, Color("5a2828"))
|
||||||
|
b_save_leave = Style.button(confirm, "Speichern und zurück", Rect2(550, 420, 180, 46), save_and_leave, Color("285a3c"))
|
||||||
|
b_save_leave.add_theme_font_size_override("font_size", 14)
|
||||||
|
Style.button(confirm, "Weiterspielen", Rect2(745, 420, 180, 46), func(): confirm.visible = false)
|
||||||
# Übergabe zwischen Menschen (Hotseat)
|
# Übergabe zwischen Menschen (Hotseat)
|
||||||
handover = _overlay()
|
handover = _overlay()
|
||||||
Style.panel(handover, Rect2(340, 280, 600, 220), Style.PANEL, 10, Style.GOLD, 2)
|
Style.panel(handover, Rect2(340, 280, 600, 220), Style.PANEL, 10, Style.GOLD, 2)
|
||||||
@@ -324,9 +355,31 @@ func _overlay() -> Control:
|
|||||||
return Style.overlay(self)
|
return Style.overlay(self)
|
||||||
|
|
||||||
|
|
||||||
|
## Einzelspieler-Kämpfe (auch Kampagne und Herausforderung) lassen sich unterbrechen und fortsetzen.
|
||||||
|
func resumable() -> bool:
|
||||||
|
return counts_for_profile() and not tutorial
|
||||||
|
|
||||||
|
|
||||||
|
func save_battle() -> void:
|
||||||
|
if not resumable() or game == null or game.winner != "":
|
||||||
|
return
|
||||||
|
var d := {"version": 1, "n_opp": n_opp, "stage": stage, "challenge": challenge, "played_ids": played_ids, "game": game.snapshot(-1)}
|
||||||
|
var f := FileAccess.open(app.battle_save_path(), FileAccess.WRITE)
|
||||||
|
if f != null:
|
||||||
|
f.store_string(JSON.stringify(d))
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
|
func save_and_leave() -> void:
|
||||||
|
save_battle()
|
||||||
|
app.show_screen(MenuScreen.new())
|
||||||
|
|
||||||
|
|
||||||
func leave() -> void:
|
func leave() -> void:
|
||||||
if is_online():
|
if is_online():
|
||||||
net.leave()
|
net.leave()
|
||||||
|
if resumable() and game != null and game.winner == "":
|
||||||
|
app.clear_battle_save() # aufgegeben
|
||||||
if tutorial:
|
if tutorial:
|
||||||
if tutorial_part == 1:
|
if tutorial_part == 1:
|
||||||
app.profile.tutorial_done = true
|
app.profile.tutorial_done = true
|
||||||
@@ -398,7 +451,9 @@ func build_hero_boxes() -> void:
|
|||||||
if not o.perks.is_empty():
|
if not o.perks.is_empty():
|
||||||
title += " ★"
|
title += " ★"
|
||||||
labels["name"] = Style.label(hero_layer, title, 14 if c else 18, Style.TEXT,
|
labels["name"] = Style.label(hero_layer, title, 14 if c else 18, Style.TEXT,
|
||||||
Rect2(box.position.x + 10, box.position.y + 6, box.size.x - (44 if c else 20), 26), "left", true)
|
Rect2(box.position.x + 10, box.position.y + 6, box.size.x - (58 if c else 20), 26), "left", true)
|
||||||
|
labels.name.clip_text = true
|
||||||
|
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
labels["life"] = Style.label(hero_layer, "", 16 if c else 20, Style.RED,
|
labels["life"] = Style.label(hero_layer, "", 16 if c else 20, Style.RED,
|
||||||
Rect2(box.position.x + 10, box.position.y + (35 if c else 32), 120, 28), "left", true)
|
Rect2(box.position.x + 10, box.position.y + (35 if c else 32), 120, 28), "left", true)
|
||||||
labels["stats"] = Style.label(hero_layer, "", 12 if c else 14, Style.MUTED,
|
labels["stats"] = Style.label(hero_layer, "", 12 if c else 14, Style.MUTED,
|
||||||
@@ -414,6 +469,8 @@ func build_hero_boxes() -> void:
|
|||||||
# --- Ablauf und Eingabe ------------------------------------------------------------------
|
# --- Ablauf und Eingabe ------------------------------------------------------------------
|
||||||
|
|
||||||
func my_turn() -> bool:
|
func my_turn() -> bool:
|
||||||
|
if spectating():
|
||||||
|
return false
|
||||||
return game != null and game.winner == "" and game.current() == perspective
|
return game != null and game.winner == "" and game.current() == perspective
|
||||||
|
|
||||||
|
|
||||||
@@ -500,7 +557,7 @@ func _process(delta: float) -> void:
|
|||||||
handover.visible = true
|
handover.visible = true
|
||||||
hand_layer.visible = false
|
hand_layer.visible = false
|
||||||
trap_layer.visible = false
|
trap_layer.visible = false
|
||||||
elif cur == perspective and cur.turns == 1 and not cur.mulligan_done and not mulligan.visible and not handover.visible:
|
elif cur == perspective and not spectating() and cur.turns == 1 and not cur.mulligan_done and not mulligan.visible and not handover.visible:
|
||||||
show_mulligan()
|
show_mulligan()
|
||||||
return
|
return
|
||||||
if is_client():
|
if is_client():
|
||||||
@@ -558,6 +615,7 @@ func _finish() -> void:
|
|||||||
app.profile.tutorial2_done = true
|
app.profile.tutorial2_done = true
|
||||||
app.save()
|
app.save()
|
||||||
app.sfx.play("win" if won else "lose")
|
app.sfx.play("win" if won else "lose")
|
||||||
|
app.clear_battle_save()
|
||||||
else:
|
else:
|
||||||
var wp := game.winner_player
|
var wp := game.winner_player
|
||||||
var mine: bool = wp != null and wp == perspective
|
var mine: bool = wp != null and wp == perspective
|
||||||
@@ -704,7 +762,7 @@ func on_click(kind: String, obj) -> void:
|
|||||||
selected = obj
|
selected = obj
|
||||||
say(tr("Wähle ein Angriffsziel"))
|
say(tr("Wähle ein Angriffsziel"))
|
||||||
else:
|
else:
|
||||||
say(tr("%s kann nicht angreifen") % obj.name)
|
say(tr("%s kann nicht angreifen") % obj.card.title())
|
||||||
after_action()
|
after_action()
|
||||||
|
|
||||||
|
|
||||||
@@ -761,13 +819,13 @@ func combo_hints(card: Cards.Card) -> Array[String]:
|
|||||||
var combos := {}
|
var combos := {}
|
||||||
for other in perspective.board:
|
for other in perspective.board:
|
||||||
if other.card.elements.any(func(e): return e in card.elements):
|
if other.card.elements.any(func(e): return e in card.elements):
|
||||||
partners.append(other.name)
|
partners.append(other.card.title())
|
||||||
for e1 in card.elements:
|
for e1 in card.elements:
|
||||||
for e2 in other.card.elements:
|
for e2 in other.card.elements:
|
||||||
if e1 != e2:
|
if e1 != e2:
|
||||||
var name := Cards.combo_name([e1, e2])
|
var name := Cards.combo_name([e1, e2])
|
||||||
if not combos.has(name):
|
if not combos.has(name):
|
||||||
combos[name] = other.name
|
combos[name] = other.card.title()
|
||||||
if not partners.is_empty():
|
if not partners.is_empty():
|
||||||
out.append("Resonanz +%d mit %s" % [mini(partners.size(), Game.RESONANCE_MAX), ", ".join(partners)])
|
out.append("Resonanz +%d mit %s" % [mini(partners.size(), Game.RESONANCE_MAX), ", ".join(partners)])
|
||||||
for name in combos:
|
for name in combos:
|
||||||
@@ -784,11 +842,28 @@ func _on_snapshot(snap: Dictionary) -> void:
|
|||||||
if first:
|
if first:
|
||||||
_begin()
|
_begin()
|
||||||
return
|
return
|
||||||
perspective = game.players[online_seat]
|
perspective = game.players[maxi(online_seat, 0)]
|
||||||
build_hero_boxes()
|
build_hero_boxes()
|
||||||
after_action()
|
after_action()
|
||||||
|
|
||||||
|
|
||||||
|
## Host-Wechsel: das Spiel läuft jetzt auf diesem Rechner weiter.
|
||||||
|
func _on_migrated() -> void:
|
||||||
|
game = net.game
|
||||||
|
if not net.action_applied.is_connected(after_action):
|
||||||
|
net.action_applied.connect(after_action)
|
||||||
|
wait_label.visible = false
|
||||||
|
perspective = game.players[online_seat]
|
||||||
|
build_hero_boxes()
|
||||||
|
say("Du bist jetzt Host.")
|
||||||
|
after_action()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_net_status() -> void:
|
||||||
|
wait_label.text = net.status
|
||||||
|
wait_label.visible = true
|
||||||
|
|
||||||
|
|
||||||
func _on_net_ended(reason: String) -> void:
|
func _on_net_ended(reason: String) -> void:
|
||||||
if game == null or game.winner == "":
|
if game == null or game.winner == "":
|
||||||
say(reason)
|
say(reason)
|
||||||
@@ -814,6 +889,7 @@ func after_action() -> void:
|
|||||||
centers[ref_key(ev[key])] = target_center(ev[key])
|
centers[ref_key(ev[key])] = target_center(ev[key])
|
||||||
refresh()
|
refresh()
|
||||||
fx.play_events(evs, centers)
|
fx.play_events(evs, centers)
|
||||||
|
save_battle()
|
||||||
|
|
||||||
|
|
||||||
func _note_events(evs: Array) -> void:
|
func _note_events(evs: Array) -> void:
|
||||||
@@ -962,21 +1038,24 @@ func refresh() -> void:
|
|||||||
trap_views.append(tv)
|
trap_views.append(tv)
|
||||||
|
|
||||||
Style.clear(hand_layer)
|
Style.clear(hand_layer)
|
||||||
for i in h.hand.size():
|
if spectating():
|
||||||
var card: Cards.Card = h.hand[i]
|
Style.label(hand_layer, "Zuschauer: Hände sind verdeckt", 18, Style.MUTED, Rect2(0, 690, 1280, 30), "center")
|
||||||
var playable: bool = mine and g.can_play(h, card)[0]
|
else:
|
||||||
var r := BattleLayout.hand_rect(h.hand.size(), i)
|
for i in h.hand.size():
|
||||||
var v := CardView.new(card, r.size)
|
var card: Cards.Card = h.hand[i]
|
||||||
v.position = r.position
|
var playable: bool = mine and g.can_play(h, card)[0]
|
||||||
if card == pending:
|
var r := BattleLayout.hand_rect(h.hand.size(), i)
|
||||||
v.border = Style.YELLOW
|
var v := CardView.new(card, r.size)
|
||||||
elif playable:
|
v.position = r.position
|
||||||
v.border = Style.GREEN
|
if card == pending:
|
||||||
v.dim = not playable
|
v.border = Style.YELLOW
|
||||||
v.hover.connect(show_hand_preview)
|
elif playable:
|
||||||
v.clicked.connect(func(_view): on_click("hand", card))
|
v.border = Style.GREEN
|
||||||
hand_layer.add_child(v)
|
v.dim = not playable
|
||||||
hand_views.append(v)
|
v.hover.connect(show_hand_preview)
|
||||||
|
v.clicked.connect(func(_view): on_click("hand", card))
|
||||||
|
hand_layer.add_child(v)
|
||||||
|
hand_views.append(v)
|
||||||
|
|
||||||
var bar_border = Style.BLUE if mine else null
|
var bar_border = Style.BLUE if mine else null
|
||||||
if int(flash_until.get(g.players.find(h), 0)) > now:
|
if int(flash_until.get(g.players.find(h), 0)) > now:
|
||||||
@@ -987,10 +1066,12 @@ func refresh() -> void:
|
|||||||
energy_label.text = "◆ %d/%d" % [h.energy, h.max_energy]
|
energy_label.text = "◆ %d/%d" % [h.energy, h.max_energy]
|
||||||
deck_label.text = "Deck %d" % h.deck.size()
|
deck_label.text = "Deck %d" % h.deck.size()
|
||||||
status_label.text = (tr("Zug %d · Du bist dran") % g.turn) if mine else (tr("Zug %d · %s ist dran …") % [g.turn, g.current().name])
|
status_label.text = (tr("Zug %d · Du bist dran") % g.turn) if mine else (tr("Zug %d · %s ist dran …") % [g.turn, g.current().name])
|
||||||
|
if spectating():
|
||||||
|
status_label.text = tr("Zuschauer") + " · " + status_label.text
|
||||||
b_end.disabled = not mine
|
b_end.disabled = not mine
|
||||||
var ai_running: bool = g.winner == "" and not g.current().is_human and not is_client()
|
var ai_running: bool = g.winner == "" and not g.current().is_human and not is_client()
|
||||||
b_skip.visible = ai_running
|
b_skip.visible = ai_running
|
||||||
b_end.visible = not ai_running
|
b_end.visible = not ai_running and not spectating()
|
||||||
b_undo.visible = undo_state != null and mine
|
b_undo.visible = undo_state != null and mine
|
||||||
var lines: Array[String] = []
|
var lines: Array[String] = []
|
||||||
var start := maxi(0, g.log.size() - 60)
|
var start := maxi(0, g.log.size() - 60)
|
||||||
|
|||||||
@@ -38,8 +38,11 @@ func stage_index() -> int:
|
|||||||
return 0 if part == 1 else 1
|
return 0 if part == 1 else 1
|
||||||
|
|
||||||
|
|
||||||
|
## Teil 1: ein Wesen für 1 Energie auf der Hand, damit der erste Schritt immer möglich ist.
|
||||||
## Teil 2: Lehrkarten auf die eigene Hand; Mara bekommt Wesen, damit sie sicher in die Falle läuft.
|
## Teil 2: Lehrkarten auf die eigene Hand; Mara bekommt Wesen, damit sie sicher in die Falle läuft.
|
||||||
func inject_cards(g: Game) -> void:
|
func inject_cards(g: Game) -> void:
|
||||||
|
if part == 1 and not g.human.hand.any(func(c): return c.is_creature() and c.cost == 1):
|
||||||
|
g.human.hand.append(Cards.by_id("f_kobold"))
|
||||||
if part == 2:
|
if part == 2:
|
||||||
g.human.hand.append(Cards.by_id("w_falle"))
|
g.human.hand.append(Cards.by_id("w_falle"))
|
||||||
g.human.hand.append(Cards.by_id("w_reiter"))
|
g.human.hand.append(Cards.by_id("w_reiter"))
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ var b_all: Button
|
|||||||
var b_improve: Button
|
var b_improve: Button
|
||||||
var suggestion := ""
|
var suggestion := ""
|
||||||
var slots: Control
|
var slots: Control
|
||||||
|
var element_buttons: Array = []
|
||||||
|
|
||||||
|
|
||||||
class CardBack extends Control:
|
class CardBack extends Control:
|
||||||
@@ -24,15 +25,21 @@ class CardBack extends Control:
|
|||||||
size = sz
|
size = sz
|
||||||
custom_minimum_size = sz
|
custom_minimum_size = sz
|
||||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
focus_mode = Control.FOCUS_ALL
|
||||||
|
focus_entered.connect(queue_redraw)
|
||||||
|
focus_exited.connect(queue_redraw)
|
||||||
|
|
||||||
func _gui_input(event: InputEvent) -> void:
|
func _gui_input(event: InputEvent) -> void:
|
||||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
accept_event()
|
accept_event()
|
||||||
clicked.emit()
|
clicked.emit()
|
||||||
|
elif has_focus() and event.is_action_pressed("ui_accept"):
|
||||||
|
accept_event()
|
||||||
|
clicked.emit()
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
var rect := Rect2(Vector2.ZERO, size)
|
var rect := Rect2(Vector2.ZERO, size)
|
||||||
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD, 3), rect)
|
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD, 5 if has_focus() else 3), rect)
|
||||||
Style.border(self, rect.grow(-15), Color("5a3c82"), 2, 6)
|
Style.border(self, rect.grow(-15), Color("5a3c82"), 2, 6)
|
||||||
Style.text(self, "?", 48, Style.GOLD, size / 2, "center", true)
|
Style.text(self, "?", 48, Style.GOLD, size / 2, "center", true)
|
||||||
|
|
||||||
@@ -48,15 +55,20 @@ func _ready() -> void:
|
|||||||
coins_label = Style.label(self, "", 22, Style.GOLD, Rect2(0, 78, 1280, 30), "center")
|
coins_label = Style.label(self, "", 22, Style.GOLD, Rect2(0, 78, 1280, 30), "center")
|
||||||
Style.label(self, "5 Karten pro Booster, mindestens eine seltene. Klicke auf eine Karte zum Aufdecken.",
|
Style.label(self, "5 Karten pro Booster, mindestens eine seltene. Klicke auf eine Karte zum Aufdecken.",
|
||||||
16, Style.MUTED, Rect2(0, 115, 1280, 24), "center")
|
16, Style.MUTED, Rect2(0, 115, 1280, 24), "center")
|
||||||
|
Style.label(self, "Elementbooster (%d Münzen): nur Karten des Elements und seiner Kombos" % Profile.ELEMENT_BOOSTER_COST,
|
||||||
|
14, Style.MUTED, Rect2(0, 586, 1280, 22), "center")
|
||||||
|
for i in Cards.ELEMENTS.size():
|
||||||
|
var e: String = Cards.ELEMENTS[i]
|
||||||
|
element_buttons.append(Style.button(self, e, Rect2(322 + i * 162, 612, 150, 38), buy.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8)))
|
||||||
slots = Control.new()
|
slots = Control.new()
|
||||||
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
add_child(slots)
|
add_child(slots)
|
||||||
msg_label = Style.label(self, "", 18, Style.RED, Rect2(0, 610, 1280, 30), "center")
|
msg_label = Style.label(self, "", 17, Style.RED, Rect2(0, 700, 1280, 22), "center")
|
||||||
b_buy = Style.button(self, "Booster kaufen (%d Münzen)" % Profile.BOOSTER_COST, Rect2(360, 720, 260, 48), buy, Color("5a4628"))
|
b_buy = Style.button(self, "Booster kaufen (%d Münzen)" % Profile.BOOSTER_COST, Rect2(290, 720, 330, 48), buy, Color("5a4628"))
|
||||||
b_all = Style.button(self, "Alle aufdecken", Rect2(640, 720, 140, 48), reveal_all)
|
b_all = Style.button(self, "Alle aufdecken", Rect2(640, 720, 200, 48), reveal_all)
|
||||||
b_improve = Style.button(self, "", Rect2(360, 650, 560, 44), improve, Color("285a3c"))
|
b_improve = Style.button(self, "", Rect2(360, 650, 560, 44), improve, Color("285a3c"))
|
||||||
b_improve.visible = false
|
b_improve.visible = false
|
||||||
Style.button(self, "Zurück", Rect2(800, 720, 120, 48), func(): app.show_screen(MenuScreen.new()))
|
Style.button(self, "Zurück", Rect2(860, 720, 130, 48), func(): app.show_screen(MenuScreen.new()))
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
@@ -65,10 +77,10 @@ func _input(event: InputEvent) -> void:
|
|||||||
app.show_screen(MenuScreen.new())
|
app.show_screen(MenuScreen.new())
|
||||||
|
|
||||||
|
|
||||||
func buy() -> void:
|
func buy(element: String = "") -> void:
|
||||||
var p: Profile = app.profile
|
var p: Profile = app.profile
|
||||||
var before: Dictionary = p.collection.duplicate()
|
var before: Dictionary = p.collection.duplicate()
|
||||||
var got: Array = p.open_booster()
|
var got: Array = p.open_booster(null, element)
|
||||||
if got.is_empty():
|
if got.is_empty():
|
||||||
msg_label.text = "Nicht genug Münzen"
|
msg_label.text = "Nicht genug Münzen"
|
||||||
msg_label.add_theme_color_override("font_color", Style.RED)
|
msg_label.add_theme_color_override("font_color", Style.RED)
|
||||||
@@ -106,7 +118,7 @@ func improve() -> void:
|
|||||||
var err: String = app.profile.improve_deck_with(suggestion)
|
var err: String = app.profile.improve_deck_with(suggestion)
|
||||||
if err == "":
|
if err == "":
|
||||||
app.save()
|
app.save()
|
||||||
msg_label.text = "%s ist jetzt im Deck %s" % [Cards.by_id(suggestion).name, app.profile.deck_name()]
|
msg_label.text = tr("%s ist jetzt im Deck %s") % [Cards.by_id(suggestion).title(), app.profile.deck_name()]
|
||||||
msg_label.add_theme_color_override("font_color", Style.GREEN)
|
msg_label.add_theme_color_override("font_color", Style.GREEN)
|
||||||
suggestion = ""
|
suggestion = ""
|
||||||
else:
|
else:
|
||||||
@@ -119,6 +131,8 @@ func refresh() -> void:
|
|||||||
var p: Profile = app.profile
|
var p: Profile = app.profile
|
||||||
coins_label.text = "Münzen: %d" % p.coins
|
coins_label.text = "Münzen: %d" % p.coins
|
||||||
b_buy.disabled = p.coins < Profile.BOOSTER_COST
|
b_buy.disabled = p.coins < Profile.BOOSTER_COST
|
||||||
|
for b in element_buttons:
|
||||||
|
b.disabled = p.coins < Profile.ELEMENT_BOOSTER_COST
|
||||||
b_all.disabled = cards.is_empty() or revealed.size() >= 5
|
b_all.disabled = cards.is_empty() or revealed.size() >= 5
|
||||||
# Deck-Vorschlag, sobald alle Karten offen liegen
|
# Deck-Vorschlag, sobald alle Karten offen liegen
|
||||||
suggestion = ""
|
suggestion = ""
|
||||||
@@ -129,7 +143,7 @@ func refresh() -> void:
|
|||||||
suggestion = better[0]
|
suggestion = better[0]
|
||||||
b_improve.visible = suggestion != ""
|
b_improve.visible = suggestion != ""
|
||||||
if suggestion != "":
|
if suggestion != "":
|
||||||
b_improve.text = tr("Deck verbessern: %s ins aktive Deck (%s)") % [Cards.by_id(suggestion).name, p.deck_name()]
|
b_improve.text = tr("Deck verbessern: %s ins aktive Deck (%s)") % [Cards.by_id(suggestion).title(), p.deck_name()]
|
||||||
Style.clear(slots)
|
Style.clear(slots)
|
||||||
for i in 5:
|
for i in 5:
|
||||||
var r := slot_rect(i)
|
var r := slot_rect(i)
|
||||||
@@ -140,7 +154,7 @@ func refresh() -> void:
|
|||||||
var v := CardView.new(c, r.size)
|
var v := CardView.new(c, r.size)
|
||||||
v.position = r.position
|
v.position = r.position
|
||||||
slots.add_child(v)
|
slots.add_child(v)
|
||||||
Style.label(slots, c.rarity, 16, Cards.RARITY_COLORS[c.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true)
|
Style.label(slots, tr(c.rarity), 16, Cards.RARITY_COLORS[c.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true)
|
||||||
if new_ids.has(c.id):
|
if new_ids.has(c.id):
|
||||||
Style.label(slots, "Neu!", 16, Style.GREEN, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
|
Style.label(slots, "Neu!", 16, Style.GREEN, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ var perk_box: OptionButton
|
|||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
var p: Profile = app.profile
|
var p: Profile = app.profile
|
||||||
chapter = 2 if p.campaign_stage >= Campaign.STAGES.size() else 1
|
chapter = mini(Campaign.chapter(p.campaign_stage), Campaign.CHAPTERS.size())
|
||||||
build()
|
build()
|
||||||
|
|
||||||
|
|
||||||
@@ -19,9 +19,9 @@ func build() -> void:
|
|||||||
Style.clabel(col, "Kampagne", 34, Style.GOLD, "center", true)
|
Style.clabel(col, "Kampagne", 34, Style.GOLD, "center", true)
|
||||||
var tabs := Style.row(col, 10)
|
var tabs := Style.row(col, 10)
|
||||||
tabs.alignment = BoxContainer.ALIGNMENT_CENTER
|
tabs.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
var b1 := Style.cbutton(tabs, "Kapitel 1", Vector2(160, 36), func(): set_chapter(1), Color("285a3c") if chapter == 1 else null)
|
for c in Campaign.CHAPTERS.size():
|
||||||
var b2 := Style.cbutton(tabs, "Kapitel 2", Vector2(160, 36), func(): set_chapter(2), Color("285a3c") if chapter == 2 else null)
|
var b := Style.cbutton(tabs, "Kapitel %d" % (c + 1), Vector2(140, 36), set_chapter.bind(c + 1), Color("285a3c") if chapter == c + 1 else null)
|
||||||
b2.disabled = p.campaign_stage < Campaign.STAGES.size()
|
b.disabled = p.campaign_stage < c * Campaign.STAGES.size()
|
||||||
if p.perk_unlocked():
|
if p.perk_unlocked():
|
||||||
var pr := Style.row(tabs, 8)
|
var pr := Style.row(tabs, 8)
|
||||||
Style.clabel(pr, "Dein Merkmal:", 15, Style.MUTED, "right", false, 110)
|
Style.clabel(pr, "Dein Merkmal:", 15, Style.MUTED, "right", false, 110)
|
||||||
@@ -36,9 +36,11 @@ func build() -> void:
|
|||||||
p.perk = "" if i == 0 else keys[i - 1]
|
p.perk = "" if i == 0 else keys[i - 1]
|
||||||
app.save())
|
app.save())
|
||||||
pr.add_child(perk_box)
|
pr.add_child(perk_box)
|
||||||
Style.clabel(col, "Zehn Magier je Kapitel. Jede Stufe bringt mehr Gegner an den Tisch. Bosse haben Merkmale (★)." if chapter == 1
|
var intro := ["Zehn Magier je Kapitel. Jede Stufe bringt mehr Gegner an den Tisch. Bosse haben Merkmale (★).",
|
||||||
else "Die Magier kehren mit zwei Merkmalen und größerem Gefolge zurück.", 14, Style.MUTED)
|
"Die Magier kehren mit zwei Merkmalen und größerem Gefolge zurück.",
|
||||||
var offset := 0 if chapter == 1 else Campaign.STAGES.size()
|
"Kapitel 3: drei Merkmale, volle Tische und die stärksten Decks."]
|
||||||
|
Style.clabel(col, intro[chapter - 1], 14, Style.MUTED)
|
||||||
|
var offset := (chapter - 1) * Campaign.STAGES.size()
|
||||||
for k in Campaign.STAGES.size():
|
for k in Campaign.STAGES.size():
|
||||||
var i := offset + k
|
var i := offset + k
|
||||||
var st := Campaign.stage(i)
|
var st := Campaign.stage(i)
|
||||||
@@ -76,7 +78,7 @@ func build() -> void:
|
|||||||
var b := Style.cbutton(r, "Gesperrt", Vector2(130, 36), func(): pass)
|
var b := Style.cbutton(r, "Gesperrt", Vector2(130, 36), func(): pass)
|
||||||
b.disabled = true
|
b.disabled = true
|
||||||
if p.campaign_stage >= Campaign.total():
|
if p.campaign_stage >= Campaign.total():
|
||||||
Style.clabel(col, "Beide Kapitel abgeschlossen. Der Archimagier ist endgültig besiegt!", 16, Style.GREEN, "center", true)
|
Style.clabel(col, "Alle drei Kapitel abgeschlossen. Der Archimagier ist endgültig besiegt!", 16, Style.GREEN, "center", true)
|
||||||
var br := Style.row(col)
|
var br := Style.row(col)
|
||||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
Style.cbutton(br, "Zurück", Vector2(160, 40), func(): app.show_screen(MenuScreen.new()))
|
Style.cbutton(br, "Zurück", Vector2(160, 40), func(): app.show_screen(MenuScreen.new()))
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ func _init(rect: Rect2) -> void:
|
|||||||
position = rect.position
|
position = rect.position
|
||||||
size = rect.size
|
size = rect.size
|
||||||
horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
follow_focus = true # Gamepad: der fokussierten Karte hinterherscrollen
|
||||||
grid = GridContainer.new()
|
grid = GridContainer.new()
|
||||||
grid.columns = COLS
|
grid.columns = COLS
|
||||||
grid.add_theme_constant_override("h_separation", GAP)
|
grid.add_theme_constant_override("h_separation", GAP)
|
||||||
@@ -31,7 +32,7 @@ func fill(cards: Array, label_fn: Callable, dim_fn: Callable) -> void:
|
|||||||
var cell := Control.new()
|
var cell := Control.new()
|
||||||
cell.custom_minimum_size = Vector2(CW, CH + 16)
|
cell.custom_minimum_size = Vector2(CW, CH + 16)
|
||||||
cell.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
cell.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
var v := CardView.new(c, Vector2(CW, CH))
|
var v := CardView.new(c, Vector2(CW, CH), true)
|
||||||
v.dim = dim_fn.call(c)
|
v.dim = dim_fn.call(c)
|
||||||
v.clicked.connect(func(_view): card_clicked.emit(c))
|
v.clicked.connect(func(_view): card_clicked.emit(c))
|
||||||
v.hover.connect(func(card, entered): card_hover.emit(card, entered))
|
v.hover.connect(func(card, entered): card_hover.emit(card, entered))
|
||||||
|
|||||||
@@ -10,19 +10,31 @@ var border = null # Color oder null (dann Seltenheitsfarbe)
|
|||||||
var dim := false
|
var dim := false
|
||||||
|
|
||||||
|
|
||||||
func _init(c: Cards.Card, sz: Vector2) -> void:
|
## focusable: per Gamepad oder Tab anwählbar (Sammlung, Booster); A bzw. Enter wirkt wie ein Klick.
|
||||||
|
func _init(c: Cards.Card, sz: Vector2, focusable: bool = false) -> void:
|
||||||
card = c
|
card = c
|
||||||
custom_minimum_size = sz
|
custom_minimum_size = sz
|
||||||
size = sz
|
size = sz
|
||||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
mouse_entered.connect(func(): hover.emit(card, true))
|
mouse_entered.connect(func(): hover.emit(card, true))
|
||||||
mouse_exited.connect(func(): hover.emit(card, false))
|
mouse_exited.connect(func(): hover.emit(card, false))
|
||||||
|
if focusable:
|
||||||
|
focus_mode = Control.FOCUS_ALL
|
||||||
|
focus_entered.connect(func():
|
||||||
|
hover.emit(card, true)
|
||||||
|
queue_redraw())
|
||||||
|
focus_exited.connect(func():
|
||||||
|
hover.emit(card, false)
|
||||||
|
queue_redraw())
|
||||||
|
|
||||||
|
|
||||||
func _gui_input(event: InputEvent) -> void:
|
func _gui_input(event: InputEvent) -> void:
|
||||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
accept_event()
|
accept_event()
|
||||||
clicked.emit(self)
|
clicked.emit(self)
|
||||||
|
elif has_focus() and event.is_action_pressed("ui_accept"):
|
||||||
|
accept_event()
|
||||||
|
clicked.emit(self)
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
@@ -41,10 +53,11 @@ func _draw() -> void:
|
|||||||
draw_circle(cc, r, Color("14141c"))
|
draw_circle(cc, r, Color("14141c"))
|
||||||
Style.text(self, str(card.cost), f_stat, Style.GOLD, cc, "center", true)
|
Style.text(self, str(card.cost), f_stat, Style.GOLD, cc, "center", true)
|
||||||
# Name und Kombo
|
# Name und Kombo
|
||||||
var nm := Style.fit(card.name, true, f_name, size.x - 2 * r - 12)
|
var name_left := 2 * r + 10
|
||||||
Style.text(self, nm, f_name, Style.TEXT, Vector2(size.x / 2 + r / 2.0, 4), "midtop", true)
|
var nm := Style.fit(card.title(), true, f_name, size.x - name_left - 6)
|
||||||
|
Style.text(self, nm, f_name, Style.TEXT, Vector2((name_left + size.x - 6) / 2.0, 4), "midtop", true)
|
||||||
if mid:
|
if mid:
|
||||||
var sub: String = (card.combo() + " · " + card.kind) if card.is_combo() else card.kind
|
var sub: String = (tr(card.combo()) + " · " + tr(card.kind)) if card.is_combo() else tr(card.kind)
|
||||||
Style.text(self, sub, f_text, Color("e6e6e6"), Vector2(size.x / 2, 2 * r + 4), "midtop")
|
Style.text(self, sub, f_text, Color("e6e6e6"), Vector2(size.x / 2, 2 * r + 4), "midtop")
|
||||||
# Textfeld: Höhe folgt dem umbrochenen Text und der Schriftgröße, das Bild bekommt den Rest
|
# Textfeld: Höhe folgt dem umbrochenen Text und der Schriftgröße, das Bild bekommt den Rest
|
||||||
var tsize := Style.fs(f_text)
|
var tsize := Style.fs(f_text)
|
||||||
@@ -60,11 +73,14 @@ func _draw() -> void:
|
|||||||
var gsize: float = minf(band_bottom - band_top, size.x * 0.42)
|
var gsize: float = minf(band_bottom - band_top, size.x * 0.42)
|
||||||
Style.glyph(self, card, Vector2(size.x / 2, (band_top + band_bottom) / 2), gsize)
|
Style.glyph(self, card, Vector2(size.x / 2, (band_top + band_bottom) / 2), gsize)
|
||||||
draw_rect(box, Color(0.06, 0.06, 0.09, 0.67))
|
draw_rect(box, Color(0.06, 0.06, 0.09, 0.67))
|
||||||
|
var max_lines := maxi(1, int((box.size.y - 6) / Style.FONT.get_height(tsize))) # große Schrift: lieber kürzen als überlaufen
|
||||||
draw_multiline_string(Style.FONT, Vector2(box.position.x + 4, box.position.y + 4 + Style.FONT.get_ascent(tsize)),
|
draw_multiline_string(Style.FONT, Vector2(box.position.x + 4, box.position.y + 4 + Style.FONT.get_ascent(tsize)),
|
||||||
card.text(), HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, tsize, -1, Style.TEXT)
|
card.text(), HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, tsize, max_lines, Style.TEXT)
|
||||||
# Werte
|
# Werte
|
||||||
if card.is_creature():
|
if card.is_creature():
|
||||||
Style.text(self, str(card.attack), f_stat, Style.YELLOW, Vector2(10, size.y - 6), "bottomleft", true)
|
Style.text(self, str(card.attack), f_stat, Style.YELLOW, Vector2(10, size.y - 6), "bottomleft", true)
|
||||||
Style.text(self, str(card.health), f_stat, Style.RED, Vector2(size.x - 10, size.y - 6), "bottomright", true)
|
Style.text(self, str(card.health), f_stat, Style.RED, Vector2(size.x - 10, size.y - 6), "bottomright", true)
|
||||||
if dim:
|
if dim:
|
||||||
draw_rect(rect, Color(0, 0, 0, 0.47))
|
draw_rect(rect, Color(0, 0, 0, 0.47))
|
||||||
|
if has_focus():
|
||||||
|
Style.border(self, rect, Style.GOLD, 4, 8)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func _draw() -> void:
|
|||||||
_draw_mini()
|
_draw_mini()
|
||||||
return
|
return
|
||||||
var fs := 11 if small else 13
|
var fs := 11 if small else 13
|
||||||
var nm := Style.fit(creature.name, true, fs, size.x - 6)
|
var nm := Style.fit(creature.card.title(), true, fs, size.x - 6)
|
||||||
Style.text(self, nm, fs, Style.TEXT, Vector2(size.x / 2, 4), "midtop", true)
|
Style.text(self, nm, fs, Style.TEXT, Vector2(size.x / 2, 4), "midtop", true)
|
||||||
var badges: Array[String] = []
|
var badges: Array[String] = []
|
||||||
if creature.taunt():
|
if creature.taunt():
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ var b_activate: Button
|
|||||||
var b_autofill: Button
|
var b_autofill: Button
|
||||||
var name_box: LineEdit
|
var name_box: LineEdit
|
||||||
var confirm: Control
|
var confirm: Control
|
||||||
|
var code_dialog: Control
|
||||||
|
var code_box: LineEdit
|
||||||
|
|
||||||
|
|
||||||
class DeckRow extends Control:
|
class DeckRow extends Control:
|
||||||
@@ -43,26 +45,30 @@ class DeckRow extends Control:
|
|||||||
size = sz
|
size = sz
|
||||||
custom_minimum_size = sz
|
custom_minimum_size = sz
|
||||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
mouse_entered.connect(func():
|
focus_mode = Control.FOCUS_ALL
|
||||||
lit = true
|
var light := func(on: bool):
|
||||||
|
lit = on
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
hover.emit(card, true))
|
hover.emit(card, on)
|
||||||
mouse_exited.connect(func():
|
mouse_entered.connect(light.bind(true))
|
||||||
lit = false
|
mouse_exited.connect(light.bind(false))
|
||||||
queue_redraw()
|
focus_entered.connect(light.bind(true))
|
||||||
hover.emit(card, false))
|
focus_exited.connect(light.bind(false))
|
||||||
|
|
||||||
func _gui_input(event: InputEvent) -> void:
|
func _gui_input(event: InputEvent) -> void:
|
||||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
accept_event()
|
accept_event()
|
||||||
clicked.emit(card)
|
clicked.emit(card)
|
||||||
|
elif has_focus() and event.is_action_pressed("ui_accept"):
|
||||||
|
accept_event()
|
||||||
|
clicked.emit(card)
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
if lit:
|
if lit:
|
||||||
draw_style_box(Style.box(Style.PANEL_LIGHT, 4), Rect2(Vector2.ZERO, size))
|
draw_style_box(Style.box(Style.PANEL_LIGHT, 4), Rect2(Vector2.ZERO, size))
|
||||||
for k in card.elements.size():
|
for k in card.elements.size():
|
||||||
draw_circle(Vector2(10 + k * 12, size.y / 2), 7, Cards.ELEMENT_COLORS[card.elements[k]])
|
draw_circle(Vector2(10 + k * 12, size.y / 2), 7, Cards.ELEMENT_COLORS[card.elements[k]])
|
||||||
Style.text(self, "%dx %s" % [count, card.name], 15, Style.TEXT, Vector2(40, size.y / 2), "midleft")
|
Style.text(self, "%dx %s" % [count, card.title()], 15, Style.TEXT, Vector2(40, size.y / 2), "midleft")
|
||||||
Style.text(self, str(card.cost), 15, Style.GOLD, Vector2(size.x - 10, size.y / 2), "midright", true)
|
Style.text(self, str(card.cost), 15, Style.GOLD, Vector2(size.x - 10, size.y / 2), "midright", true)
|
||||||
|
|
||||||
|
|
||||||
@@ -93,10 +99,11 @@ func _ready() -> void:
|
|||||||
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))
|
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))
|
||||||
for i in Cards.ELEMENTS.size():
|
for i in Cards.ELEMENTS.size():
|
||||||
var e: String = Cards.ELEMENTS[i]
|
var e: String = Cards.ELEMENTS[i]
|
||||||
Style.button(self, e, Rect2(88 + i * 76, 55, 70, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
var fb := Style.button(self, e, Rect2(88 + i * 82, 55, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||||
|
fb.add_theme_font_size_override("font_size", 14)
|
||||||
search_box = LineEdit.new()
|
search_box = LineEdit.new()
|
||||||
search_box.position = Vector2(400, 55)
|
search_box.position = Vector2(424, 55)
|
||||||
search_box.size = Vector2(160, 34)
|
search_box.size = Vector2(136, 34)
|
||||||
search_box.placeholder_text = "Suche"
|
search_box.placeholder_text = "Suche"
|
||||||
search_box.text_changed.connect(func(t: String):
|
search_box.text_changed.connect(func(t: String):
|
||||||
search = t
|
search = t
|
||||||
@@ -150,9 +157,23 @@ func _ready() -> void:
|
|||||||
curve.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
curve.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
add_child(curve)
|
add_child(curve)
|
||||||
msg_label = Style.label(self, "", 16, Style.GREEN, Rect2(860, 736, 400, 24))
|
msg_label = Style.label(self, "", 16, Style.GREEN, Rect2(860, 736, 400, 24))
|
||||||
Style.button(self, "Speichern", Rect2(860, 740, 180, 44), save_deck, Color("285a3c"))
|
Style.button(self, "Speichern", Rect2(860, 740, 140, 44), save_deck, Color("285a3c"))
|
||||||
Style.button(self, "Leeren", Rect2(1050, 740, 90, 44), clear_deck)
|
Style.button(self, "Leeren", Rect2(1008, 740, 70, 44), clear_deck)
|
||||||
Style.button(self, "Zurück", Rect2(1150, 740, 110, 44), leave)
|
Style.button(self, "Code", Rect2(1086, 740, 84, 44), open_code)
|
||||||
|
Style.button(self, "Zurück", Rect2(1178, 740, 82, 44), leave)
|
||||||
|
# Deck-Code zum Teilen: kopieren, einfügen, übernehmen
|
||||||
|
code_dialog = Style.overlay(self)
|
||||||
|
Style.panel(code_dialog, Rect2(240, 270, 800, 240), Style.PANEL, 10, Style.GOLD, 2)
|
||||||
|
Style.label(code_dialog, "Deck-Code", 26, Style.TEXT, Rect2(240, 290, 800, 40), "center", true)
|
||||||
|
Style.label(code_dialog, "Zum Teilen kopieren oder einen Code einfügen. Nicht vorhandene Karten werden weggelassen.", 15, Style.MUTED, Rect2(240, 334, 800, 24), "center")
|
||||||
|
code_box = LineEdit.new()
|
||||||
|
code_box.position = Vector2(270, 368)
|
||||||
|
code_box.size = Vector2(740, 36)
|
||||||
|
code_dialog.add_child(code_box)
|
||||||
|
Style.button(code_dialog, "In Zwischenablage", Rect2(270, 430, 180, 44), func(): DisplayServer.clipboard_set(code_box.text))
|
||||||
|
Style.button(code_dialog, "Aus Zwischenablage", Rect2(460, 430, 180, 44), func(): code_box.text = DisplayServer.clipboard_get())
|
||||||
|
Style.button(code_dialog, "Übernehmen", Rect2(650, 430, 170, 44), func(): apply_code(code_box.text), Color("285a3c"))
|
||||||
|
Style.button(code_dialog, "Schließen", Rect2(830, 430, 180, 44), func(): code_dialog.visible = false)
|
||||||
confirm = Style.confirm(self, "Ungespeicherte Änderungen", "Das Deck wurde geändert, aber nicht gespeichert.",
|
confirm = Style.confirm(self, "Ungespeicherte Änderungen", "Das Deck wurde geändert, aber nicht gespeichert.",
|
||||||
"Verwerfen", func(): app.show_screen(MenuScreen.new()), "Weiter bearbeiten", Style.GOLD)
|
"Verwerfen", func(): app.show_screen(MenuScreen.new()), "Weiter bearbeiten", Style.GOLD)
|
||||||
preview = CardView.new(Cards.all()[0], Vector2(200, 280))
|
preview = CardView.new(Cards.all()[0], Vector2(200, 280))
|
||||||
@@ -174,9 +195,11 @@ func leave() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE and not search_box.has_focus() and not name_box.has_focus():
|
if event.is_action_pressed("ui_cancel") and not search_box.has_focus() and not name_box.has_focus() and not code_box.has_focus():
|
||||||
if confirm.visible:
|
if confirm.visible:
|
||||||
confirm.visible = false
|
confirm.visible = false
|
||||||
|
elif code_dialog.visible:
|
||||||
|
code_dialog.visible = false
|
||||||
else:
|
else:
|
||||||
leave()
|
leave()
|
||||||
|
|
||||||
@@ -244,7 +267,7 @@ func cards() -> Array:
|
|||||||
out = out.filter(func(c): return filter in c.elements)
|
out = out.filter(func(c): return filter in c.elements)
|
||||||
if search != "":
|
if search != "":
|
||||||
var q := search.to_lower()
|
var q := search.to_lower()
|
||||||
out = out.filter(func(c): return q in c.name.to_lower() or q in c.text().to_lower() or q in c.combo().to_lower())
|
out = out.filter(func(c): return q in c.name.to_lower() or q in c.title().to_lower() or q in c.text().to_lower() or q in c.combo().to_lower())
|
||||||
match sort_mode:
|
match sort_mode:
|
||||||
0:
|
0:
|
||||||
out.sort_custom(func(a, b): return a.cost < b.cost or (a.cost == b.cost and a.name < b.name))
|
out.sort_custom(func(a, b): return a.cost < b.cost or (a.cost == b.cost and a.name < b.name))
|
||||||
@@ -308,6 +331,34 @@ func save_deck() -> void:
|
|||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func open_code() -> void:
|
||||||
|
code_box.text = Profile.deck_code(deck)
|
||||||
|
code_dialog.visible = true
|
||||||
|
|
||||||
|
|
||||||
|
## Übernimmt einen Deck-Code; Karten, die in der Sammlung fehlen, werden gemeldet und weggelassen.
|
||||||
|
func apply_code(code: String) -> void:
|
||||||
|
var ids: Array = Profile.parse_deck_code(code)
|
||||||
|
if ids.is_empty():
|
||||||
|
say("Ungültiger Code", false)
|
||||||
|
return
|
||||||
|
var p: Profile = app.profile
|
||||||
|
var missing := {}
|
||||||
|
deck = []
|
||||||
|
for cid in ids:
|
||||||
|
var owned: int = p.collection.get(cid, 0)
|
||||||
|
if deck.count(cid) < owned and deck.size() < Game.DECK_SIZE:
|
||||||
|
deck.append(cid)
|
||||||
|
else:
|
||||||
|
missing[cid] = true
|
||||||
|
code_dialog.visible = false
|
||||||
|
if missing.is_empty():
|
||||||
|
say("Code übernommen", true)
|
||||||
|
else:
|
||||||
|
say(tr("Code übernommen, es fehlen: ") + ", ".join(missing.keys().map(func(cid): return Cards.by_id(cid).title())), false)
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
func deck_cards() -> Array:
|
func deck_cards() -> Array:
|
||||||
var out := []
|
var out := []
|
||||||
for cid in deck:
|
for cid in deck:
|
||||||
@@ -361,7 +412,7 @@ func refresh() -> void:
|
|||||||
deck_label.add_theme_color_override("font_color", Style.GREEN if deck.size() == Game.DECK_SIZE else Style.TEXT)
|
deck_label.add_theme_color_override("font_color", Style.GREEN if deck.size() == Game.DECK_SIZE else Style.TEXT)
|
||||||
var elems := Game.deck_elements(dc)
|
var elems := Game.deck_elements(dc)
|
||||||
elems.sort_custom(func(a, b): return Cards.ELEMENTS.find(a) < Cards.ELEMENTS.find(b))
|
elems.sort_custom(func(a, b): return Cards.ELEMENTS.find(a) < Cards.ELEMENTS.find(b))
|
||||||
elems_label.text = "Elemente: " + (", ".join(elems) if not elems.is_empty() else "–")
|
elems_label.text = tr("Elemente: ") + (", ".join(elems.map(func(e): return tr(e))) if not elems.is_empty() else "–")
|
||||||
if not dc.is_empty():
|
if not dc.is_empty():
|
||||||
elems_label.text += " · " + tr("Stärke %.1f") % Game.deck_rating(dc)
|
elems_label.text += " · " + tr("Stärke %.1f") % Game.deck_rating(dc)
|
||||||
var ok: Array = Game.validate_deck(dc, p.collection)
|
var ok: Array = Game.validate_deck(dc, p.collection)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ var list_label: Label
|
|||||||
var ip_box: LineEdit
|
var ip_box: LineEdit
|
||||||
var b_host: Button
|
var b_host: Button
|
||||||
var b_join: Button
|
var b_join: Button
|
||||||
|
var b_watch: Button
|
||||||
var b_start: Button
|
var b_start: Button
|
||||||
var ai_buttons: Array = []
|
var ai_buttons: Array = []
|
||||||
var n_ai := 0
|
var n_ai := 0
|
||||||
@@ -32,6 +33,7 @@ func _ready() -> void:
|
|||||||
ip_box.text = "127.0.0.1"
|
ip_box.text = "127.0.0.1"
|
||||||
hr.add_child(ip_box)
|
hr.add_child(ip_box)
|
||||||
b_join = Style.cbutton(hr, "Beitreten", Vector2(160, 46), _join)
|
b_join = Style.cbutton(hr, "Beitreten", Vector2(160, 46), _join)
|
||||||
|
b_watch = Style.cbutton(hr, "Zuschauen", Vector2(150, 46), _watch)
|
||||||
addr_label = Style.clabel(col, "", 14, Style.MUTED)
|
addr_label = Style.clabel(col, "", 14, Style.MUTED)
|
||||||
status_label = Style.clabel(col, "", 16, Style.BLUE)
|
status_label = Style.clabel(col, "", 16, Style.BLUE)
|
||||||
Style.spacer(col, 6)
|
Style.spacer(col, 6)
|
||||||
@@ -71,6 +73,13 @@ func _join() -> void:
|
|||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
## Als Zuschauer beitreten: sieht die Partie ohne Hände, spielt nicht mit.
|
||||||
|
func _watch() -> void:
|
||||||
|
net.spectator = true
|
||||||
|
net.join(ip_box.text.strip_edges())
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
func _set_ai(n: int) -> void:
|
func _set_ai(n: int) -> void:
|
||||||
n_ai = n
|
n_ai = n
|
||||||
refresh()
|
refresh()
|
||||||
@@ -96,9 +105,12 @@ func refresh() -> void:
|
|||||||
ids.sort()
|
ids.sort()
|
||||||
for id in ids:
|
for id in ids:
|
||||||
lines.append("%s%s" % [net.names[id], " (Host)" if id == 1 else ""])
|
lines.append("%s%s" % [net.names[id], " (Host)" if id == 1 else ""])
|
||||||
|
for id in net.spectators:
|
||||||
|
lines.append("%s (%s)" % [net.spectators[id], tr("Zuschauer")])
|
||||||
list_label.text = "\n".join(lines) if not lines.is_empty() else "–"
|
list_label.text = "\n".join(lines) if not lines.is_empty() else "–"
|
||||||
b_host.disabled = net.connected
|
b_host.disabled = net.connected
|
||||||
b_join.disabled = net.connected
|
b_join.disabled = net.connected
|
||||||
|
b_watch.disabled = net.connected
|
||||||
ip_box.editable = not net.connected
|
ip_box.editable = not net.connected
|
||||||
var humans := ids.size()
|
var humans := ids.size()
|
||||||
b_start.disabled = not (net.is_host and humans >= 2 and humans + n_ai <= Game.MAX_OPPONENTS + 1)
|
b_start.disabled = not (net.is_host and humans >= 2 and humans + n_ai <= Game.MAX_OPPONENTS + 1)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ func apply_settings() -> void:
|
|||||||
func reset_profile() -> void:
|
func reset_profile() -> void:
|
||||||
profile = Profile.starter()
|
profile = Profile.starter()
|
||||||
profile.refresh_daily()
|
profile.refresh_daily()
|
||||||
|
clear_battle_save()
|
||||||
save()
|
save()
|
||||||
apply_settings()
|
apply_settings()
|
||||||
|
|
||||||
@@ -108,6 +109,16 @@ func save() -> void:
|
|||||||
profile.save(save_path)
|
profile.save(save_path)
|
||||||
|
|
||||||
|
|
||||||
|
## Datei für einen unterbrochenen Kampf (neben dem Spielstand).
|
||||||
|
func battle_save_path() -> String:
|
||||||
|
return save_path.get_basename() + "_kampf.json"
|
||||||
|
|
||||||
|
|
||||||
|
func clear_battle_save() -> void:
|
||||||
|
if FileAccess.file_exists(battle_save_path()):
|
||||||
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(battle_save_path()))
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
if net != null and net.is_inside_tree():
|
if net != null and net.is_inside_tree():
|
||||||
net.leave()
|
net.leave()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ var b_hotseat: Button
|
|||||||
var b_online: Button
|
var b_online: Button
|
||||||
var b_tutorial: Button
|
var b_tutorial: Button
|
||||||
var b_challenge: Button
|
var b_challenge: Button
|
||||||
|
var b_resume: Button
|
||||||
var coins_label: Label
|
var coins_label: Label
|
||||||
var coll_label: Label
|
var coll_label: Label
|
||||||
var deck_label: Label
|
var deck_label: Label
|
||||||
@@ -36,6 +37,8 @@ func _ready() -> void:
|
|||||||
var sz := Vector2(270, 44)
|
var sz := Vector2(270, 44)
|
||||||
var p0: Profile = app.profile
|
var p0: Profile = app.profile
|
||||||
var part2: bool = p0.tutorial_done and not p0.tutorial2_done
|
var part2: bool = p0.tutorial_done and not p0.tutorial2_done
|
||||||
|
if FileAccess.file_exists(app.battle_save_path()):
|
||||||
|
b_resume = Style.cbutton(grid, "Kampf fortsetzen", sz, _resume, Color("5a4628"))
|
||||||
b_tutorial = Style.cbutton(grid, "Anleitung 2: Fallen" if part2 else "Anleitung", sz,
|
b_tutorial = Style.cbutton(grid, "Anleitung 2: Fallen" if part2 else "Anleitung", sz,
|
||||||
func(): app.show_screen(BattleScreen.guided(2 if part2 else 1)),
|
func(): app.show_screen(BattleScreen.guided(2 if part2 else 1)),
|
||||||
Color("5a4628") if not (p0.tutorial_done and p0.tutorial2_done) else null)
|
Color("5a4628") if not (p0.tutorial_done and p0.tutorial2_done) else null)
|
||||||
@@ -58,6 +61,17 @@ func _ready() -> void:
|
|||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
## Unterbrochenen Kampf laden; eine unlesbare Datei wird verworfen.
|
||||||
|
func _resume() -> void:
|
||||||
|
var json := JSON.new()
|
||||||
|
var text := FileAccess.get_file_as_string(app.battle_save_path())
|
||||||
|
if json.parse(text) != OK or not (json.data is Dictionary) or not json.data.has("game"):
|
||||||
|
app.clear_battle_save()
|
||||||
|
app.show_screen(MenuScreen.new())
|
||||||
|
return
|
||||||
|
app.show_screen(BattleScreen.resume(json.data))
|
||||||
|
|
||||||
|
|
||||||
func refresh() -> void:
|
func refresh() -> void:
|
||||||
var p: Profile = app.profile
|
var p: Profile = app.profile
|
||||||
var ok: Array = p.deck_valid()
|
var ok: Array = p.deck_valid()
|
||||||
@@ -74,7 +88,7 @@ func refresh() -> void:
|
|||||||
coll_label.text = tr("Sammlung: %d von %d Karten (%d insgesamt) · Kampagne: %d/%d") % [
|
coll_label.text = tr("Sammlung: %d von %d Karten (%d insgesamt) · Kampagne: %d/%d") % [
|
||||||
p.distinct_owned(), Cards.all().size(), total, p.campaign_stage, Campaign.total()]
|
p.distinct_owned(), Cards.all().size(), total, p.campaign_stage, Campaign.total()]
|
||||||
var elems := Game.deck_elements(p.deck_cards())
|
var elems := Game.deck_elements(p.deck_cards())
|
||||||
deck_label.text = tr("Aktives Deck: %s (%s)") % [p.deck_name(), ", ".join(elems) if not elems.is_empty() else tr("leer")]
|
deck_label.text = tr("Aktives Deck: %s (%s)") % [p.deck_name(), ", ".join(elems.map(func(e): return tr(e))) if not elems.is_empty() else tr("leer")]
|
||||||
var task := p.daily_task()
|
var task := p.daily_task()
|
||||||
if p.daily.done:
|
if p.daily.done:
|
||||||
daily_label.text = tr("Tagesaufgabe erledigt: %s ✓") % task.text
|
daily_label.text = tr("Tagesaufgabe erledigt: %s ✓") % task.text
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ func _ready() -> void:
|
|||||||
var info := (tr("Duell") if n == 1 else tr("%d am Tisch") % (n + 1)) + " · " + tr("%d Münzen") % Profile.REWARD_WIN[n]
|
var info := (tr("Duell") if n == 1 else tr("%d am Tisch") % (n + 1)) + " · " + tr("%d Münzen") % Profile.REWARD_WIN[n]
|
||||||
Style.clabel(r, info, 13, Style.MUTED, "left", false, 150)
|
Style.clabel(r, info, 13, Style.MUTED, "left", false, 150)
|
||||||
preview_labels[n] = Style.clabel(r, "", 13, Style.TEXT, "left", false, 600)
|
preview_labels[n] = Style.clabel(r, "", 13, Style.TEXT, "left", false, 600)
|
||||||
|
preview_labels[n].autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||||
|
preview_labels[n].custom_minimum_size = Vector2(600, 0)
|
||||||
|
preview_labels[n].size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
Style.spacer(col, 4)
|
Style.spacer(col, 4)
|
||||||
var br := Style.row(col, 16)
|
var br := Style.row(col, 16)
|
||||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ func _ready() -> void:
|
|||||||
language = OptionButton.new()
|
language = OptionButton.new()
|
||||||
language.custom_minimum_size = Vector2(280, 36)
|
language.custom_minimum_size = Vector2(280, 36)
|
||||||
language.add_item("Deutsch", 0)
|
language.add_item("Deutsch", 0)
|
||||||
language.add_item("English (Oberfläche)", 1)
|
language.add_item("English", 1)
|
||||||
language.select(1 if s.language == "en" else 0)
|
language.select(1 if s.language == "en" else 0)
|
||||||
language.item_selected.connect(_on_language)
|
language.item_selected.connect(_on_language)
|
||||||
grid.add_child(language)
|
grid.add_child(language)
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ static func rules_text() -> String:
|
|||||||
Cards.RESONANCE_TEXT,
|
Cards.RESONANCE_TEXT,
|
||||||
"Feldkombo: Spielst du ein Wesen neben ein eigenes Wesen mit anderem Element, löst eine Kombo aus (eine je Ausspielen):",
|
"Feldkombo: Spielst du ein Wesen neben ein eigenes Wesen mit anderem Element, löst eine Kombo aus (eine je Ausspielen):",
|
||||||
" Dampf friert das stärkste gegnerische Wesen, Lava macht 1 Schaden an das stärkste gegnerische Wesen, Blitz 2 Schaden",
|
" Dampf friert das stärkste gegnerische Wesen, Lava macht 1 Schaden an das stärkste gegnerische Wesen, Blitz 2 Schaden",
|
||||||
" an den schwächsten Gegner, Schlamm heilt dich um 3, Sturm zieht 1 Karte, Sand gibt dem neuen Wesen Schild.",
|
" an den schwächsten Gegner, Schlamm heilt dich um 3, Sturm zieht 1 Karte, Sand gibt dem neuen Wesen +1/+1.",
|
||||||
"",
|
"",
|
||||||
"Schlüsselwörter:",
|
"Schlüsselwörter:",
|
||||||
]
|
]
|
||||||
@@ -295,9 +295,13 @@ static func rules_text() -> String:
|
|||||||
lines.append("Deck: genau %d Karten, höchstens %dx dieselbe Karte." % [Game.DECK_SIZE, Game.MAX_COPIES])
|
lines.append("Deck: genau %d Karten, höchstens %dx dieselbe Karte." % [Game.DECK_SIZE, Game.MAX_COPIES])
|
||||||
lines.append("Leeres Deck: jede weitere gezogene Karte verursacht steigenden Schaden.")
|
lines.append("Leeres Deck: jede weitere gezogene Karte verursacht steigenden Schaden.")
|
||||||
lines.append("Booster: 5 Karten für 100 Münzen, mindestens eine seltene. Siege bringen Münzen.")
|
lines.append("Booster: 5 Karten für 100 Münzen, mindestens eine seltene. Siege bringen Münzen.")
|
||||||
|
lines.append("Elementbooster: 5 Karten eines Elements (und seiner Kombos) für %d Münzen." % Profile.ELEMENT_BOOSTER_COST)
|
||||||
|
lines.append("Deck-Code: im Deckbau unter „Code“ kopieren und mit anderen teilen.")
|
||||||
|
lines.append("Ein unterbrochener Einzelspieler-Kampf lässt sich im Menü fortsetzen.")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("Werkstatt: Karten zerlegen bringt Staub, aus Staub entstehen neue Karten. Fünf Deck-Slots.")
|
lines.append("Werkstatt: Karten zerlegen bringt Staub, aus Staub entstehen neue Karten. Fünf Deck-Slots.")
|
||||||
lines.append("Lokal zu mehreren: bis zu vier Menschen am selben Rechner, dazu KI, ohne Münzen.")
|
lines.append("Lokal zu mehreren: bis zu vier Menschen am selben Rechner, dazu KI, ohne Münzen.")
|
||||||
|
lines.append("Netzwerkspiel: Fällt der Host aus, übernimmt ein Mitspieler; Zuschauer sehen die Partie ohne Hände.")
|
||||||
lines.append("Mulligan: Im ersten eigenen Zug darfst du einmal Handkarten zurücklegen und nachziehen.")
|
lines.append("Mulligan: Im ersten eigenen Zug darfst du einmal Handkarten zurücklegen und nachziehen.")
|
||||||
lines.append("Deckstärke: Seltenheit, Schlüsselwörter und Effekte ergeben je Karte einen Wert. Gegnerdecks werden auf deine")
|
lines.append("Deckstärke: Seltenheit, Schlüsselwörter und Effekte ergeben je Karte einen Wert. Gegnerdecks werden auf deine")
|
||||||
lines.append(" Stärke gebaut (Leicht 0,9-fach, Normal 1-fach, Schwer 1,2-fach mit mehr Münzen). Die Kampagne liegt nie darunter.")
|
lines.append(" Stärke gebaut (Leicht 0,9-fach, Normal 1-fach, Schwer 1,2-fach mit mehr Münzen). Die Kampagne liegt nie darunter.")
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ func _ready() -> void:
|
|||||||
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))
|
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))
|
||||||
for i in Cards.ELEMENTS.size():
|
for i in Cards.ELEMENTS.size():
|
||||||
var e: String = Cards.ELEMENTS[i]
|
var e: String = Cards.ELEMENTS[i]
|
||||||
Style.button(self, e, Rect2(88 + i * 76, 55, 70, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
var fb := Style.button(self, e, Rect2(88 + i * 82, 55, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||||
|
fb.add_theme_font_size_override("font_size", 14)
|
||||||
search_box = LineEdit.new()
|
search_box = LineEdit.new()
|
||||||
search_box.position = Vector2(400, 55)
|
search_box.position = Vector2(424, 55)
|
||||||
search_box.size = Vector2(200, 34)
|
search_box.size = Vector2(200, 34)
|
||||||
search_box.placeholder_text = "Suche"
|
search_box.placeholder_text = "Suche"
|
||||||
search_box.text_changed.connect(func(t: String):
|
search_box.text_changed.connect(func(t: String):
|
||||||
@@ -51,7 +52,7 @@ func _ready() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE and not search_box.has_focus():
|
if event.is_action_pressed("ui_cancel") and not search_box.has_focus():
|
||||||
app.show_screen(MenuScreen.new())
|
app.show_screen(MenuScreen.new())
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ func cards() -> Array:
|
|||||||
out = out.filter(func(c): return filter in c.elements)
|
out = out.filter(func(c): return filter in c.elements)
|
||||||
if search != "":
|
if search != "":
|
||||||
var q := search.to_lower()
|
var q := search.to_lower()
|
||||||
out = out.filter(func(c): return q in c.name.to_lower() or q in c.text().to_lower() or q in c.combo().to_lower())
|
out = out.filter(func(c): return q in c.name.to_lower() or q in c.title().to_lower() or q in c.text().to_lower() or q in c.combo().to_lower())
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ func dismantle() -> void:
|
|||||||
if err == "":
|
if err == "":
|
||||||
app.sfx.play("click")
|
app.sfx.play("click")
|
||||||
app.save()
|
app.save()
|
||||||
say("%s zerlegt (+%d Staub)" % [selected.name, Profile.DUST_DISMANTLE[selected.rarity]], true)
|
say(tr("%s zerlegt (+%d Staub)") % [selected.title(), Profile.DUST_DISMANTLE[selected.rarity]], true)
|
||||||
else:
|
else:
|
||||||
say(err, false)
|
say(err, false)
|
||||||
refresh()
|
refresh()
|
||||||
@@ -102,7 +103,7 @@ func craft() -> void:
|
|||||||
app.sfx.play("play")
|
app.sfx.play("play")
|
||||||
app.save()
|
app.save()
|
||||||
var notes: Array[String] = app.profile.take_notices()
|
var notes: Array[String] = app.profile.take_notices()
|
||||||
say("%s hergestellt" % selected.name + (" · " + " · ".join(notes) if not notes.is_empty() else ""), true)
|
say(tr("%s hergestellt") % selected.title() + (" · " + " · ".join(notes) if not notes.is_empty() else ""), true)
|
||||||
else:
|
else:
|
||||||
say(err, false)
|
say(err, false)
|
||||||
refresh()
|
refresh()
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
extends Node
|
extends Node
|
||||||
## Test-Mitspieler: tritt einem Host bei und spielt einfache Züge.
|
## Test-Mitspieler: tritt einem Host bei und spielt einfache Züge; mit --host eröffnet er
|
||||||
|
## selbst und startet, sobald ein zweiter Mensch am Tisch sitzt (ohne KI).
|
||||||
## godot --path . tests/net_bot.tscn -- --ip=127.0.0.1 --name=Bot
|
## godot --path . tests/net_bot.tscn -- --ip=127.0.0.1 --name=Bot
|
||||||
|
## godot --path . tests/net_bot.tscn -- --host --name=BotHost
|
||||||
|
|
||||||
var net: Net
|
var net: Net
|
||||||
var seat := -1
|
var seat := -1
|
||||||
var game: Game
|
var game: Game
|
||||||
var t := 0.0
|
var t := 0.0
|
||||||
|
var hosting := false
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -16,20 +19,37 @@ func _ready() -> void:
|
|||||||
ip = a.substr(5)
|
ip = a.substr(5)
|
||||||
if a.begins_with("--name="):
|
if a.begins_with("--name="):
|
||||||
bot_name = a.substr(7)
|
bot_name = a.substr(7)
|
||||||
|
if a == "--host":
|
||||||
|
hosting = true
|
||||||
net = Net.new()
|
net = Net.new()
|
||||||
net.name = "Net"
|
net.name = "Net"
|
||||||
get_tree().root.add_child.call_deferred(net)
|
get_tree().root.add_child.call_deferred(net)
|
||||||
net.my_name = bot_name
|
net.my_name = bot_name
|
||||||
net.my_deck = Profile.starter().deck.duplicate()
|
net.my_deck = Profile.starter().deck.duplicate()
|
||||||
net.started.connect(func(s): seat = s)
|
net.started.connect(func(s):
|
||||||
|
seat = s
|
||||||
|
if hosting:
|
||||||
|
net.broadcast() # erster Zustand für alle, wie es sonst der Kampfbildschirm tut
|
||||||
|
_act_host.call_deferred())
|
||||||
net.snapshot_received.connect(_on_snapshot)
|
net.snapshot_received.connect(_on_snapshot)
|
||||||
net.ended.connect(func(_r): get_tree().quit())
|
net.ended.connect(func(_r): get_tree().quit())
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
net.join(ip)
|
if hosting:
|
||||||
|
net.host()
|
||||||
|
net.lobby_changed.connect(func():
|
||||||
|
if net.game == null and net.names.size() >= 2:
|
||||||
|
net.start_battle(0))
|
||||||
|
net.action_applied.connect(func():
|
||||||
|
net.broadcast()
|
||||||
|
_act_host.call_deferred())
|
||||||
|
else:
|
||||||
|
net.join(ip)
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
t += delta
|
t += delta
|
||||||
|
if hosting and net.game != null:
|
||||||
|
net.tick()
|
||||||
if t > 180.0 or FileAccess.file_exists("user://bot_quit"):
|
if t > 180.0 or FileAccess.file_exists("user://bot_quit"):
|
||||||
net.leave()
|
net.leave()
|
||||||
get_tree().quit()
|
get_tree().quit()
|
||||||
@@ -43,7 +63,23 @@ func _on_snapshot(snap: Dictionary) -> void:
|
|||||||
return
|
return
|
||||||
if game.current_idx != seat:
|
if game.current_idx != seat:
|
||||||
return
|
return
|
||||||
var me: Game.Player = game.players[seat]
|
_decide(game.players[seat])
|
||||||
|
|
||||||
|
|
||||||
|
func _act_host() -> void:
|
||||||
|
if not hosting or net.game == null or net.game.winner != "" or seat < 0 or net.game.current_idx != seat:
|
||||||
|
return
|
||||||
|
game = net.game
|
||||||
|
_decide(game.players[seat])
|
||||||
|
|
||||||
|
|
||||||
|
## Einfache Züge mit kurzer Bedenkzeit: Mulligan überspringen, erstes spielbares Wesen ohne Ziel,
|
||||||
|
## dann angreifen, sonst Zug beenden.
|
||||||
|
func _decide(me: Game.Player) -> void:
|
||||||
|
var g := game
|
||||||
|
await get_tree().create_timer(0.25).timeout
|
||||||
|
if game != g or (hosting and (net.game == null or net.game.current_idx != seat)):
|
||||||
|
return # inzwischen ein neuer Zustand
|
||||||
if not me.mulligan_done and me.turns == 1:
|
if not me.mulligan_done and me.turns == 1:
|
||||||
net.send_action({"type": "mulligan", "hand": []})
|
net.send_action({"type": "mulligan", "hand": []})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func _initialize() -> void:
|
|||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
r.seed = 21
|
r.seed = 21
|
||||||
var arts := archetypes()
|
var arts := archetypes()
|
||||||
var n := 60
|
var n := int(OS.get_environment("N")) if OS.get_environment("N") != "" else 60
|
||||||
var results := {}
|
var results := {}
|
||||||
for a in arts:
|
for a in arts:
|
||||||
var wins := 0
|
var wins := 0
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ func put(p: Game.Player, cid: String, ready: bool = true) -> Game.Creature:
|
|||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
|
TranslationServer.set_locale("de") # das Spiel setzt die Sprache aus den Einstellungen; hier fehlt der Einstieg
|
||||||
test_kartenpool()
|
test_kartenpool()
|
||||||
|
test_kampf_speichern()
|
||||||
|
test_elementbooster_und_deckcode()
|
||||||
|
test_englische_karten()
|
||||||
test_kombos()
|
test_kombos()
|
||||||
test_wesen_spielen()
|
test_wesen_spielen()
|
||||||
test_flink()
|
test_flink()
|
||||||
@@ -117,7 +121,7 @@ func _initialize() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func test_kartenpool() -> void:
|
func test_kartenpool() -> void:
|
||||||
check(Cards.all().size() == 110, "110 Karten")
|
check(Cards.all().size() == 118, "118 Karten")
|
||||||
for key in Cards.COMBOS:
|
for key in Cards.COMBOS:
|
||||||
var found: bool = Cards.all().any(func(c): return c.is_combo() and Cards.combo_key(c.elements) == key)
|
var found: bool = Cards.all().any(func(c): return c.is_combo() and Cards.combo_key(c.elements) == key)
|
||||||
check(found, "Kombo vorhanden: " + key)
|
check(found, "Kombo vorhanden: " + key)
|
||||||
@@ -462,7 +466,7 @@ func test_spielstand() -> void:
|
|||||||
p.coins = 999
|
p.coins = 999
|
||||||
p.save(path)
|
p.save(path)
|
||||||
var p2 := Profile.load_or_new(path)
|
var p2 := Profile.load_or_new(path)
|
||||||
check(p2.coins == 999 and p2.deck.size() == 20 and p2.collection.size() == 42, "geladen")
|
check(p2.coins == 999 and p2.deck.size() == 20 and p2.collection.size() == Cards.of_rarity("gewöhnlich").size() + 2, "geladen")
|
||||||
for suffix in ["", ".bak"]:
|
for suffix in ["", ".bak"]:
|
||||||
if FileAccess.file_exists(path + suffix):
|
if FileAccess.file_exists(path + suffix):
|
||||||
DirAccess.remove_absolute(abs + suffix)
|
DirAccess.remove_absolute(abs + suffix)
|
||||||
@@ -551,7 +555,7 @@ func test_feldkombos() -> void:
|
|||||||
s[2][1].life = 5
|
s[2][1].life = 5
|
||||||
g.play_card(h, C("l_wind"))
|
g.play_card(h, C("l_wind"))
|
||||||
check(s[2][1].life == 3 and s[2][0].life == 15, "Blitz trifft den schwächsten Gegner")
|
check(s[2][1].life == 3 and s[2][0].life == 15, "Blitz trifft den schwächsten Gegner")
|
||||||
# Schlamm heilt, Sturm zieht, Sand gibt Schild
|
# Schlamm heilt, Sturm zieht, Sand gibt +1/+1
|
||||||
s = setup(["e_wall", "l_wind", "w_reiter"])
|
s = setup(["e_wall", "l_wind", "w_reiter"])
|
||||||
g = s[0]
|
g = s[0]
|
||||||
h = s[1]
|
h = s[1]
|
||||||
@@ -562,13 +566,13 @@ func test_feldkombos() -> void:
|
|||||||
var hand_before := h.hand.size()
|
var hand_before := h.hand.size()
|
||||||
g.play_card(h, C("l_wind"))
|
g.play_card(h, C("l_wind"))
|
||||||
check(h.hand.size() == hand_before, "Sturm zieht 1 (eine gespielt, eine gezogen)")
|
check(h.hand.size() == hand_before, "Sturm zieht 1 (eine gespielt, eine gezogen)")
|
||||||
check(not h.board[2].shield, "nur eine Feldkombo je Ausspielen")
|
check(h.board[2].attack == 1, "nur eine Feldkombo je Ausspielen")
|
||||||
s = setup(["l_wind"])
|
s = setup(["l_wind"])
|
||||||
g = s[0]
|
g = s[0]
|
||||||
h = s[1]
|
h = s[1]
|
||||||
put(h, "e_wall")
|
put(h, "e_wall")
|
||||||
g.play_card(h, C("l_wind"))
|
g.play_card(h, C("l_wind"))
|
||||||
check(h.board[1].shield, "Sand: neues Wesen hat Schild")
|
check(h.board[1].attack == 2 and h.board[1].health == 2, "Sand: neues Wesen erhält +1/+1")
|
||||||
# Gleiche Elemente lösen keine Feldkombo aus
|
# Gleiche Elemente lösen keine Feldkombo aus
|
||||||
s = setup(["f_kobold"])
|
s = setup(["f_kobold"])
|
||||||
g = s[0]
|
g = s[0]
|
||||||
@@ -736,7 +740,7 @@ func test_fallen() -> void:
|
|||||||
g.end_turn()
|
g.end_turn()
|
||||||
var a := put(o, "e_troll")
|
var a := put(o, "e_troll")
|
||||||
check(g.attack(a, h) == "", "Angriff in die Feuerfalle")
|
check(g.attack(a, h) == "", "Angriff in die Feuerfalle")
|
||||||
check(h.traps.is_empty() and a.health == 3 and h.life == 26, "Feuerfalle: 3 Schaden, Angriff geht durch")
|
check(h.traps.is_empty() and a.health == 3 and h.life == 27, "Feuerfalle: 3 Schaden, Angriff geht durch")
|
||||||
# Frostfalle bricht den Angriff ab
|
# Frostfalle bricht den Angriff ab
|
||||||
s = setup(["w_falle"])
|
s = setup(["w_falle"])
|
||||||
g = s[0]
|
g = s[0]
|
||||||
@@ -1089,7 +1093,7 @@ func test_deck_umbenennen_und_auffuellen() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func test_neue_fallen() -> void:
|
func test_neue_fallen() -> void:
|
||||||
check(Cards.all().filter(func(c): return c.is_trap()).size() == 10, "zehn Fallen")
|
check(Cards.all().filter(func(c): return c.is_trap()).size() == 12, "zwölf Fallen")
|
||||||
check(C("e_sandfalle").text() == "Falle. Wenn ein Gegner ein Wesen ausspielt: Es wird eingefroren.", "Fallentext je Auslöser")
|
check(C("e_sandfalle").text() == "Falle. Wenn ein Gegner ein Wesen ausspielt: Es wird eingefroren.", "Fallentext je Auslöser")
|
||||||
check(C("f_aschefalle").text().ends_with("3 Schaden an dieses Wesen."), "Aschefalle-Text")
|
check(C("f_aschefalle").text().ends_with("3 Schaden an dieses Wesen."), "Aschefalle-Text")
|
||||||
# Böenfalle: Angreifer zurück auf die Hand, Angriff verpufft
|
# Böenfalle: Angreifer zurück auf die Hand, Angriff verpufft
|
||||||
@@ -1144,7 +1148,11 @@ func test_neue_fallen() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func test_kapitel_zwei_und_merkmalwahl() -> void:
|
func test_kapitel_zwei_und_merkmalwahl() -> void:
|
||||||
check(Campaign.total() == 20 and Campaign.chapter(9) == 1 and Campaign.chapter(10) == 2, "zwanzig Stufen in zwei Kapiteln")
|
check(Campaign.total() == 30 and Campaign.chapter(9) == 1 and Campaign.chapter(10) == 2 and Campaign.chapter(29) == 3, "dreißig Stufen in drei Kapiteln")
|
||||||
|
check(Campaign.stage(25).perks.size() == 3 and Campaign.stage(25).name == "Nyx" and Campaign.target_rating(29, 1.0) == 4.4, "Kapitel 3 mit drei Merkmalen und höherer Zielstärke")
|
||||||
|
for st in Campaign.STAGES3:
|
||||||
|
for pk in st.perks:
|
||||||
|
check(Game.PERKS.has(pk), "Merkmal %s bekannt" % pk)
|
||||||
for i in Campaign.total():
|
for i in Campaign.total():
|
||||||
var st := Campaign.stage(i)
|
var st := Campaign.stage(i)
|
||||||
check(st.has("perks") and st.perks.all(func(k): return Game.PERKS.has(k)), "Stufe %d hat gültige Merkmale" % (i + 1))
|
check(st.has("perks") and st.perks.all(func(k): return Game.PERKS.has(k)), "Stufe %d hat gültige Merkmale" % (i + 1))
|
||||||
@@ -1362,6 +1370,73 @@ func test_ki_zeitbudget() -> void:
|
|||||||
check(v.settings.has("big_font") and not v.tutorial2_done and v.records.margins is Array, "Version 5 mit neuen Feldern")
|
check(v.settings.has("big_font") and not v.tutorial2_done and v.records.margins is Array, "Version 5 mit neuen Feldern")
|
||||||
|
|
||||||
|
|
||||||
|
## Elementbooster enthalten nur Karten des Elements; Deck-Codes sind umkehrbar und robust.
|
||||||
|
## Englisch: jede Karte hat einen englischen Namen, Kartentexte und Schlüsselwörter werden übersetzt.
|
||||||
|
func test_englische_karten() -> void:
|
||||||
|
check(Cards.all().all(func(c): return c.name_en != ""), "alle Karten mit englischem Namen")
|
||||||
|
var de: String = Cards.by_id("f_funke").text()
|
||||||
|
TranslationServer.set_locale("en")
|
||||||
|
var funke := Cards.by_id("f_funke")
|
||||||
|
var falle := Cards.by_id("w_falle")
|
||||||
|
var wall := Cards.by_id("e_wall")
|
||||||
|
check(funke.title() == "Spark" and funke.text() == "2 damage to a target.", "Funke auf Englisch: %s / %s" % [funke.title(), funke.text()])
|
||||||
|
check(falle.text().begins_with("Trap. When an enemy creature attacks") and "The attack fizzles" in falle.text(), "Fallentext auf Englisch: %s" % falle.text())
|
||||||
|
check(wall.text() == "Guardian." and Cards.by_id("l_spiegel").text().ends_with("Summon a Wind Spirit."), "Schlüsselwort und Beschwörung auf Englisch")
|
||||||
|
TranslationServer.set_locale("de")
|
||||||
|
check(funke.title() == "Funke" and funke.text() == de, "zurück auf Deutsch")
|
||||||
|
|
||||||
|
|
||||||
|
func test_elementbooster_und_deckcode() -> void:
|
||||||
|
var p := Profile.starter()
|
||||||
|
p.coins = 500
|
||||||
|
var r := RandomNumberGenerator.new()
|
||||||
|
r.seed = 5
|
||||||
|
var got: Array = p.open_booster(r, "Wasser")
|
||||||
|
check(got.size() == 5 and got.all(func(c): return "Wasser" in c.elements) and p.coins == 500 - Profile.ELEMENT_BOOSTER_COST, "Wasser-Booster: fünf Wasserkarten, %d Münzen" % Profile.ELEMENT_BOOSTER_COST)
|
||||||
|
p.coins = 100
|
||||||
|
check(p.open_booster(r, "Feuer").is_empty() and p.coins == 100, "Elementbooster braucht %d Münzen" % Profile.ELEMENT_BOOSTER_COST)
|
||||||
|
for e in Cards.ELEMENTS:
|
||||||
|
for rar in Profile.RARITY_WEIGHTS:
|
||||||
|
check(not Profile.booster_pool(rar, e).is_empty(), "%s hat %s Karten" % [e, rar])
|
||||||
|
var deck: Array = Profile.starter().deck
|
||||||
|
var code := Profile.deck_code(deck)
|
||||||
|
check(code.begins_with(Profile.CODE_PREFIX) and Profile.parse_deck_code(code).size() == 20, "Deck-Code hin und zurück")
|
||||||
|
var back: Array = Profile.parse_deck_code(code)
|
||||||
|
back.sort()
|
||||||
|
var orig: Array = deck.duplicate()
|
||||||
|
orig.sort()
|
||||||
|
check(back == orig, "Deck-Code enthält genau die Karten")
|
||||||
|
check(Profile.parse_deck_code("kaputt").is_empty() and Profile.parse_deck_code("DINGER1:x_nix*2").is_empty()
|
||||||
|
and Profile.parse_deck_code("DINGER1:f_funke*9").size() == Game.MAX_COPIES, "ungültige Codes leer, Kopien gedeckelt")
|
||||||
|
check(Cards.by_id("w_flutfalle").is_trap() and Cards.by_id("fl_sturmfalke").combo() == "Blitz" and Cards.by_id("el_sandsturm").is_combo(), "neue Karten im Pool")
|
||||||
|
|
||||||
|
|
||||||
|
## Voller Spielstand (viewer -1) stellt Hände, Decks und Bretter wieder her; Zuschauer (-2) sehen nichts Verborgenes.
|
||||||
|
func test_kampf_speichern() -> void:
|
||||||
|
var r := RandomNumberGenerator.new()
|
||||||
|
r.seed = 7
|
||||||
|
var g := Game.new_battle(Profile.starter().deck_cards(), 2, r)
|
||||||
|
g.start()
|
||||||
|
AI.take_turn(g, g.current())
|
||||||
|
var d: Dictionary = JSON.parse_string(JSON.stringify(g.snapshot(-1)))
|
||||||
|
var g2 := Game.from_snapshot(d)
|
||||||
|
var same := g2.players.size() == g.players.size()
|
||||||
|
for i in g.players.size():
|
||||||
|
var a: Game.Player = g.players[i]
|
||||||
|
var b: Game.Player = g2.players[i]
|
||||||
|
var ids := func(cs: Array): return cs.map(func(c): return c.id)
|
||||||
|
same = same and ids.call(a.hand) == ids.call(b.hand) and ids.call(a.deck) == ids.call(b.deck) \
|
||||||
|
and a.life == b.life and a.board.size() == b.board.size() and a.is_human == b.is_human
|
||||||
|
check(same and g2.current_idx == g.current_idx and g2.turn == g.turn and g2.human.is_human, "voller Spielstand: Hände, Decks und Bretter erhalten")
|
||||||
|
var guard := 0
|
||||||
|
while g2.winner == "" and guard < 300:
|
||||||
|
AI.take_turn(g2, g2.current())
|
||||||
|
guard += 1
|
||||||
|
check(g2.winner != "", "fortgesetzte Partie ist bis zum Ende spielbar")
|
||||||
|
var s2: Dictionary = g.snapshot(-2)
|
||||||
|
check(s2.players.all(func(p): return not (p.hand is Array) and not (p.traps is Array) and not (p.deck is Array)), "Zuschauer-Sicht ohne Hände, Decks und Fallen")
|
||||||
|
|
||||||
|
|
||||||
## Jeder tr("...")-Text in der Oberfläche und jeder Anleitungsschritt hat einen englischen Eintrag.
|
## Jeder tr("...")-Text in der Oberfläche und jeder Anleitungsschritt hat einen englischen Eintrag.
|
||||||
func test_uebersetzung_vollstaendig() -> void:
|
func test_uebersetzung_vollstaendig() -> void:
|
||||||
var keys := {}
|
var keys := {}
|
||||||
@@ -1376,6 +1451,9 @@ func test_uebersetzung_vollstaendig() -> void:
|
|||||||
key = line.substr(0, line.find(","))
|
key = line.substr(0, line.find(","))
|
||||||
keys[key] = true
|
keys[key] = true
|
||||||
var missing := []
|
var missing := []
|
||||||
|
for k in Cards.COMBOS.values() + Cards.ELEMENTS + Cards.KEYWORDS + Cards.RARITIES + ["Wesen", "Zauber", "Falle"] + Cards.TRIGGER_TEXT.values():
|
||||||
|
if not keys.has(k):
|
||||||
|
missing.append(k)
|
||||||
var d := DirAccess.open("res://scripts/ui")
|
var d := DirAccess.open("res://scripts/ui")
|
||||||
d.list_dir_begin()
|
d.list_dir_begin()
|
||||||
var name := d.get_next()
|
var name := d.get_next()
|
||||||
@@ -1389,6 +1467,11 @@ func test_uebersetzung_vollstaendig() -> void:
|
|||||||
if not keys.has(k) and not (k in missing):
|
if not keys.has(k) and not (k in missing):
|
||||||
missing.append(k)
|
missing.append(k)
|
||||||
name = d.get_next()
|
name = d.get_next()
|
||||||
|
var rx_cards := RegEx.new()
|
||||||
|
rx_cards.compile("tr\\(\"([^\"]+)\"\\)")
|
||||||
|
for m in rx_cards.search_all(FileAccess.get_file_as_string("res://scripts/cards.gd")):
|
||||||
|
if not keys.has(m.get_string(1)) and not (m.get_string(1) in missing):
|
||||||
|
missing.append(m.get_string(1))
|
||||||
for stepset in [BattleTutorial.STEPS1, BattleTutorial.STEPS2]:
|
for stepset in [BattleTutorial.STEPS1, BattleTutorial.STEPS2]:
|
||||||
for st in stepset:
|
for st in stepset:
|
||||||
if not keys.has(st[0]):
|
if not keys.has(st[0]):
|
||||||
|
|||||||
216
tests/ui_test.gd
216
tests/ui_test.gd
@@ -157,16 +157,22 @@ func _ready() -> void:
|
|||||||
for suffix in ["", ".bak", ".corrupt"]:
|
for suffix in ["", ".bak", ".corrupt"]:
|
||||||
if FileAccess.file_exists(SAVE + suffix):
|
if FileAccess.file_exists(SAVE + suffix):
|
||||||
DirAccess.remove_absolute(abs + suffix)
|
DirAccess.remove_absolute(abs + suffix)
|
||||||
|
if FileAccess.file_exists(SAVE.get_basename() + "_kampf.json"):
|
||||||
|
DirAccess.remove_absolute(abs.get_basename() + "_kampf.json")
|
||||||
main = load("res://scenes/main.tscn").instantiate()
|
main = load("res://scenes/main.tscn").instantiate()
|
||||||
main.save_path = SAVE
|
main.save_path = SAVE
|
||||||
add_child(main)
|
add_child(main)
|
||||||
await frames(3)
|
await frames(3)
|
||||||
var fast := OS.get_environment("DINGER_FAST") != ""
|
var fast := OS.get_environment("DINGER_FAST") != ""
|
||||||
var suite := [test_menu_und_regeln, test_gamepad_menue, test_booster, test_deckbau, test_gegnerauswahl, test_einstellungen,
|
var suite := [test_menu_und_regeln, test_gamepad_menue, test_booster, test_deckbau, test_gegnerauswahl, test_einstellungen,
|
||||||
test_erfolge, test_kampagne, test_werkstatt, test_sprache, test_anleitung, test_anleitung2, test_herausforderung]
|
test_erfolge, test_kampagne, test_werkstatt, test_sprache, test_anleitung, test_anleitung2, test_herausforderung,
|
||||||
|
test_kampf_fortsetzen, test_gamepad_deckbau, test_deckcode, test_elementbooster]
|
||||||
if not fast:
|
if not fast:
|
||||||
suite += [test_hotseat, test_netzwerk, test_kampf.bind(2, 3), test_kampf_abbrechen]
|
suite += [test_hotseat, test_netzwerk, test_host_wechsel, test_zuschauer, test_kampf.bind(2, 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_host_wechsel,test_zuschauer
|
||||||
|
if only != "":
|
||||||
|
suite = suite.filter(func(t: Callable): return t.get_method() in only.split(","))
|
||||||
for t in suite:
|
for t in suite:
|
||||||
await ensure_menu()
|
await ensure_menu()
|
||||||
await t.call()
|
await t.call()
|
||||||
@@ -443,6 +449,97 @@ func give_up() -> void:
|
|||||||
check(screen() is MenuScreen, "Aufgeben führt ins Menü")
|
check(screen() is MenuScreen, "Aufgeben führt ins Menü")
|
||||||
|
|
||||||
|
|
||||||
|
## Gamepad im Deckbau: Karten und Deckzeilen sind fokussierbar, A wirkt wie ein Klick, B geht zurück.
|
||||||
|
func test_gamepad_deckbau() -> void:
|
||||||
|
await click(find_button("Deck bauen"))
|
||||||
|
var scr = screen()
|
||||||
|
var before: int = scr.deck.size()
|
||||||
|
await pad(JOY_BUTTON_DPAD_DOWN)
|
||||||
|
check(get_viewport().gui_get_focus_owner() != null and main.pad_mode, "Gamepad setzt Fokus im Deckbau")
|
||||||
|
var row: Control = scr.deck_list.get_child(0)
|
||||||
|
row.grab_focus()
|
||||||
|
await frames(1)
|
||||||
|
check(row.has_focus() and scr.preview.visible, "Deckzeile fokussierbar, Vorschau folgt")
|
||||||
|
await pad(JOY_BUTTON_A)
|
||||||
|
check(scr.deck.size() == before - 1, "A entfernt die fokussierte Karte aus dem Deck")
|
||||||
|
var elems: Array = Game.deck_elements(scr.deck_cards())
|
||||||
|
var target: CardView = null
|
||||||
|
for v in scr.grid.views():
|
||||||
|
if not v.dim and v.card.elements.all(func(e): return e in elems):
|
||||||
|
target = v
|
||||||
|
break
|
||||||
|
target.grab_focus()
|
||||||
|
await frames(1)
|
||||||
|
check(target.has_focus() and scr.preview.visible and scr.preview.card == target.card, "Karte im Raster fokussierbar mit Vorschau")
|
||||||
|
var target_id: String = target.card.id
|
||||||
|
await pad(JOY_BUTTON_A) # das Raster wird danach neu aufgebaut, target ist dann freigegeben
|
||||||
|
check(scr.deck.size() == before and target_id in scr.deck, "A nimmt die fokussierte Karte ins Deck (%d Karten, Meldung '%s')" % [scr.deck.size(), scr.msg_label.text])
|
||||||
|
await shot("31_gamepad_deckbau")
|
||||||
|
await pad(JOY_BUTTON_B)
|
||||||
|
check(scr.confirm.visible, "B fragt bei ungespeicherten Änderungen nach")
|
||||||
|
await click(find_button("Verwerfen"))
|
||||||
|
check(screen() is MenuScreen, "Verwerfen führt ins Menü")
|
||||||
|
await hover(main)
|
||||||
|
|
||||||
|
|
||||||
|
## Deck-Code kopieren, einfügen und übernehmen.
|
||||||
|
func test_deckcode() -> void:
|
||||||
|
await click(find_button("Deck bauen"))
|
||||||
|
var scr = screen()
|
||||||
|
await click(find_button("Code"))
|
||||||
|
check(scr.code_dialog.visible and scr.code_box.text.begins_with(Profile.CODE_PREFIX), "Code-Dialog zeigt den Deck-Code")
|
||||||
|
await click(find_button("In Zwischenablage"))
|
||||||
|
check(DisplayServer.clipboard_get() == scr.code_box.text, "Code in der Zwischenablage")
|
||||||
|
await shot("32_deckcode")
|
||||||
|
scr.code_box.text = "DINGER1:f_funke*2,e_wall*1,x_nix*1"
|
||||||
|
await click(find_button("Übernehmen"))
|
||||||
|
check(scr.deck.size() == Game.DECK_SIZE and "Ungültiger Code" in scr.msg_label.text, "ungültiger Code wird abgelehnt")
|
||||||
|
scr.code_box.text = "DINGER1:f_funke*2,e_wall*1,fff_sonne*1"
|
||||||
|
await click(find_button("Übernehmen"))
|
||||||
|
check(scr.deck == ["f_funke", "f_funke", "e_wall"] and "Kern der Sonne" in scr.msg_label.text and not scr.code_dialog.visible, "Code übernommen, fehlende Karte gemeldet")
|
||||||
|
await click(find_button("Zurück"))
|
||||||
|
await click(find_button("Verwerfen"))
|
||||||
|
check(screen() is MenuScreen, "Deckbau verlassen")
|
||||||
|
|
||||||
|
|
||||||
|
## Elementbooster liefert nur Karten des Elements.
|
||||||
|
func test_elementbooster() -> void:
|
||||||
|
main.profile.coins = 300
|
||||||
|
await click(find_button("Booster öffnen"))
|
||||||
|
var scr = screen()
|
||||||
|
await click(find_button("Feuer"))
|
||||||
|
check(scr.cards.size() == 5 and scr.cards.all(func(c): return "Feuer" in c.elements) and main.profile.coins == 300 - Profile.ELEMENT_BOOSTER_COST, "Feuer-Booster: fünf Feuerkarten")
|
||||||
|
await click(find_button("Alle aufdecken"))
|
||||||
|
await shot("33_elementbooster")
|
||||||
|
await click(find_button("Zurück"))
|
||||||
|
|
||||||
|
|
||||||
|
## Kampf speichern, im Menü fortsetzen, Aufgeben löscht die Datei.
|
||||||
|
func test_kampf_fortsetzen() -> void:
|
||||||
|
await start_battle(1)
|
||||||
|
var scr = screen()
|
||||||
|
scr.ai_delay = 0.0
|
||||||
|
await wait_for_human(scr)
|
||||||
|
var kampf: String = main.battle_save_path()
|
||||||
|
check(FileAccess.file_exists(kampf), "Kampf wird laufend gespeichert")
|
||||||
|
await click(scr.b_end)
|
||||||
|
await wait_for_human(scr)
|
||||||
|
var turn: int = scr.game.turn
|
||||||
|
var life: int = scr.game.human.life
|
||||||
|
var hand: int = scr.game.human.hand.size()
|
||||||
|
await click(find_button("Aufgeben"))
|
||||||
|
await click(find_button("Speichern und zurück"))
|
||||||
|
check(screen() is MenuScreen and find_button("Kampf fortsetzen") != null, "Speichern führt ins Menü mit Fortsetzen-Knopf")
|
||||||
|
await click(find_button("Kampf fortsetzen"))
|
||||||
|
var scr2 = screen()
|
||||||
|
check(scr2 is BattleScreen and scr2.game.turn == turn and scr2.game.human.life == life and scr2.game.human.hand.size() == hand
|
||||||
|
and scr2.game.current() == scr2.game.human, "Kampf fortgesetzt mit gleichem Zug, Leben und Hand")
|
||||||
|
scr2.ai_delay = 0.0
|
||||||
|
await shot("30_kampf_fortgesetzt")
|
||||||
|
await give_up()
|
||||||
|
check(not FileAccess.file_exists(kampf) and find_button("Kampf fortsetzen") == null, "Aufgeben löscht den gespeicherten Kampf")
|
||||||
|
|
||||||
|
|
||||||
func test_einstellungen() -> void:
|
func test_einstellungen() -> void:
|
||||||
await click(find_button("Einstellungen"))
|
await click(find_button("Einstellungen"))
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
@@ -684,7 +781,7 @@ func test_anleitung() -> void:
|
|||||||
await shot("22_anleitung_mulligan")
|
await shot("22_anleitung_mulligan")
|
||||||
await click(scr.b_swap)
|
await click(scr.b_swap)
|
||||||
check(not scr.mulligan_box.visible and g.human.mulligan_done, "getauscht")
|
check(not scr.mulligan_box.visible and g.human.mulligan_done, "getauscht")
|
||||||
check(g.log.any(func(l): return "tauscht 1 Karten" in l), "Tausch im Verlauf")
|
check(g.log.any(func(l): return "tauscht 1 Karte" in l), "Tausch im Verlauf")
|
||||||
check(scr.tutorial_step == 0 and "Energie" in scr.tutorial_text.text, "Schritt 1 sichtbar")
|
check(scr.tutorial_step == 0 and "Energie" in scr.tutorial_text.text, "Schritt 1 sichtbar")
|
||||||
var idx := -1
|
var idx := -1
|
||||||
for i in g.human.hand.size():
|
for i in g.human.hand.size():
|
||||||
@@ -738,7 +835,7 @@ func test_anleitung() -> void:
|
|||||||
check(scr.tutorial_step >= 2 or idx < 0, "Schritt 3 im zweiten Zug")
|
check(scr.tutorial_step >= 2 or idx < 0, "Schritt 3 im zweiten Zug")
|
||||||
var step_before: int = scr.tutorial_step
|
var step_before: int = scr.tutorial_step
|
||||||
await click(scr.b_tut_next)
|
await click(scr.b_tut_next)
|
||||||
check(scr.tutorial_step == step_before + 1, "Weiter-Knopf")
|
check(scr.tutorial_step >= step_before + 1, "Weiter-Knopf (Schritt %d)" % step_before)
|
||||||
await key(KEY_ESCAPE)
|
await key(KEY_ESCAPE)
|
||||||
check(scr.confirm.visible, "Esc öffnet Aufgeben")
|
check(scr.confirm.visible, "Esc öffnet Aufgeben")
|
||||||
await key(KEY_ESCAPE)
|
await key(KEY_ESCAPE)
|
||||||
@@ -834,6 +931,117 @@ func test_netzwerk() -> void:
|
|||||||
check(screen() is MenuScreen and not main.net.connected, "Netzwerk getrennt, zurück im Menü")
|
check(screen() is MenuScreen and not main.net.connected, "Netzwerk getrennt, zurück im Menü")
|
||||||
|
|
||||||
|
|
||||||
|
func _bot_args(extra: Array) -> Array:
|
||||||
|
return ["--headless", "--path", ProjectSettings.globalize_path("res://"), "tests/net_bot.tscn", "--"] + extra
|
||||||
|
|
||||||
|
|
||||||
|
func _bot_quit(on: bool) -> void:
|
||||||
|
var path := "user://bot_quit"
|
||||||
|
if on:
|
||||||
|
var f := FileAccess.open(path, FileAccess.WRITE)
|
||||||
|
f.store_string("x")
|
||||||
|
f.close()
|
||||||
|
elif FileAccess.file_exists(path):
|
||||||
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
||||||
|
|
||||||
|
|
||||||
|
## Verbindet sich mit dem Bot-Host (mehrere Versuche, bis er lauscht).
|
||||||
|
func _join_bot_host(watch: bool) -> void:
|
||||||
|
await click(find_button("Netzwerkspiel"))
|
||||||
|
var lobby = screen()
|
||||||
|
lobby.ip_box.text = "127.0.0.1"
|
||||||
|
for attempt in 8:
|
||||||
|
await click(find_button("Zuschauen" if watch else "Beitreten"))
|
||||||
|
var w := 0
|
||||||
|
while not main.net.connected and w < 180:
|
||||||
|
await frames(1)
|
||||||
|
w += 1
|
||||||
|
if main.net.connected:
|
||||||
|
return
|
||||||
|
main.net.leave()
|
||||||
|
await frames(30)
|
||||||
|
|
||||||
|
|
||||||
|
## Der Host (ein Bot) verschwindet: der einzige Mitspieler übernimmt und spielt zu Ende.
|
||||||
|
func test_host_wechsel() -> void:
|
||||||
|
_bot_quit(false)
|
||||||
|
var pid := OS.create_process(OS.get_executable_path(), _bot_args(["--host", "--name=BotHost"]))
|
||||||
|
check(pid > 0, "Bot-Host gestartet")
|
||||||
|
await frames(30)
|
||||||
|
await _join_bot_host(false)
|
||||||
|
var w := 0
|
||||||
|
while not (screen() is BattleScreen) and w < 900:
|
||||||
|
await frames(1)
|
||||||
|
w += 1
|
||||||
|
var b = screen()
|
||||||
|
check(b is BattleScreen and b.is_client(), "als Mitspieler beim Bot-Host im Kampf")
|
||||||
|
w = 0
|
||||||
|
while (b.game == null or main.net.backup.is_empty()) and w < 600:
|
||||||
|
await frames(1)
|
||||||
|
w += 1
|
||||||
|
check(b.game != null and main.net.i_am_successor and not main.net.backup.is_empty(), "Nachfolger kennt den vollen Spielstand")
|
||||||
|
main.net.rejoin_seconds = 3.0
|
||||||
|
_bot_quit(true)
|
||||||
|
w = 0
|
||||||
|
while not main.net.is_host and w < 900:
|
||||||
|
await frames(1)
|
||||||
|
w += 1
|
||||||
|
check(main.net.is_host and b.game == main.net.game and screen() == b and b.game.log.any(func(l): return "übernimmt als Host" in l),
|
||||||
|
"Host-Wechsel: Mitspieler führt das Spiel weiter")
|
||||||
|
check(main.net.waiting.has("BotHost"), "Platz des alten Hosts bleibt zunächst frei")
|
||||||
|
await shot("34_host_wechsel")
|
||||||
|
# Der alte Host kehrt nicht zurück: ein Zug wird übersprungen, nach der Frist scheidet er aus
|
||||||
|
b.ai_delay = 0.0
|
||||||
|
await clear_mulligan(b)
|
||||||
|
if b.game.current() == b.perspective:
|
||||||
|
await click(b.b_end)
|
||||||
|
w = 0
|
||||||
|
while b.game.winner == "" and w < 900:
|
||||||
|
await frames(1)
|
||||||
|
w += 1
|
||||||
|
check(b.game.log.any(func(l): return "übersprungen" in l), "Zug des fehlenden Hosts übersprungen")
|
||||||
|
check(b.game.winner != "" and b.game.winner_player == b.perspective and b.game.log.any(func(l): return "kehrt nicht zurück" in l),
|
||||||
|
"nach der Frist scheidet der alte Host aus, Partie gewonnen (Sieger '%s', Zug %d)" % [b.game.winner, b.game.turn])
|
||||||
|
await leave_battle()
|
||||||
|
_bot_quit(false)
|
||||||
|
check(screen() is MenuScreen and not main.net.connected, "nach dem Host-Wechsel zurück im Menü")
|
||||||
|
|
||||||
|
|
||||||
|
## Zuschauer: sieht die Partie zweier Bots ohne Hände und ohne eigene Züge.
|
||||||
|
func test_zuschauer() -> void:
|
||||||
|
_bot_quit(false)
|
||||||
|
var pid_h := OS.create_process(OS.get_executable_path(), _bot_args(["--host", "--name=BotHost"]))
|
||||||
|
await frames(30)
|
||||||
|
var pid_c := OS.create_process(OS.get_executable_path(), _bot_args(["--ip=127.0.0.1", "--name=Bot"]))
|
||||||
|
check(pid_h > 0 and pid_c > 0, "zwei Bots gestartet")
|
||||||
|
await _join_bot_host(true)
|
||||||
|
var w := 0
|
||||||
|
while not (screen() is BattleScreen) and w < 1200:
|
||||||
|
await frames(1)
|
||||||
|
w += 1
|
||||||
|
var b = screen()
|
||||||
|
check(b is BattleScreen and b.spectating(), "als Zuschauer im Kampf")
|
||||||
|
w = 0
|
||||||
|
while (b.game == null or b.game.turn < 2) and w < 900:
|
||||||
|
await frames(1)
|
||||||
|
w += 1
|
||||||
|
check(b.game != null and b.game.turn >= 2 and b.hand_views.is_empty() and not b.mulligan_box.visible and not b.b_end.visible,
|
||||||
|
"Zuschauer sieht die Partie ohne Hände und ohne Züge (Zug %d)" % (b.game.turn if b.game != null else 0))
|
||||||
|
await shot("35_zuschauer")
|
||||||
|
check(not b.handover.visible and not b.is_hotseat(), "Zuschauer bekommt keine Hotseat-Übergabe")
|
||||||
|
if screen() is BattleScreen: # die Bots spielen schnell, die Partie kann schon vorbei sein
|
||||||
|
await leave_battle()
|
||||||
|
else:
|
||||||
|
var w2 := 0
|
||||||
|
while not (screen() is MenuScreen) and w2 < 300:
|
||||||
|
await frames(1)
|
||||||
|
w2 += 1
|
||||||
|
check(screen() is MenuScreen and not main.net.connected, "Zuschauer zurück im Menü")
|
||||||
|
_bot_quit(true)
|
||||||
|
await frames(60)
|
||||||
|
_bot_quit(false)
|
||||||
|
|
||||||
|
|
||||||
func test_kampagne() -> void:
|
func test_kampagne() -> void:
|
||||||
await click(find_button("Kampagne"))
|
await click(find_button("Kampagne"))
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
|
|||||||
@@ -135,3 +135,85 @@ Duell,Duel
|
|||||||
Stärke %.1f · %d Münzen,Strength %.1f · %d coins
|
Stärke %.1f · %d Münzen,Strength %.1f · %d coins
|
||||||
Abbrechen,Cancel
|
Abbrechen,Cancel
|
||||||
"Ja, zurücksetzen","Yes, reset"
|
"Ja, zurücksetzen","Yes, reset"
|
||||||
|
"Speichern und zurück","Save and go back"
|
||||||
|
"Kampf fortsetzen","Resume battle"
|
||||||
|
Aufgeben beendet den Kampf ohne Münzen.,Surrendering ends the battle without coins.
|
||||||
|
Speichern hebt ihn für später auf.,Saving keeps it for later.
|
||||||
|
%s Schaden an den Angreifer,%s damage to the attacker
|
||||||
|
%s Schaden an dieses Wesen,%s damage to that creature
|
||||||
|
Der Angreifer wird eingefroren,The attacker is frozen
|
||||||
|
Es wird eingefroren,It is frozen
|
||||||
|
Es geht zurück auf die Hand,It returns to its owner's hand
|
||||||
|
Der Angriff verpufft,The attack fizzles
|
||||||
|
Beschwöre einen %s,Summon a %s
|
||||||
|
%s Schaden an ein gegnerisches Wesen,%s damage to an enemy creature
|
||||||
|
%s Schaden an ein Ziel,%s damage to a target
|
||||||
|
%s Schaden an alle gegnerischen Wesen,%s damage to all enemy creatures
|
||||||
|
%s Schaden an alle Gegner,%s damage to all opponents
|
||||||
|
%s Schaden an alle Gegner und ihre Wesen,%s damage to all opponents and their creatures
|
||||||
|
Heile dich um %s,Heal yourself for %s
|
||||||
|
Ziehe 1 Karte,Draw 1 card
|
||||||
|
Ziehe %s Karten,Draw %s cards
|
||||||
|
Friere ein gegnerisches Wesen ein,Freeze an enemy creature
|
||||||
|
Friere das stärkste gegnerische Wesen ein,Freeze the strongest enemy creature
|
||||||
|
Friere alle gegnerischen Wesen ein,Freeze all enemy creatures
|
||||||
|
Ein gegnerisches Wesen zurück auf die Hand,Return an enemy creature to its owner's hand
|
||||||
|
Alle gegnerischen Wesen zurück auf die Hand,Return all enemy creatures to their owners' hands
|
||||||
|
Ein befreundetes Wesen erhält %s,A friendly creature gets %s
|
||||||
|
Alle befreundeten Wesen erhalten %s,All friendly creatures get %s
|
||||||
|
Ein befreundetes Wesen erhält Schild,A friendly creature gets Shield
|
||||||
|
Erhält Schild,Gets Shield
|
||||||
|
Alle befreundeten Wesen erhalten Schild,All friendly creatures get Shield
|
||||||
|
Falle. %s: %s,Trap. %s: %s
|
||||||
|
Beim Ausspielen: %s,On play: %s
|
||||||
|
Wenn ein gegnerisches Wesen dich oder eines deiner Wesen angreift,When an enemy creature attacks you or one of your creatures
|
||||||
|
Wenn ein Gegner ein Wesen ausspielt,When an opponent plays a creature
|
||||||
|
Wächter,Guardian
|
||||||
|
Flink,Swift
|
||||||
|
Schild,Shield
|
||||||
|
Doppelschlag,Double Strike
|
||||||
|
Wesen,Creature
|
||||||
|
Zauber,Spell
|
||||||
|
Falle,Trap
|
||||||
|
gewöhnlich,common
|
||||||
|
selten,rare
|
||||||
|
episch,epic
|
||||||
|
legendär,legendary
|
||||||
|
mythisch,mythic
|
||||||
|
Feuer,Fire
|
||||||
|
Wasser,Water
|
||||||
|
Erde,Earth
|
||||||
|
Luft,Air
|
||||||
|
Inferno,Inferno
|
||||||
|
Tiefsee,Deep Sea
|
||||||
|
Berg,Mountain
|
||||||
|
Orkan,Hurricane
|
||||||
|
Dampf,Steam
|
||||||
|
Lava,Lava
|
||||||
|
Blitz,Lightning
|
||||||
|
Schlamm,Mud
|
||||||
|
Sturm,Storm
|
||||||
|
Sand,Sand
|
||||||
|
Sonne,Sun
|
||||||
|
Ozean,Ocean
|
||||||
|
Gebirge,Mountains
|
||||||
|
Himmel,Sky
|
||||||
|
"Code übernommen, es fehlen: ","Code applied, missing: "
|
||||||
|
Elemente: ,Elements:
|
||||||
|
%s zerlegt (+%d Staub),%s dismantled (+%d dust)
|
||||||
|
%s hergestellt,%s crafted
|
||||||
|
%s ist jetzt im Deck %s,%s is now in deck %s
|
||||||
|
Deck-Code,Deck code
|
||||||
|
In Zwischenablage,To clipboard
|
||||||
|
Aus Zwischenablage,From clipboard
|
||||||
|
Code,Code
|
||||||
|
Ungültiger Code,Invalid code
|
||||||
|
Code übernommen,Code applied
|
||||||
|
Zum Teilen kopieren oder einen Code einfügen. Nicht vorhandene Karten werden weggelassen.,Copy to share or paste a code. Cards you do not own are skipped.
|
||||||
|
Elementbooster (%d Münzen): nur Karten des Elements und seiner Kombos,Element booster (%d coins): only cards of that element and its combos
|
||||||
|
"Kapitel 3: drei Merkmale, volle Tische und die stärksten Decks.","Chapter 3: three perks, full tables and the strongest decks."
|
||||||
|
Alle drei Kapitel abgeschlossen. Der Archimagier ist endgültig besiegt!,All three chapters completed. The Archmage is defeated for good!
|
||||||
|
Kapitel %d,Chapter %d
|
||||||
|
Zuschauen,Watch
|
||||||
|
Zuschauer,Spectator
|
||||||
|
"Zuschauer: Hände sind verdeckt","Spectator: hands are hidden"
|
||||||
|
|||||||
|
Binary file not shown.
Reference in New Issue
Block a user