Gegnerauswahl und Kampagne aufgeräumt, Hotseat entfernt

- Einzelspieler: Schwierigkeit, darunter nur die zwei Deckstärken, Gegnerzahl als
  Schieberegler, Starten und Zurück; kein Würfeln, die Gegnerdecks entstehen beim Start
- Kampagne: nur noch Name, Elementpunkte und Knopf, mit Innenabstand in den Zeilen
- Hotseat (Lokal zu mehreren) samt Übergabe-Vorhang, Auswahlbildschirm und Tests entfernt
- Netzwerktests laufen nur mit DINGER_NETZ=1, weil die Bot-Prozesse Minuten brauchen

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
Team3
2026-09-17 21:05:09 +02:00
parent 5c1084fd79
commit 0f2dc22ef6
16 changed files with 95 additions and 381 deletions

View File

@@ -54,7 +54,7 @@ jobs:
sudo apt-get update -qq && sudo apt-get install -y -qq xvfb libgl1 libegl1 sudo apt-get update -qq && sudo apt-get install -y -qq xvfb libgl1 libegl1
- name: Import - name: Import
run: ./godot --headless --path . --import || true run: ./godot --headless --path . --import || true
- name: Fenster-Test (voll, mit Netzwerk und Hotseat) - name: Fenster-Test (voll)
run: xvfb-run -a --server-args="-screen 0 1280x800x24" ./godot --path . tests/ui_test.tscn 2>&1 | tee fenster.log | grep -E "FEHLER|Prüfungen" || true run: xvfb-run -a --server-args="-screen 0 1280x800x24" ./godot --path . tests/ui_test.tscn 2>&1 | tee fenster.log | grep -E "FEHLER|Prüfungen" || true
- name: Ergebnis - name: Ergebnis
run: grep -q " 0 fehlgeschlagen" fenster.log run: grep -q " 0 fehlgeschlagen" fenster.log

View File

@@ -29,7 +29,6 @@ Der Spielstand liegt unter `user://spielstand.json`
- Resonanz: gleiche Elemente auf dem Feld verstärken sich. Feldkombos: verschiedene Elemente lösen beim Ausspielen Effekte aus - Resonanz: gleiche Elemente auf dem Feld verstärken sich. Feldkombos: verschiedene Elemente lösen beim Ausspielen Effekte aus
- Fallen liegen verdeckt und lösen im gegnerischen Zug aus (die KI kennt fremde Fallen nicht) - Fallen liegen verdeckt und lösen im gegnerischen Zug aus (die KI kennt fremde Fallen nicht)
- Jeder gegen jeden mit ausgeloster Zugreihenfolge, 1 bis 5 KI-Gegner (KI mit Vorausschau), Kampagne mit 10 Magiern, Erfolge, Tagesaufgabe - Jeder gegen jeden mit ausgeloster Zugreihenfolge, 1 bis 5 KI-Gegner (KI mit Vorausschau), Kampagne mit 10 Magiern, Erfolge, Tagesaufgabe
- Lokal zu mehreren: bis zu 4 Menschen am selben Rechner plus KI
- Booster, fünf Deck-Slots, Werkstatt (Zerlegen und Herstellen aus Staub), Einstellungen (Ton, KI-Tempo, Animationen) - Booster, fünf Deck-Slots, Werkstatt (Zerlegen und Herstellen aus Staub), Einstellungen (Ton, KI-Tempo, Animationen)
- Spielstand versioniert mit Sicherungskopie (`spielstand.json.bak`) - Spielstand versioniert mit Sicherungskopie (`spielstand.json.bak`)
- Anleitung (geführte erste Partie), Mulligan im ersten Zug, Tastatur (Leertaste, 1 bis 9, Esc, Enter) - Anleitung (geführte erste Partie), Mulligan im ersten Zug, Tastatur (Leertaste, 1 bis 9, Esc, Enter)
@@ -66,7 +65,7 @@ Der Spielstand liegt unter `user://spielstand.json`
`tests/run_headless.sh` führt Import, statische Prüfung (`tests/lint.gd`) und Headless-Prüfungen aus. `.github/workflows/tests.yml`: `tests/run_headless.sh` führt Import, statische Prüfung (`tests/lint.gd`) und Headless-Prüfungen aus. `.github/workflows/tests.yml`:
Headless und schneller Fenster-Test bei jedem Push, voller Fenster-Test und Archetypen-Messung nachts um 3 Uhr oder von Hand (Xvfb, lokal nicht geprüft). Headless und schneller Fenster-Test bei jedem Push, voller Fenster-Test und Archetypen-Messung nachts um 3 Uhr oder von Hand (Xvfb, lokal nicht geprüft).
`DINGER_FAST=1` lässt den Fenster-Test ohne Hotseat, Netzwerk und lange Kämpfe laufen (etwa ein Drittel der Zeit). `DINGER_FAST=1` lässt den Fenster-Test ohne die langen Kämpfe laufen (etwa ein Drittel der Zeit).
Statistiken aus Spieltests lassen sich unter Erfolge als CSV exportieren. Statistiken aus Spieltests lassen sich unter Erfolge als CSV exportieren.
``` ```
@@ -76,7 +75,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 DINGER_ONLY=test_deckbau godot --path . tests/ui_test.tscn # nur einzelne Oberflächentests
DINGER_NETZ=1 godot --path . tests/ui_test.tscn # zusätzlich die langsamen Netzwerktests (Bot-Prozesse)
N=200 godot --headless --path . -s tests/strength_probe.gd # Messlauf mit 200 statt 60 Partien je Archetyp 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
@@ -94,13 +94,13 @@ godot --headless --path . --export-release Windows build/dinger-windows.exe
## Aufbau ## Aufbau
- `scripts/cards.gd` – Elemente, Kombos, Kartenklassen; Pool aus `data/karten.json` - `scripts/cards.gd` – Elemente, Kombos, Kartenklassen; Pool aus `data/karten.json`
- `scripts/game.gd` – Spielregeln, Züge, Kampf, Fallen, Feldkombos, Resonanz, Deckregeln, Hotseat - `scripts/game.gd` – Spielregeln, Züge, Kampf, Fallen, Feldkombos, Resonanz, Deckregeln
- `scripts/deckgen.gd` – Deckstärke, Gegnerdecks entlang der Energiekurve, Archetypen - `scripts/deckgen.gd` – Deckstärke, Gegnerdecks entlang der Energiekurve, Archetypen
- `scripts/netstate.gd` – Zustandsbilder fürs Netzwerk - `scripts/netstate.gd` – Zustandsbilder fürs Netzwerk
- `scripts/ai.gd` – KI mit Vorausschau (plus gierige Vergleichs-KI) - `scripts/ai.gd` – KI mit Vorausschau (plus gierige Vergleichs-KI)
- `scripts/profile.gd` – Spielstand (versioniert), Booster, Erfolge, Tagesaufgabe, Einstellungen - `scripts/profile.gd` – Spielstand (versioniert), Booster, Erfolge, Tagesaufgabe, Einstellungen
- `scripts/campaign.gd` – Kampagnenstufen - `scripts/campaign.gd` – Kampagnenstufen
- `scripts/ui/` – Oberfläche: Menü, Gegnerauswahl, Kampagne, Hotseat, Lobby, Booster, Deckbau, Werkstatt, Kampf (Bildschirm, Layout, Effekte, Anleitung, Mulligan, Gamepad), Erfolge, Einstellungen, Regeln, Ton - `scripts/ui/` – Oberfläche: Menü, Gegnerauswahl, Kampagne, Lobby, Booster, Deckbau, Werkstatt, Kampf (Bildschirm, Layout, Effekte, Anleitung, Mulligan, Gamepad), Erfolge, Einstellungen, Regeln, Ton
- `scenes/main.tscn` – Startszene - `scenes/main.tscn` – Startszene
- `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden - `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden
- `scripts/challenge.gd` – wöchentliche Herausforderungen - `scripts/challenge.gd` – wöchentliche Herausforderungen

View File

@@ -6,7 +6,7 @@
- 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, auch als Zuschauer - Im lokalen Netz zu mehreren spielbar, auch als Zuschauer
- Ein unterbrochener Kampf lässt sich später fortsetzen - 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

View File

@@ -156,7 +156,7 @@ class Player extends RefCounted:
var human: Player # der (erste) menschliche Spieler var human: Player # der (erste) menschliche Spieler
var humans: Array = [] # alle menschlichen Spieler (Hotseat: mehrere) var humans: Array = [] # alle menschlichen Spieler (im Netz mehrere)
var opponents: Array = [] # alle außer dem ersten Menschen var opponents: Array = [] # alle außer dem ersten Menschen
var players: Array = [] # in Zugreihenfolge var players: Array = [] # in Zugreihenfolge
var winner_player: Player = null var winner_player: Player = null
@@ -746,19 +746,3 @@ static func new_battle_with(deck: Array, opps: Array, r: RandomNumberGenerator =
p.max_life = 35 p.max_life = 35
g.start(fixed_order) g.start(fixed_order)
return g return g
## Hotseat: mehrere Menschen am selben Rechner, dazu KI-Gegner.
static func new_hotseat(human_decks: Array, n_ai: int, r: RandomNumberGenerator = null) -> Game:
assert(human_decks.size() >= 1 and human_decks.size() + n_ai <= MAX_OPPONENTS + 1, "höchstens 6 am Tisch")
if r == null:
r = make_rng()
var first := Player.new("Spieler 1", human_decks[0], true, START_LIFE)
var others := []
for i in range(1, human_decks.size()):
others.append(Player.new("Spieler %d" % (i + 1), human_decks[i], true, START_LIFE))
for i in n_ai:
others.append(random_opponent(r, i + 1))
var g := Game.new(first, others, r)
g.start()
return g

View File

@@ -64,9 +64,6 @@ func move(dir: int) -> void:
func confirm() -> void: func confirm() -> void:
if screen.handover.visible:
screen.take_over()
return
if screen.mulligan.visible: if screen.mulligan.visible:
screen.do_mulligan([]) screen.do_mulligan([])
return return

View File

@@ -1,14 +1,12 @@
class_name BattleScreen class_name BattleScreen
extends Control extends Control
## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx. ## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx.
## Modi: Einzelspieler, Kampagne, Anleitung, Hotseat (Perspektivwechsel), Netzwerk (Host oder Mitspieler). ## Modi: Einzelspieler, Kampagne, Anleitung, Netzwerk (Host, Mitspieler oder Zuschauer).
var app var app
var n_opp: int var n_opp: int
var stage := -1 # Kampagnenstufe oder -1 var stage := -1 # Kampagnenstufe oder -1
var tutorial := false var tutorial := false
var hotseat_decks: Array = []
var hotseat_ai := 0
var net_play := false # Netzwerkspiel (auch als Zuschauer) var net_play := false # Netzwerkspiel (auch als Zuschauer)
var online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1 var online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1
var net: Net var net: Net
@@ -41,7 +39,6 @@ var gp_index: int:
get: get:
return pad.index return pad.index
var challenge := {} # wöchentliche Herausforderung oder leer var challenge := {} # wöchentliche Herausforderung oder leer
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
@@ -83,8 +80,6 @@ var reward_label: Label
var notice_label: Label var notice_label: Label
var confirm: Control var confirm: Control
var b_save_leave: Button var b_save_leave: Button
var handover: Control
var handover_label: Label
var mulligan: BattleMulligan var mulligan: BattleMulligan
var mulligan_box: Control: var mulligan_box: Control:
get: get:
@@ -129,13 +124,6 @@ func _init(n: int = 2, campaign_stage: int = -1) -> void:
stage = campaign_stage stage = campaign_stage
static func hotseat(decks: Array, n_ai: int) -> BattleScreen:
var b := BattleScreen.new(0, -1)
b.hotseat_decks = decks
b.hotseat_ai = n_ai
return b
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.net_play = true
@@ -150,12 +138,6 @@ static func guided(part: int = 1) -> BattleScreen:
return b return b
static func against(opps: Array) -> BattleScreen:
var b := BattleScreen.new(opps.size(), -1)
b.preset_opponents = opps
return b
## Unterbrochenen Kampf aus der Datei fortsetzen (siehe save_battle). ## Unterbrochenen Kampf aus der Datei fortsetzen (siehe save_battle).
static func resume(data: Dictionary) -> BattleScreen: static func resume(data: Dictionary) -> BattleScreen:
var s := BattleScreen.new(int(data.get("n_opp", 1)), int(data.get("stage", -1))) var s := BattleScreen.new(int(data.get("n_opp", 1)), int(data.get("stage", -1)))
@@ -171,10 +153,6 @@ static func weekly(ch: Dictionary) -> BattleScreen:
return b return b
func is_hotseat() -> bool:
return game != null and game.humans.size() > 1 and not net_play
func is_online() -> bool: func is_online() -> bool:
return net_play return net_play
@@ -189,7 +167,7 @@ func is_client() -> bool:
func counts_for_profile() -> bool: func counts_for_profile() -> bool:
return not is_hotseat() and not is_online() return not is_online()
func _ready() -> void: func _ready() -> void:
@@ -213,8 +191,6 @@ func _ready() -> void:
return # der erste Zustand kommt vom Host return # der erste Zustand kommt vom Host
elif saved_game != null: elif saved_game != null:
game = saved_game game = saved_game
elif not hotseat_decks.is_empty():
game = Game.new_hotseat(hotseat_decks, hotseat_ai)
elif tutorial: elif tutorial:
guide = BattleTutorial.new(self, tutorial_part) guide = BattleTutorial.new(self, tutorial_part)
var r := Game.make_rng() var r := Game.make_rng()
@@ -229,8 +205,6 @@ func _ready() -> void:
elif stage >= 0: elif stage >= 0:
var r := Game.make_rng() var r := Game.make_rng()
game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r, app.profile.target_rating()), r, false, own_rules()) game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r, app.profile.target_rating()), r, false, own_rules())
elif not preset_opponents.is_empty():
game = Game.new_battle_with(app.profile.deck_cards(), preset_opponents, null, false, own_rules())
else: else:
game = Game.new_battle(app.profile.deck_cards(), n_opp, null, own_rules()) game = Game.new_battle(app.profile.deck_cards(), n_opp, null, own_rules())
smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile() smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile()
@@ -336,12 +310,6 @@ func _build_static() -> void:
b_save_leave = Style.button(confirm, "Speichern und zurück", Rect2(550, 420, 180, 46), save_and_leave, Color("285a3c")) 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) b_save_leave.add_theme_font_size_override("font_size", 14)
Style.button(confirm, "Weiterspielen", Rect2(745, 420, 180, 46), func(): confirm.visible = false) Style.button(confirm, "Weiterspielen", Rect2(745, 420, 180, 46), func(): confirm.visible = false)
# Übergabe zwischen Menschen (Hotseat)
handover = _overlay()
Style.panel(handover, Rect2(340, 280, 600, 220), Style.PANEL, 10, Style.GOLD, 2)
handover_label = Style.label(handover, "", 30, Style.TEXT, Rect2(340, 310, 600, 44), "center", true)
Style.label(handover, "Maus weitergeben, dann weiter. Die Hand des Vorgängers ist verdeckt.", 16, Style.MUTED, Rect2(340, 362, 600, 24), "center")
Style.button(handover, "Weiter", Rect2(540, 420, 200, 50), take_over, Color("285a3c"))
func _layer() -> Control: func _layer() -> Control:
@@ -475,7 +443,7 @@ func my_turn() -> bool:
func any_overlay() -> bool: func any_overlay() -> bool:
return confirm.visible or handover.visible or mulligan.visible or overlay.visible return confirm.visible or mulligan.visible or overlay.visible
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
@@ -506,9 +474,7 @@ func _input(event: InputEvent) -> void:
if b_undo.visible: if b_undo.visible:
undo() undo()
KEY_ENTER, KEY_KP_ENTER: KEY_ENTER, KEY_KP_ENTER:
if handover.visible: if mulligan.visible:
take_over()
elif mulligan.visible:
do_mulligan([]) do_mulligan([])
elif overlay.visible: elif overlay.visible:
leave() leave()
@@ -552,12 +518,7 @@ func _process(delta: float) -> void:
return return
var cur := game.current() var cur := game.current()
if cur.is_human: if cur.is_human:
if cur != perspective and is_hotseat() and not handover.visible: if cur == perspective and not spectating() and cur.turns == 1 and not cur.mulligan_done and not mulligan.visible:
handover_label.text = "%s ist dran" % cur.name
handover.visible = true
hand_layer.visible = false
trap_layer.visible = false
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():
@@ -623,17 +584,6 @@ func _finish() -> void:
refresh() refresh()
func take_over() -> void:
handover.visible = false
hand_layer.visible = true
trap_layer.visible = true
perspective = game.current()
cancel()
zoom_player = null
build_hero_boxes()
refresh()
func say(m: String) -> void: func say(m: String) -> void:
msg_t = 2.5 msg_t = 2.5
msg_label.text = m msg_label.text = m
@@ -1110,7 +1060,6 @@ func refresh() -> void:
reward_label.text = "Gemeinsame Partie, keine Münzen" reward_label.text = "Gemeinsame Partie, keine Münzen"
overlay.visible = true overlay.visible = true
confirm.visible = false confirm.visible = false
handover.visible = false
mulligan.visible = false mulligan.visible = false

View File

@@ -36,10 +36,6 @@ 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)
var intro := ["Zehn Magier je Kapitel. Jede Stufe bringt mehr Gegner an den Tisch. Bosse haben Merkmale (★).",
"Die Magier kehren mit zwei Merkmalen und größerem Gefolge zurück.",
"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() 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
@@ -50,31 +46,27 @@ func build() -> void:
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if not done else Style.darker(Style.PANEL, 0.8), 6, panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if not done else Style.darker(Style.PANEL, 0.8), 6,
Style.GOLD if next else null, 2 if next else 0)) Style.GOLD if next else null, 2 if next else 0))
col.add_child(panel) col.add_child(panel)
var r := Style.row(panel, 10) var pad := MarginContainer.new() # Innenabstand, damit Name und Knopf nicht am Rahmen kleben
for side in ["left", "right"]:
pad.add_theme_constant_override("margin_" + side, 14)
panel.add_child(pad)
var r := Style.row(pad, 10)
r.custom_minimum_size = Vector2(0, 50) r.custom_minimum_size = Vector2(0, 50)
Style.clabel(r, str(i + 1), 18, Style.GOLD if next else Style.MUTED, "center", true, 36) var nm := Style.clabel(r, "%s · %s" % [st.name, st.title], 16, Style.TEXT if not done else Style.MUTED, "left", true, 420)
Style.clabel(r, "%s · %s" % [st.name, st.title], 16, Style.TEXT if not done else Style.MUTED, "left", true, 290) nm.size_flags_horizontal = Control.SIZE_EXPAND_FILL # schiebt die Punkte und den Knopf nach rechts
var dots := Control.new() var dots := Control.new()
dots.custom_minimum_size = Vector2(50, 0) dots.custom_minimum_size = Vector2(60, 0)
dots.mouse_filter = Control.MOUSE_FILTER_IGNORE dots.mouse_filter = Control.MOUSE_FILTER_IGNORE
var elems: Array = st.elements var elems: Array = st.elements
dots.draw.connect(func(): dots.draw.connect(func():
for m in elems.size(): for m in elems.size():
dots.draw_circle(Vector2(10 + m * 18, dots.size.y / 2), 7, Cards.ELEMENT_COLORS[elems[m]])) dots.draw_circle(Vector2(10 + m * 18, dots.size.y / 2), 7, Cards.ELEMENT_COLORS[elems[m]]))
r.add_child(dots) r.add_child(dots)
var extra: int = mini(st.extra, Game.MAX_OPPONENTS - 1)
var table: String = "Duell" if extra == 0 else "%d am Tisch" % (extra + 2)
var perks_text: String = ", ".join(st.perks.map(func(pk): return Game.PERKS[pk]))
var target := Campaign.target_rating(i, p.target_rating())
Style.clabel(r, "%s · Stärke %.1f · %d Münzen\n★ %s" % [table, target, st.reward, perks_text], 12, Style.MUTED, "left", false, 360)
if done: if done:
Style.clabel(r, "abgeschlossen", 13, Style.GREEN, "right", false, 100)
Style.cbutton(r, "Wiederholen", Vector2(130, 36), _start.bind(i)) Style.cbutton(r, "Wiederholen", Vector2(130, 36), _start.bind(i))
elif next: elif next:
Style.clabel(r, "", 13, Style.GREEN, "right", false, 100)
Style.cbutton(r, "Kämpfen", Vector2(130, 36), _start.bind(i), Color("285a3c")) Style.cbutton(r, "Kämpfen", Vector2(130, 36), _start.bind(i), Color("285a3c"))
else: else:
Style.clabel(r, "", 13, Style.GREEN, "right", false, 100)
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():

View File

@@ -1,111 +0,0 @@
class_name HotseatScreen
extends Control
## Lokal zu mehreren am selben Rechner: Spielerzahl, KI-Zahl und Decks wählen.
var app
var n_humans := 2
var n_ai := 0
var deck_choice: Array = [] # je Mensch: Slot-Index oder -1 für Zufallsdeck
var human_buttons: Array = []
var ai_buttons: Array = []
var deck_rows: VBoxContainer
var b_start: Button
var hint: Label
func _ready() -> void:
var col := Style.column(self, 760, 40, 10)
Style.clabel(col, "Lokal zu mehreren", 36, Style.GOLD, "center", true)
Style.clabel(col, "Alle spielen am selben Rechner und reichen die Maus weiter. Jeder gegen jeden, ohne Münzen.", 16, Style.MUTED)
Style.spacer(col, 10)
Style.clabel(col, "Spieler", 20, Style.TEXT, "center", true)
var hr := Style.row(col, 10)
hr.alignment = BoxContainer.ALIGNMENT_CENTER
for n in range(2, 5):
human_buttons.append(Style.cbutton(hr, "%d Spieler" % n, Vector2(150, 44), func(): set_humans(n)))
Style.clabel(col, "KI-Gegner", 20, Style.TEXT, "center", true)
var ar := Style.row(col, 10)
ar.alignment = BoxContainer.ALIGNMENT_CENTER
for n in range(0, 5):
ai_buttons.append(Style.cbutton(ar, "%d KI" % n if n > 0 else "keine KI", Vector2(120, 44), func(): set_ai(n)))
Style.spacer(col, 6)
Style.clabel(col, "Decks", 20, Style.TEXT, "center", true)
deck_rows = VBoxContainer.new()
deck_rows.add_theme_constant_override("separation", 6)
col.add_child(deck_rows)
Style.spacer(col, 6)
hint = Style.clabel(col, "", 15, Style.ORANGE)
var br := Style.row(col, 20)
br.alignment = BoxContainer.ALIGNMENT_CENTER
b_start = Style.cbutton(br, "Start", Vector2(200, 48), start, Color("285a3c"))
Style.cbutton(br, "Zurück", Vector2(160, 48), func(): app.show_screen(MenuScreen.new()))
for i in 4:
deck_choice.append(-1)
var p: Profile = app.profile
if p.deck_valid()[0]:
deck_choice[0] = p.active_deck
refresh()
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
app.show_screen(MenuScreen.new())
func set_humans(n: int) -> void:
n_humans = n
n_ai = mini(n_ai, Game.MAX_OPPONENTS + 1 - n_humans)
refresh()
func set_ai(n: int) -> void:
n_ai = n
refresh()
func valid_slots() -> Array:
var out := []
for i in Profile.DECK_SLOTS:
if app.profile.deck_valid(i)[0]:
out.append(i)
return out
func refresh() -> void:
var p: Profile = app.profile
for i in human_buttons.size():
human_buttons[i].disabled = (i + 2) == n_humans
for i in ai_buttons.size():
ai_buttons[i].disabled = i == n_ai or n_humans + i > Game.MAX_OPPONENTS + 1
Style.clear(deck_rows)
var slots := valid_slots()
for h in n_humans:
var r := Style.row(deck_rows, 12)
r.alignment = BoxContainer.ALIGNMENT_CENTER
Style.clabel(r, "Spieler %d" % (h + 1), 16, Style.TEXT, "right", true, 140)
var ob := OptionButton.new()
ob.custom_minimum_size = Vector2(340, 36)
ob.add_item("Zufallsdeck", -1)
for slot in slots:
var elems := Game.deck_elements(p.deck_cards(slot))
ob.add_item("%s (%s)" % [p.deck_name(slot), ", ".join(elems)], slot)
ob.select(ob.get_item_index(deck_choice[h]) if deck_choice[h] >= 0 and slots.has(deck_choice[h]) else 0)
var idx := h
ob.item_selected.connect(func(i: int): deck_choice[idx] = ob.get_item_id(i))
r.add_child(ob)
hint.text = "%d am Tisch" % (n_humans + n_ai)
func start() -> void:
var p: Profile = app.profile
var r := Game.make_rng()
var decks := []
for h in n_humans:
var slot: int = deck_choice[h]
if slot >= 0 and p.deck_valid(slot)[0]:
decks.append(p.deck_cards(slot))
else:
var pool: Array = Cards.ELEMENTS.duplicate()
Game.shuffle(pool, r)
decks.append(Game.build_opponent_deck(r, [pool[0], pool[1]]))
app.show_screen(BattleScreen.hotseat(decks, n_ai))

View File

@@ -1 +0,0 @@
uid://crlne4y0uiygl

View File

@@ -4,7 +4,6 @@ extends Control
var app var app
var b_single: Button var b_single: Button
var b_campaign: Button var b_campaign: Button
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
@@ -44,7 +43,6 @@ func _ready() -> void:
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)
b_single = Style.cbutton(grid, "Einzelspieler", sz, func(): app.show_screen(OpponentSelectScreen.new()), Color("285a3c")) b_single = Style.cbutton(grid, "Einzelspieler", sz, func(): app.show_screen(OpponentSelectScreen.new()), Color("285a3c"))
b_campaign = Style.cbutton(grid, "Kampagne", sz, func(): app.show_screen(CampaignScreen.new()), Color("285a3c")) b_campaign = Style.cbutton(grid, "Kampagne", sz, func(): app.show_screen(CampaignScreen.new()), Color("285a3c"))
b_hotseat = Style.cbutton(grid, "Lokal zu mehreren", sz, func(): app.show_screen(HotseatScreen.new()), Color("285a3c"))
b_online = Style.cbutton(grid, "Netzwerkspiel", sz, func(): app.show_screen(LobbyScreen.new()), Color("285a3c")) b_online = Style.cbutton(grid, "Netzwerkspiel", sz, func(): app.show_screen(LobbyScreen.new()), Color("285a3c"))
b_challenge = Style.cbutton(grid, "Herausforderung", sz, func(): app.show_screen(ChallengeScreen.new()), Color("285a3c")) b_challenge = Style.cbutton(grid, "Herausforderung", sz, func(): app.show_screen(ChallengeScreen.new()), Color("285a3c"))
Style.cbutton(grid, "Booster öffnen", sz, func(): app.show_screen(BoosterScreen.new())) Style.cbutton(grid, "Booster öffnen", sz, func(): app.show_screen(BoosterScreen.new()))

View File

@@ -1,109 +1,79 @@
class_name OpponentSelectScreen class_name OpponentSelectScreen
extends Control extends Control
## Einzelspieler: Schwierigkeit, Gegnerzahl und Vorschau der Gegner (Element, Stärke). ## Einzelspieler: Schwierigkeit wählen und die Gegnerzahl schieben. Die Gegnerdecks
## werden erst beim Start zufällig gezogen (siehe BattleScreen und DeckGen).
var app var app
var rating_label: Label var rating_label: Label
var tip_label: Label
var diff_buttons := {} var diff_buttons := {}
var previews := {} # n -> Array von Game.Player var count_slider: HSlider
var preview_labels := {} var count_label: Label
var b_tip: Button var info_label: Label
var b_reroll: Button
var rerolls_left := 3
func _ready() -> void: func _ready() -> void:
var col := Style.column(self, 1000, 30, 10) var col := Style.column(self, 1000, 60, 14)
Style.clabel(col, "Einzelspieler", 36, Style.GOLD, "center", true) Style.clabel(col, "Einzelspieler", 36, Style.GOLD, "center", true)
Style.clabel(col, "Jeder kämpft gegen jeden, alle nach denselben Regeln mit %d Leben. Du gewinnst, wenn alle anderen besiegt sind." % Game.START_LIFE, 15, Style.MUTED) Style.spacer(col, 10)
Style.clabel(col, "Schwierigkeit", 20, Style.TEXT, "center", true) Style.clabel(col, "Schwierigkeit", 20, Style.TEXT, "center", true)
var dr := Style.row(col, 10) var dr := Style.row(col, 10)
dr.alignment = BoxContainer.ALIGNMENT_CENTER dr.alignment = BoxContainer.ALIGNMENT_CENTER
for key in ["leicht", "normal", "schwer"]: for key in ["leicht", "normal", "schwer"]:
var k: String = key var k: String = key
diff_buttons[k] = Style.cbutton(dr, Profile.DIFFICULTIES[k].name, Vector2(150, 40), func(): set_difficulty(k)) diff_buttons[k] = Style.cbutton(dr, Profile.DIFFICULTIES[k].name, Vector2(150, 40), func(): set_difficulty(k))
rating_label = Style.clabel(col, "", 14, Style.MUTED) rating_label = Style.clabel(col, "", 15, Style.MUTED)
var tr_row := Style.row(col, 10) Style.spacer(col, 24)
tr_row.alignment = BoxContainer.ALIGNMENT_CENTER count_label = Style.clabel(col, "", 20, Style.TEXT, "center", true)
tip_label = Style.clabel(tr_row, "", 14, Style.GOLD, "right", false, 520) var sr := Style.row(col, 12)
b_tip = Style.cbutton(tr_row, "Übernehmen", Vector2(140, 32), take_tip) sr.alignment = BoxContainer.ALIGNMENT_CENTER
Style.spacer(col, 2) Style.clabel(sr, "1", 15, Style.MUTED, "right", false, 20)
for i in range(1, Game.MAX_OPPONENTS + 1): count_slider = HSlider.new()
var n := i count_slider.min_value = 1
var r := Style.row(col, 16) count_slider.max_value = Game.MAX_OPPONENTS
r.alignment = BoxContainer.ALIGNMENT_CENTER count_slider.step = 1
Style.cbutton(r, "%d Gegner" % n, Vector2(150, 46), func(): start(n), Color("285a3c")) count_slider.value = 1
var info := (tr("Duell") if n == 1 else tr("%d am Tisch") % (n + 1)) + " · " + tr("%d Münzen") % Profile.REWARD_WIN[n] count_slider.tick_count = Game.MAX_OPPONENTS
Style.clabel(r, info, 13, Style.MUTED, "left", false, 150) count_slider.ticks_on_borders = true
preview_labels[n] = Style.clabel(r, "", 13, Style.TEXT, "left", false, 600) count_slider.custom_minimum_size = Vector2(420, 32)
preview_labels[n].autowrap_mode = TextServer.AUTOWRAP_WORD count_slider.value_changed.connect(func(_v): refresh())
preview_labels[n].custom_minimum_size = Vector2(600, 0) sr.add_child(count_slider)
preview_labels[n].size_flags_horizontal = Control.SIZE_SHRINK_BEGIN Style.clabel(sr, str(Game.MAX_OPPONENTS), 15, Style.MUTED, "left", false, 20)
Style.spacer(col, 4) info_label = Style.clabel(col, "", 15, Style.MUTED)
Style.spacer(col, 30)
var br := Style.row(col, 16) var br := Style.row(col, 16)
br.alignment = BoxContainer.ALIGNMENT_CENTER br.alignment = BoxContainer.ALIGNMENT_CENTER
b_reroll = Style.cbutton(br, "Neu würfeln", Vector2(190, 40), reroll) Style.cbutton(br, "Starten", Vector2(190, 46), start, Color("285a3c"))
Style.cbutton(br, "Zurück", Vector2(150, 40), func(): app.show_screen(MenuScreen.new())) Style.cbutton(br, "Zurück", Vector2(150, 46), func(): app.show_screen(MenuScreen.new()))
refresh(true) refresh()
func set_difficulty(key: String) -> void: func set_difficulty(key: String) -> void:
app.profile.settings.difficulty = key app.profile.settings.difficulty = key
app.save() app.save()
refresh(true) refresh()
func take_tip() -> void: func opponents() -> int:
var tip: String = app.profile.suggest_difficulty() return int(count_slider.value)
if tip != "":
set_difficulty(tip)
## Höchstens dreimal neu würfeln, sonst ließen sich schwache Gegner erwürfeln. func start() -> void:
func reroll() -> void: app.show_screen(BattleScreen.new(opponents()))
if rerolls_left <= 0:
return
rerolls_left -= 1
refresh(true)
func start(n: int) -> void: func refresh() -> void:
app.show_screen(BattleScreen.against(previews[n]))
## Baut die Gegner für jede Anzahl vor und zeigt Element und Stärke; der Kampf nutzt genau diese.
func refresh(reroll: bool = false) -> void:
var p: Profile = app.profile var p: Profile = app.profile
var d := p.difficulty()
for key in diff_buttons: for key in diff_buttons:
var b: Button = diff_buttons[key] var b: Button = diff_buttons[key]
var active: bool = key == p.settings.difficulty var active: bool = key == p.settings.difficulty
b.add_theme_stylebox_override("normal", Style.box(Color("285a3c") if active else Style.PANEL_LIGHT, 8, Style.GOLD if active else Style.MUTED, 2)) b.add_theme_stylebox_override("normal", Style.box(Color("285a3c") if active else Style.PANEL_LIGHT, 8, Style.GOLD if active else Style.MUTED, 2))
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(Color("285a3c") if active else Style.PANEL_LIGHT), 8, Style.GOLD if active else Style.MUTED, 2)) b.add_theme_stylebox_override("hover", Style.box(Style.lighter(Color("285a3c") if active else Style.PANEL_LIGHT), 8, Style.GOLD if active else Style.MUTED, 2))
var arche: String = tr("schwache Archetypen") if d.tier == "leicht" else (tr("starke Archetypen") if d.tier == "schwer" else tr("alle Archetypen")) rating_label.text = tr("Deine Deckstärke %.1f · Gegnerdecks %.1f") % [p.deck_rating(), p.target_rating()]
rating_label.text = tr("Deine Deckstärke %.1f · Gegnerdecks %.1f · %s · Münzen x%.2f · KI: %s") % [ var n := opponents()
p.deck_rating(), p.target_rating(), arche, d.reward, tr("Vorausschau") if d.smart else tr("einfach")] count_label.text = tr("1 Gegner") if n == 1 else tr("%d Gegner") % n
var tip := p.suggest_difficulty() info_label.text = (tr("Duell") if n == 1 else tr("%d am Tisch") % (n + 1)) + " · " + tr("%d Münzen") % Profile.REWARD_WIN[n]
tip_label.text = (tr("Tipp: %s") % ((tr("%d Siege in Folge, versuche %s") % [p.records.streak, tr(Profile.DIFFICULTIES[tip].name)]) if p.records.streak >= 3
else (tr("%d Niederlagen in Folge, versuche %s") % [p.records.loss_streak, tr(Profile.DIFFICULTIES[tip].name)]))) if tip != "" else ""
b_tip.visible = tip != ""
if reroll:
var r := Game.make_rng()
for n in range(1, Game.MAX_OPPONENTS + 1):
var opps := []
for i in n:
opps.append(Game.random_opponent(r, i + 1, Game.START_LIFE, p.target_rating(), str(d.tier)))
previews[n] = opps
b_reroll.text = tr("Neu würfeln (%d)") % rerolls_left
b_reroll.disabled = rerolls_left <= 0
for n in previews:
var parts: Array[String] = []
for o in previews[n]:
parts.append("%s (%.1f)" % [o.title, Game.deck_rating(o.deck)])
preview_labels[n].text = ", ".join(parts)
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE: if event.is_action_pressed("ui_cancel"):
app.show_screen(MenuScreen.new()) app.show_screen(MenuScreen.new())

View File

@@ -300,7 +300,6 @@ static func rules_text() -> String:
lines.append("Ein unterbrochener Einzelspieler-Kampf lässt sich im Menü fortsetzen.") 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("Netzwerkspiel: Fällt der Host aus, übernimmt ein Mitspieler; Zuschauer sehen die Partie ohne Hände.") 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")

View File

@@ -97,7 +97,6 @@ func _initialize() -> void:
test_kampagne() test_kampagne()
test_fallen() test_fallen()
test_ki_sieht_fallen_nicht() test_ki_sieht_fallen_nicht()
test_hotseat()
test_zufallssamen() test_zufallssamen()
test_deck_slots_und_werkstatt() test_deck_slots_und_werkstatt()
test_spielstand_sicherung() test_spielstand_sicherung()
@@ -810,34 +809,6 @@ func test_ki_sieht_fallen_nicht() -> void:
check(g.clone_for(h).players[hi].traps.size() == 1, "eigene Fallen bleiben in der eigenen Kopie") check(g.clone_for(h).players[hi].traps.size() == 1, "eigene Fallen bleiben in der eigenen Kopie")
func test_hotseat() -> void:
var r := RandomNumberGenerator.new()
r.seed = 3
var d := Profile.starter().deck_cards()
var g := Game.new_hotseat([d, d], 1, r)
check(g.players.size() == 3 and g.humans.size() == 2, "zwei Menschen und eine KI")
check(g.humans[0].name == "Spieler 1" and g.humans[1].name == "Spieler 2", "Spielernamen")
var ai = g.players.filter(func(p): return not p.is_human)[0]
g.damage(ai, 99)
g.check_end()
check(g.winner == "", "zwei Menschen übrig: weiter")
g.damage(g.humans[1], 99)
g.check_end()
check(g.winner == "human" and g.winner_player == g.humans[0], "Spieler 1 gewinnt")
var g2 := Game.new_hotseat([d, d], 1, r)
for hu in g2.humans:
g2.damage(hu, 99)
g2.check_end()
check(g2.winner == "opponents" and g2.winner_player != null and not g2.winner_player.is_human, "KI gewinnt, wenn alle Menschen tot sind")
var g3 := Game.new_hotseat([d, d, d], 0, r)
check(g3.players.size() == 3 and g3.opponents.size() == 2, "drei Menschen ohne KI")
var guard := 0
while g3.winner == "" and guard < 300:
AI.take_turn(g3, g3.current(), true)
guard += 1
check(g3.winner == "human" and g3.winner_player != null, "Hotseat-Partie endet mit Sieger")
func test_zufallssamen() -> void: func test_zufallssamen() -> void:
Game.test_seed = 77 Game.test_seed = 77
var a := Game.make_rng().randi() var a := Game.make_rng().randi()

View File

@@ -168,7 +168,11 @@ func _ready() -> void:
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] test_kampf_fortsetzen, test_gamepad_deckbau, test_deckcode, test_elementbooster]
if not fast: if not fast:
suite += [test_hotseat, test_netzwerk, test_host_wechsel, test_zuschauer, test_kampf.bind(2, 3), test_kampf_abbrechen] suite += [test_kampf.bind(2, 3), test_kampf_abbrechen]
# Die Netzwerktests starten Bot-Prozesse und warten auf Verbindungen; das dauert Minuten.
# Sie laufen nur auf Wunsch: DINGER_NETZ=1 (oder gezielt über DINGER_ONLY).
if OS.get_environment("DINGER_NETZ") != "":
suite += [test_netzwerk, test_host_wechsel, test_zuschauer]
suite.append(test_profil_zuruecksetzen) suite.append(test_profil_zuruecksetzen)
var only := OS.get_environment("DINGER_ONLY") # z. B. DINGER_ONLY=test_host_wechsel,test_zuschauer var only := OS.get_environment("DINGER_ONLY") # z. B. DINGER_ONLY=test_host_wechsel,test_zuschauer
if only != "": if only != "":
@@ -373,25 +377,38 @@ func human_turn(scr) -> void:
func start_battle(n_opp: int) -> void: func start_battle(n_opp: int) -> void:
await click(find_button("Einzelspieler")) await click(find_button("Einzelspieler"))
check(screen() is OpponentSelectScreen, "Gegnerauswahl") check(screen() is OpponentSelectScreen, "Gegnerauswahl")
await click(find_button("%d Gegner" % n_opp)) screen().count_slider.value = n_opp
await frames(1)
await click(find_button("Starten"))
func test_gegnerauswahl() -> void: func test_gegnerauswahl() -> void:
await click(find_button("Einzelspieler")) await click(find_button("Einzelspieler"))
var sel = screen() var sel = screen()
check(sel is OpponentSelectScreen, "Gegnerauswahl geöffnet") check(sel is OpponentSelectScreen, "Gegnerauswahl geöffnet")
check("Deckstärke" in sel.rating_label.text, "Deckstärke angezeigt") check("Deckstärke" in sel.rating_label.text and "Gegnerdecks" in sel.rating_label.text
check(sel.preview_labels[3].text.count("(") == 3 and sel.previews[3].size() == 3, "Vorschau zeigt drei Gegner mit Stärke") and not ("Archetypen" in sel.rating_label.text or "KI" in sel.rating_label.text), "unter der Schwierigkeit nur die zwei Stärken")
for i in 3: check(find_button("Neu würfeln") == null and find_button("1 Gegner") == null and find_button("Starten") != null, "kein Würfeln und keine Gegnerknöpfe, nur Starten")
await click(sel.b_reroll) check(sel.count_slider.min_value == 1 and sel.count_slider.max_value == Game.MAX_OPPONENTS and sel.count_slider.step == 1, "Gegnerzahl als Schieberegler von 1 bis %d" % Game.MAX_OPPONENTS)
check(sel.rerolls_left == 0 and sel.b_reroll.disabled, "höchstens dreimal neu würfeln") check(sel.opponents() == 1 and sel.count_label.text == "1 Gegner" and "Duell" in sel.info_label.text, "Regler beginnt beim Duell")
var rect: Rect2 = sel.count_slider.get_global_rect()
await click_at(Vector2(rect.end.x - 4, rect.get_center().y)) # ganz rechts anklicken
check(sel.opponents() == Game.MAX_OPPONENTS and sel.count_label.text == "%d Gegner" % Game.MAX_OPPONENTS, "Mausklick am rechten Rand stellt %d Gegner ein" % Game.MAX_OPPONENTS)
sel.count_slider.grab_focus()
await key(KEY_LEFT)
check(sel.opponents() == Game.MAX_OPPONENTS - 1, "Pfeiltaste links nimmt einen Gegner weg")
sel.count_slider.value = 4
await frames(1)
check(sel.opponents() == 4 and sel.count_label.text == "4 Gegner" and "5 am Tisch" in sel.info_label.text
and str(Profile.REWARD_WIN[4]) in sel.info_label.text, "Regler auf 4: fünf am Tisch mit Belohnung")
await click(find_button("Schwer")) await click(find_button("Schwer"))
check(main.profile.settings.difficulty == "schwer" and Profile.load_or_new(SAVE).settings.difficulty == "schwer", "Schwer gewählt und gespeichert") check(main.profile.settings.difficulty == "schwer" and Profile.load_or_new(SAVE).settings.difficulty == "schwer", "Schwer gewählt und gespeichert")
var previewed: Array = sel.previews[1].map(func(o): return o.title)
await shot("07_gegnerauswahl") await shot("07_gegnerauswahl")
await click(find_button("1 Gegner")) sel.count_slider.value = 1
await frames(1)
await click(find_button("Starten"))
var hard = screen() var hard = screen()
check(hard.game.opponents.map(func(o): return o.title) == previewed, "Kampf nutzt die vorgeschauten Gegner") check(hard is BattleScreen and hard.game.opponents.size() == 1, "Kampf mit einem Gegner gestartet")
var own := Game.deck_rating(main.profile.deck_cards()) var own := Game.deck_rating(main.profile.deck_cards())
var opp: Game.Player = hard.game.opponents[0] var opp: Game.Player = hard.game.opponents[0]
var opp_rating := Game.deck_rating(opp.deck + opp.hand) var opp_rating := Game.deck_rating(opp.deck + opp.hand)
@@ -399,22 +416,13 @@ func test_gegnerauswahl() -> void:
await give_up() await give_up()
await click(find_button("Einzelspieler")) await click(find_button("Einzelspieler"))
await click(find_button("Leicht")) await click(find_button("Leicht"))
await click(find_button("1 Gegner")) await click(find_button("Starten"))
var easy = screen() var easy = screen()
opp = easy.game.opponents[0] opp = easy.game.opponents[0]
check(Game.deck_rating(opp.deck + opp.hand) <= own * 1.0 + 0.15 and not easy.smart_ai, "Leicht: schwächeres Deck, einfache KI") check(Game.deck_rating(opp.deck + opp.hand) <= own * 1.0 + 0.15 and not easy.smart_ai, "Leicht: schwächeres Deck, einfache KI")
await give_up() await give_up()
await click(find_button("Einzelspieler")) await click(find_button("Einzelspieler"))
await click(find_button("Normal")) await click(find_button("Normal"))
main.profile.records.streak = 3
main.profile.records.margins = [20, 22, 18]
screen().refresh()
check(screen().b_tip.visible and "Schwer" in screen().tip_label.text, "Vorschlag nach drei deutlichen Siegen")
await click(find_button("Übernehmen"))
check(main.profile.settings.difficulty == "schwer", "Vorschlag übernommen")
main.profile.records.streak = 0
main.profile.records.margins = []
await click(find_button("Normal"))
await click(find_button("Zurück")) await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü") check(screen() is MenuScreen, "zurück im Menü")
for n in [1, 5]: for n in [1, 5]:
@@ -707,40 +715,6 @@ func test_herausforderung() -> void:
check(screen() is MenuScreen, "zurück im Menü") check(screen() is MenuScreen, "zurück im Menü")
func test_hotseat() -> void:
await click(find_button("Lokal zu mehreren"))
var scr = screen()
check(scr is HotseatScreen, "Hotseat-Auswahl")
await click(find_button("3 Spieler"))
await click(find_button("1 KI"))
check(scr.n_humans == 3 and scr.n_ai == 1, "3 Menschen, 1 KI")
await shot("17_hotseat")
await click(find_button("Start"))
var b = screen()
check(b is BattleScreen and b.is_hotseat() and b.game.players.size() == 4, "Hotseat-Kampf gestartet")
b.ai_delay = 0.0
var guard := 0
while not b.handover.visible and b.game.winner == "" and guard < 400:
if b.game.current() == b.perspective:
await clear_mulligan(b)
if b.game.current() == b.perspective:
await click(b.b_end)
await frames(1)
guard += 1
check(b.handover.visible, "Übergabe erscheint")
var cur: Game.Player = b.game.current()
check(cur.is_human and cur != b.perspective, "anderer Mensch ist dran")
await shot("18_hotseat_uebergabe")
await click(find_button("Weiter"))
check(b.perspective == cur and not b.handover.visible, "Perspektive gewechselt")
check(b.hand_views.size() == cur.hand.size() and b.name_label.text == cur.name, "Hand und Name des neuen Spielers")
check(b.mulligan_box.visible, "neuer Spieler bekommt seine Mulligan-Maske")
await clear_mulligan(b)
check(b.hero_panels.size() == 3 and not b.hero_panels.has(cur), "drei andere am Tisch")
await shot("19_hotseat_kampf")
await leave_battle()
func test_sprache() -> void: func test_sprache() -> void:
await click(find_button("Einstellungen")) await click(find_button("Einstellungen"))
var scr = screen() var scr = screen()
@@ -1028,7 +1002,7 @@ func test_zuschauer() -> void:
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, 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)) "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") await shot("35_zuschauer")
check(not b.handover.visible and not b.is_hotseat(), "Zuschauer bekommt keine Hotseat-Übergabe") check(not b.b_end.visible and not b.b_undo.visible, "Zuschauer hat keine Zugknöpfe")
if screen() is BattleScreen: # die Bots spielen schnell, die Partie kann schon vorbei sein if screen() is BattleScreen: # die Bots spielen schnell, die Partie kann schon vorbei sein
await leave_battle() await leave_battle()
else: else:

View File

@@ -3,7 +3,6 @@ Dinger,Dinger
Ein Kartenspiel: jeder gegen jeden,A card game: free-for-all Ein Kartenspiel: jeder gegen jeden,A card game: free-for-all
Einzelspieler,Single player Einzelspieler,Single player
Kampagne,Campaign Kampagne,Campaign
Lokal zu mehreren,Local multiplayer
Netzwerkspiel,Network game Netzwerkspiel,Network game
Booster öffnen,Open boosters Booster öffnen,Open boosters
Deck bauen,Build deck Deck bauen,Build deck
@@ -95,20 +94,11 @@ Schwierigkeit,Difficulty
Leicht,Easy Leicht,Easy
Normal,Normal Normal,Normal
Schwer,Hard Schwer,Hard
"Deine Deckstärke %.1f · Gegnerdecks %.1f · %s · Münzen x%.2f · KI: %s","Your deck strength %.1f · Opponent decks %.1f · %s · Coins x%.2f · AI: %s"
schwache Archetypen,weaker archetypes
starke Archetypen,stronger archetypes
alle Archetypen,all archetypes
Vorausschau,lookahead
einfach,simple
Stärke %.1f,Strength %.1f Stärke %.1f,Strength %.1f
"Deckstärke: Durchschnitt aus Seltenheit, Schlüsselwörtern und Effekten","Deck strength: average of rarity, keywords and effects" "Deckstärke: Durchschnitt aus Seltenheit, Schlüsselwörtern und Effekten","Deck strength: average of rarity, keywords and effects"
Tipp: %s,Tip: %s
Übernehmen,Apply
Neu würfeln,Reroll Neu würfeln,Reroll
Deck verbessern: %s ins aktive Deck (%s),Improve deck: add %s to the active deck (%s) Deck verbessern: %s ins aktive Deck (%s),Improve deck: add %s to the active deck (%s)
Anleitung 2: Fallen,Tutorial 2: Traps Anleitung 2: Fallen,Tutorial 2: Traps
Neu würfeln (%d),Reroll (%d)
Profil zurücksetzen,Reset profile Profil zurücksetzen,Reset profile
Große Schrift,Large text Große Schrift,Large text
Verwerfen,Discard Verwerfen,Discard
@@ -126,13 +116,10 @@ Fenn verliert bei 0 Leben. Tausche im ersten Zug einer Partie schwache Starthän
"Beende den Zug und warte. Greift Mara dich oder ein Wesen an, verpufft der Angriff und ihr Wesen friert ein.","End your turn and wait. If Mara attacks you or a creature, the attack fizzles and her creature freezes." "Beende den Zug und warte. Greift Mara dich oder ein Wesen an, verpufft der Angriff und ihr Wesen friert ein.","End your turn and wait. If Mara attacks you or a creature, the attack fizzles and her creature freezes."
Feldkombo Dampf: Ein Wasser-Wesen neben einem Feuer-Wesen friert das stärkste gegnerische Wesen ein. Spiele beide nacheinander.,Field combo Steam: a water creature next to a fire creature freezes the strongest enemy creature. Play both in sequence. Feldkombo Dampf: Ein Wasser-Wesen neben einem Feuer-Wesen friert das stärkste gegnerische Wesen ein. Spiele beide nacheinander.,Field combo Steam: a water creature next to a fire creature freezes the strongest enemy creature. Play both in sequence.
"Rückgängig (U) nimmt ein gerade gespieltes Wesen zurück, solange nichts ausgelöst wurde. Viel Erfolg gegen Mara!",Undo (U) takes back a creature you just played as long as nothing was triggered. Good luck against Mara! "Rückgängig (U) nimmt ein gerade gespieltes Wesen zurück, solange nichts ausgelöst wurde. Viel Erfolg gegen Mara!",Undo (U) takes back a creature you just played as long as nothing was triggered. Good luck against Mara!
"%d Siege in Folge, versuche %s","%d wins in a row, try %s"
"%d Niederlagen in Folge, versuche %s","%d losses in a row, try %s"
Duell,Duel Duell,Duel
%d am Tisch,%d at the table %d am Tisch,%d at the table
%d Spieler am Tisch,%d players at the table %d Spieler am Tisch,%d players at the table
%d Münzen,%d coins %d Münzen,%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" "Speichern und zurück","Save and go back"
@@ -217,3 +204,8 @@ Kapitel %d,Chapter %d
Zuschauen,Watch Zuschauen,Watch
Zuschauer,Spectator Zuschauer,Spectator
"Zuschauer: Hände sind verdeckt","Spectator: hands are hidden" "Zuschauer: Hände sind verdeckt","Spectator: hands are hidden"
Deine Deckstärke %.1f · Gegnerdecks %.1f,Your deck strength %.1f · Opponent decks %.1f
Starten,Start
1 Gegner,1 opponent
%d Gegner,%d opponents
Übernehmen,Apply
1 keys en
3 Ein Kartenspiel: jeder gegen jeden A card game: free-for-all
4 Einzelspieler Single player
5 Kampagne Campaign
Lokal zu mehreren Local multiplayer
6 Netzwerkspiel Network game
7 Booster öffnen Open boosters
8 Deck bauen Build deck
94 Leicht Easy
95 Normal Normal
96 Schwer Hard
Deine Deckstärke %.1f · Gegnerdecks %.1f · %s · Münzen x%.2f · KI: %s Your deck strength %.1f · Opponent decks %.1f · %s · Coins x%.2f · AI: %s
schwache Archetypen weaker archetypes
starke Archetypen stronger archetypes
alle Archetypen all archetypes
Vorausschau lookahead
einfach simple
97 Stärke %.1f Strength %.1f
98 Deckstärke: Durchschnitt aus Seltenheit, Schlüsselwörtern und Effekten Deck strength: average of rarity, keywords and effects
Tipp: %s Tip: %s
Übernehmen Apply
99 Neu würfeln Reroll
100 Deck verbessern: %s ins aktive Deck (%s) Improve deck: add %s to the active deck (%s)
101 Anleitung 2: Fallen Tutorial 2: Traps
Neu würfeln (%d) Reroll (%d)
102 Profil zurücksetzen Reset profile
103 Große Schrift Large text
104 Verwerfen Discard
116 Beende den Zug und warte. Greift Mara dich oder ein Wesen an, verpufft der Angriff und ihr Wesen friert ein. End your turn and wait. If Mara attacks you or a creature, the attack fizzles and her creature freezes.
117 Feldkombo Dampf: Ein Wasser-Wesen neben einem Feuer-Wesen friert das stärkste gegnerische Wesen ein. Spiele beide nacheinander. Field combo Steam: a water creature next to a fire creature freezes the strongest enemy creature. Play both in sequence.
118 Rückgängig (U) nimmt ein gerade gespieltes Wesen zurück, solange nichts ausgelöst wurde. Viel Erfolg gegen Mara! Undo (U) takes back a creature you just played as long as nothing was triggered. Good luck against Mara!
%d Siege in Folge, versuche %s %d wins in a row, try %s
%d Niederlagen in Folge, versuche %s %d losses in a row, try %s
119 Duell Duel
120 %d am Tisch %d at the table
121 %d Spieler am Tisch %d players at the table
122 %d Münzen %d coins
Stärke %.1f · %d Münzen Strength %.1f · %d coins
123 Abbrechen Cancel
124 Ja, zurücksetzen Yes, reset
125 Speichern und zurück Save and go back
204 Zuschauen Watch
205 Zuschauer Spectator
206 Zuschauer: Hände sind verdeckt Spectator: hands are hidden
207 Deine Deckstärke %.1f · Gegnerdecks %.1f Your deck strength %.1f · Opponent decks %.1f
208 Starten Start
209 1 Gegner 1 opponent
210 %d Gegner %d opponents
211 Übernehmen Apply

Binary file not shown.