From 77d1b318d6c9da4df6e460168ae1ac26d0a254e3 Mon Sep 17 00:00:00 2001 From: Team3 Date: Wed, 16 Sep 2026 17:02:59 +0200 Subject: [PATCH] =?UTF-8?q?Lava=20abgeschw=C3=A4cht,=20Archetypen=20aus=20?= =?UTF-8?q?Datei=20mit=20CI-Messlauf,=20Kartentext=20folgt=20Schriftgr?= =?UTF-8?q?=C3=B6=C3=9Fe,=20Gamepad=20in=20Men=C3=BCs,=20Anleitung=202=20m?= =?UTF-8?q?it=20sicherem=20Angriff,=20=C3=9Cbersetzungspr=C3=BCfung,=20Lin?= =?UTF-8?q?t=20und=20gestaffelte=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx --- .github/workflows/tests.yml | 54 +++++++++++++++++++++++++-- README.md | 7 +++- data/archetypen.json | 12 ++++++ scripts/cards.gd | 4 +- scripts/deckgen.gd | 28 +++++++++++--- scripts/ui/battle_screen.gd | 2 +- scripts/ui/battle_tutorial.gd | 14 ++++--- scripts/ui/card_view.gd | 15 +++++--- scripts/ui/main.gd | 35 +++++++++++++++++ scripts/ui/opponent_select_screen.gd | 6 +-- scripts/ui/style.gd | 4 +- tests/lint.gd | 29 ++++++++++++++ tests/lint.gd.uid | 1 + tests/run_headless.sh | 4 +- tests/strength_probe.gd | 4 ++ tests/test_runner.gd | 44 ++++++++++++++++++++-- tests/ui_test.gd | 39 ++++++++++++++++++- translations/ui.csv | 19 ++++++++++ translations/ui.en.translation | Bin 5156 -> 6480 bytes 19 files changed, 285 insertions(+), 36 deletions(-) create mode 100644 data/archetypen.json create mode 100644 tests/lint.gd create mode 100644 tests/lint.gd.uid diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e6ea0eb..bd4ab19 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,11 @@ name: Tests -on: [push, pull_request] +on: + push: + pull_request: + schedule: + - cron: "0 3 * * *" + workflow_dispatch: jobs: headless: @@ -13,10 +18,11 @@ jobs: unzip -q godot.zip mv Godot_v4.7.2-stable_linux.x86_64 godot chmod +x godot - - name: Tests + - name: Statische Prüfung und Tests run: ./tests/run_headless.sh ./godot - fenster: + fenster-schnell: + if: github.event_name != 'schedule' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -29,7 +35,47 @@ jobs: sudo apt-get update -qq && sudo apt-get install -y -qq xvfb libgl1 libegl1 - name: Import run: ./godot --headless --path . --import || true - - name: Fenster-Test (virtueller Bildschirm) + - name: Fenster-Test (schnell) + run: DINGER_FAST=1 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 + run: grep -q " 0 fehlgeschlagen" fenster.log + + fenster-voll: + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Godot und Xvfb laden + run: | + curl -sSL -o godot.zip https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_linux.x86_64.zip + unzip -q godot.zip + mv Godot_v4.7.2-stable_linux.x86_64 godot + chmod +x godot + sudo apt-get update -qq && sudo apt-get install -y -qq xvfb libgl1 libegl1 + - name: Import + run: ./godot --headless --path . --import || true + - name: Fenster-Test (voll, mit Netzwerk und Hotseat) 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 run: grep -q " 0 fehlgeschlagen" fenster.log + + balance: + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Godot laden + run: | + curl -sSL -o godot.zip https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_linux.x86_64.zip + unzip -q godot.zip + mv Godot_v4.7.2-stable_linux.x86_64 godot + chmod +x godot + - name: Import + run: ./godot --headless --path . --import || true + - name: Archetypen messen + run: ./godot --headless --path . -s tests/strength_probe.gd 2>&1 | grep -E "%|geschrieben" + - name: Ergebnis sichern + uses: actions/upload-artifact@v4 + with: + name: archetypen + path: data/archetypen.json diff --git a/README.md b/README.md index aef7a10..a7ccc3e 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,14 @@ Der Spielstand liegt unter `user://spielstand.json` - Anleitung Teil 2 (Mulligan, Fallen, Feldkombo Dampf), Gamepad-Steuerung, große Schrift, Profil zurücksetzen - Gegnervorschau mit höchstens drei Neuwürfen, Schwierigkeitstipp nach Spielverlauf (deutliche Siege oder drei Niederlagen) - KI mit Zeitbudget je Entscheidung (150 ms), Warnung bei ungespeichertem Deck +- 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 +- Übersetzungsprüfung: jeder tr()-Text und jeder Anleitungsschritt braucht einen englischen Eintrag ## Tests -`tests/run_headless.sh` führt Import und Headless-Prüfungen aus. `.github/workflows/tests.yml` läuft beides in CI: -die Headless-Prüfungen direkt, den Fenster-Test unter Xvfb (lokal ohne Xvfb nicht geprüft). +`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). `DINGER_FAST=1` lässt den Fenster-Test ohne Hotseat, Netzwerk und lange Kämpfe laufen (etwa ein Drittel der Zeit). Statistiken aus Spieltests lassen sich unter Erfolge als CSV exportieren. diff --git a/data/archetypen.json b/data/archetypen.json new file mode 100644 index 0000000..c9514d9 --- /dev/null +++ b/data/archetypen.json @@ -0,0 +1,12 @@ +{ + "Erde": 55, + "Erde+Luft": 78, + "Feuer": 36, + "Feuer+Erde": 65, + "Feuer+Luft": 46, + "Feuer+Wasser": 50, + "Luft": 55, + "Wasser": 40, + "Wasser+Erde": 51, + "Wasser+Luft": 45 +} \ No newline at end of file diff --git a/scripts/cards.gd b/scripts/cards.gd index 5cc7315..11763db 100644 --- a/scripts/cards.gd +++ b/scripts/cards.gd @@ -29,7 +29,7 @@ const COMBOS := { # Feldkombos: entstehen, wenn ein Wesen neben einem Wesen mit anderem Element ausgespielt wird. const FIELD_COMBO_TEXT := { "Dampf": "Friere das stärkste gegnerische Wesen ein", - "Lava": "1 Schaden an alle gegnerischen Wesen", + "Lava": "1 Schaden an das stärkste gegnerische Wesen", "Blitz": "2 Schaden an den schwächsten Gegner", "Schlamm": "Heile dich um 3", "Sturm": "Ziehe 1 Karte", @@ -74,7 +74,7 @@ static func field_combo_effect(name: String) -> Effect: "Dampf": return Effect.new("freeze", "strongest_enemy_creature") "Lava": - return Effect.new("damage", "all_enemy_creatures", 1) + return Effect.new("damage", "strongest_enemy_creature", 1) "Blitz": return Effect.new("damage", "weakest_enemy_hero", 2) "Schlamm": diff --git a/scripts/deckgen.gd b/scripts/deckgen.gd index 1ff1922..73f7bf8 100644 --- a/scripts/deckgen.gd +++ b/scripts/deckgen.gd @@ -7,17 +7,35 @@ const RESONANCE_STEPS := [0, 1, 2, 3, 3] # Gemessene Stärke der Elementkombinationen (Siegquote gegen das Feld, KI gegen KI, 60 Partien je Archetyp, tests/strength_probe.gd). const ARCHETYPE_STRENGTH := { - "Feuer": 38, "Wasser": 38, "Erde": 55, "Luft": 55, - "Feuer+Wasser": 45, "Feuer+Erde": 78, "Feuer+Luft": 46, - "Wasser+Erde": 50, "Wasser+Luft": 45, "Erde+Luft": 78, + "Feuer": 36, "Wasser": 40, "Erde": 55, "Luft": 55, + "Feuer+Wasser": 50, "Feuer+Erde": 65, "Feuer+Luft": 46, + "Wasser+Erde": 51, "Wasser+Luft": 45, "Erde+Luft": 78, } +const ARCHETYPE_PATH := "res://data/archetypen.json" +static var _strength: Dictionary = {} + + +## Gemessene Werte aus data/archetypen.json (vom Messlauf geschrieben), sonst die eingebaute Tabelle. +static func strength() -> Dictionary: + if _strength.is_empty(): + _strength = ARCHETYPE_STRENGTH.duplicate() + if FileAccess.file_exists(ARCHETYPE_PATH): + var json := JSON.new() + if json.parse(FileAccess.get_file_as_string(ARCHETYPE_PATH)) == OK and json.data is Dictionary: + for k in json.data: + if _strength.has(k): + _strength[k] = int(json.data[k]) + return _strength + + ## Elementkombinationen einer Stufe: "leicht" die schwächere Hälfte, "schwer" die stärkere, sonst alle. static func archetypes(tier: String = "") -> Array: - var keys: Array = ARCHETYPE_STRENGTH.keys() - keys.sort_custom(func(a, b): return ARCHETYPE_STRENGTH[a] < ARCHETYPE_STRENGTH[b]) + var table := strength() + var keys: Array = table.keys() + keys.sort_custom(func(a, b): return table[a] < table[b]) var half: int = keys.size() / 2 match tier: "leicht": diff --git a/scripts/ui/battle_screen.gd b/scripts/ui/battle_screen.gd index 4a45ba4..6e19e38 100644 --- a/scripts/ui/battle_screen.gd +++ b/scripts/ui/battle_screen.gd @@ -196,7 +196,7 @@ func _ready() -> void: var opps := Campaign.opponents(guide.stage_index(), r, Game.deck_rating(app.profile.deck_cards())) opps[0].perks = [] game = Game.new_battle_with(app.profile.deck_cards(), opps, r, true) - guide.inject_cards(game.human.hand) + guide.inject_cards(game) elif not challenge.is_empty(): var rules: Dictionary = challenge.rules.duplicate() rules["target_rating"] = Game.deck_rating(app.profile.deck_cards()) diff --git a/scripts/ui/battle_tutorial.gd b/scripts/ui/battle_tutorial.gd index 36fb326..b16ff6e 100644 --- a/scripts/ui/battle_tutorial.gd +++ b/scripts/ui/battle_tutorial.gd @@ -8,7 +8,7 @@ const STEPS1 := [ ["Wesen greifen erst ab dem nächsten Zug an. Beende deinen Zug mit dem Knopf rechts oder der Leertaste.", "turn_passed"], ["Grüne Rahmen zeigen angriffsbereite Wesen. Klicke ein Wesen und dann Fenn oder eines seiner Wesen. Wächter müssen zuerst fallen.", "attacked"], ["Resonanz: Wesen mit gleichem Element geben sich gegenseitig Angriff. Spiele ein zweites Feuer- oder Erde-Wesen.", "resonance"], - ["Feldkombo: Ein Wesen neben einem eigenen Wesen mit anderem Element löst einen Effekt aus. Lava macht 1 Schaden an alle gegnerischen Wesen.", "combo"], + ["Feldkombo: Ein Wesen neben einem eigenen Wesen mit anderem Element löst einen Effekt aus. Lava macht 1 Schaden an das stärkste gegnerische Wesen.", "combo"], ["Zauber wirken sofort. Funke oder Feuerball treffen auch Fenn direkt. Ziele wählst du per Klick, Rechtsklick bricht ab.", "spell"], ["Fenn verliert bei 0 Leben. Tausche im ersten Zug einer Partie schwache Starthände (Mulligan). Viel Erfolg!", ""], ] @@ -38,11 +38,13 @@ func stage_index() -> int: return 0 if part == 1 else 1 -## Teil 2 bekommt die Lehrkarten auf die Hand. -func inject_cards(hand: Array) -> void: +## Teil 2: Lehrkarten auf die eigene Hand; Mara bekommt Wesen, damit sie sicher in die Falle läuft. +func inject_cards(g: Game) -> void: if part == 2: - hand.append(Cards.by_id("w_falle")) - hand.append(Cards.by_id("w_reiter")) + g.human.hand.append(Cards.by_id("w_falle")) + g.human.hand.append(Cards.by_id("w_reiter")) + for i in 2: + g.opponents[0].hand.append(Cards.by_id("w_reiter")) func condition(kind: String) -> bool: @@ -86,7 +88,7 @@ func next() -> bool: func text() -> String: - return "%d/%d %s" % [step + 1, steps().size(), steps()[step][0]] + return "%d/%d %s" % [step + 1, steps().size(), screen.tr(steps()[step][0])] func finished() -> bool: diff --git a/scripts/ui/card_view.gd b/scripts/ui/card_view.gd index 37fb056..3a447ee 100644 --- a/scripts/ui/card_view.gd +++ b/scripts/ui/card_view.gd @@ -46,17 +46,22 @@ func _draw() -> void: if mid: var sub: String = (card.combo() + " · " + card.kind) if card.is_combo() else card.kind 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 + var tsize := Style.fs(f_text) + var text_w: float = size.x - 20 + var needed: float = Style.FONT.get_multiline_string_size(card.text(), HORIZONTAL_ALIGNMENT_LEFT, text_w, tsize).y + 8 if card.text() != "" else size.y * 0.42 + var box_bottom: float = size.y - (28 if big else 22) + var box_top: float = maxf(size.y * 0.30, box_bottom - maxf(needed, size.y * 0.42)) + var box := Rect2(6, box_top, size.x - 12, box_bottom - box_top) # Bild var band_top: float = 2 * r + 4 + (f_text + 6 if mid else 0) - var band_bottom: float = size.y * 0.42 - 4 + var band_bottom: float = box_top - 4 if band_bottom - band_top > 14: 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) - # Textfeld - var box := Rect2(6, size.y * 0.42, size.x - 12, size.y * 0.42) draw_rect(box, Color(0.06, 0.06, 0.09, 0.67)) - draw_multiline_string(Style.FONT, Vector2(box.position.x + 4, box.position.y + 4 + Style.FONT.get_ascent(f_text)), - card.text(), HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, f_text, 6 if big else 4, Style.TEXT) + 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) # Werte if card.is_creature(): Style.text(self, str(card.attack), f_stat, Style.YELLOW, Vector2(10, size.y - 6), "bottomleft", true) diff --git a/scripts/ui/main.gd b/scripts/ui/main.gd index 4a0f454..922fb50 100644 --- a/scripts/ui/main.gd +++ b/scripts/ui/main.gd @@ -54,6 +54,41 @@ func show_screen(s: Control) -> void: s.app = self s.set_anchors_preset(Control.PRESET_FULL_RECT) add_child(s) + if pad_mode: + focus_first_button.call_deferred() + + +var pad_mode := false # zuletzt Gamepad benutzt: Fokusrahmen sichtbar, Steuerkreuz wechselt Knöpfe + + +## Gamepad in Menüs: erster Tastendruck setzt den Fokus, Maus nimmt ihn wieder weg. +func _input(event: InputEvent) -> void: + if event is InputEventJoypadButton or (event is InputEventJoypadMotion and absf(event.axis_value) > 0.5): + if not pad_mode: + pad_mode = true + if get_viewport().gui_get_focus_owner() == null: + focus_first_button() + elif event is InputEventMouseMotion and pad_mode: + pad_mode = false + get_viewport().gui_release_focus() + + +func focus_first_button() -> void: + if screen == null or screen is BattleScreen: + return + var b := _first_button(screen) + if b != null: + b.grab_focus() + + +func _first_button(node: Node) -> Button: + for c in node.get_children(): + if c is Button and not c.disabled and c.is_visible_in_tree(): + return c + var found := _first_button(c) + if found != null: + return found + return null func save() -> void: diff --git a/scripts/ui/opponent_select_screen.gd b/scripts/ui/opponent_select_screen.gd index e4069ce..480a6ab 100644 --- a/scripts/ui/opponent_select_screen.gd +++ b/scripts/ui/opponent_select_screen.gd @@ -34,7 +34,7 @@ func _ready() -> void: var r := Style.row(col, 16) r.alignment = BoxContainer.ALIGNMENT_CENTER Style.cbutton(r, "%d Gegner" % n, Vector2(150, 46), func(): start(n), Color("285a3c")) - var info := ("Duell" if n == 1 else "%d am Tisch" % (n + 1)) + " · %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) preview_labels[n] = Style.clabel(r, "", 13, Style.TEXT, "left", false, 600) Style.spacer(col, 4) @@ -82,8 +82,8 @@ func refresh(reroll: bool = false) -> void: rating_label.text = tr("Deine Deckstärke %.1f · Gegnerdecks %.1f · %s · Münzen x%.2f · KI: %s") % [ p.deck_rating(), p.target_rating(), arche, d.reward, tr("Vorausschau") if d.smart else tr("einfach")] var tip := p.suggest_difficulty() - tip_label.text = (tr("Tipp: %s") % (("%d Siege in Folge, versuche %s" % [p.records.streak, Profile.DIFFICULTIES[tip].name]) if p.records.streak >= 3 - else ("%d Niederlagen in Folge, versuche %s" % [p.records.loss_streak, Profile.DIFFICULTIES[tip].name]))) if tip != "" else "" + 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() diff --git a/scripts/ui/style.gd b/scripts/ui/style.gd index b6c2e39..fb4093a 100644 --- a/scripts/ui/style.gd +++ b/scripts/ui/style.gd @@ -48,7 +48,7 @@ static func make_theme() -> Theme: t.set_stylebox("hover", "Button", box(lighter(PANEL_LIGHT), 8, MUTED, 2)) t.set_stylebox("pressed", "Button", box(darker(PANEL_LIGHT), 8, MUTED, 2)) t.set_stylebox("disabled", "Button", box(darker(PANEL, 0.9), 8, PANEL, 2)) - t.set_stylebox("focus", "Button", StyleBoxEmpty.new()) + t.set_stylebox("focus", "Button", box(Color(0, 0, 0, 0), 8, GOLD, 3)) for name in ["font_color", "font_hover_color", "font_pressed_color", "font_focus_color", "font_hover_pressed_color"]: t.set_color(name, "Button", TEXT) t.set_color("font_disabled_color", "Button", MUTED) @@ -259,7 +259,7 @@ static func rules_text() -> String: "Kombokarten sind seltener und stärker, Dreifach-Kombos sind mythisch. Ein Deck darf höchstens 2 Elemente enthalten.", Cards.RESONANCE_TEXT, "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 alle gegnerischen 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.", "", "Schlüsselwörter:", diff --git a/tests/lint.gd b/tests/lint.gd new file mode 100644 index 0000000..db85a3b --- /dev/null +++ b/tests/lint.gd @@ -0,0 +1,29 @@ +extends SceneTree +## Statische Prüfung: lädt jedes Skript; Parse- oder Kompilierfehler lassen den Lauf scheitern. +## godot --headless --path . -s tests/lint.gd + + +func scan(dir: String, out: Array) -> void: + var d := DirAccess.open(dir) + if d == null: + return + for sub in d.get_directories(): + if not sub.begins_with("."): + scan(dir.path_join(sub), out) + for f in d.get_files(): + if f.ends_with(".gd"): + out.append(dir.path_join(f)) + + +func _initialize() -> void: + var files := [] + for dir in ["res://scripts", "res://tests"]: + scan(dir, files) + var bad := 0 + for path in files: + var script = load(path) + if script == null or not (script is GDScript): + printerr("FEHLER: %s lädt nicht" % path) + bad += 1 + print("%d Skripte geprüft, %d fehlerhaft" % [files.size(), bad]) + quit(1 if bad > 0 else 0) diff --git a/tests/lint.gd.uid b/tests/lint.gd.uid new file mode 100644 index 0000000..dba19ff --- /dev/null +++ b/tests/lint.gd.uid @@ -0,0 +1 @@ +uid://c65qyffqln67m diff --git a/tests/run_headless.sh b/tests/run_headless.sh index ba4be31..109d942 100755 --- a/tests/run_headless.sh +++ b/tests/run_headless.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash -# Führt Import und alle Headless-Prüfungen aus. Aufruf: tests/run_headless.sh [godot-binary] +# Führt Import, statische Prüfung und alle Headless-Prüfungen aus. Aufruf: tests/run_headless.sh [godot-binary] set -euo pipefail GODOT="${1:-godot}" cd "$(dirname "$0")/.." "$GODOT" --headless --path . --import > /dev/null 2>&1 || true +"$GODOT" --headless --path . -s tests/lint.gd 2>&1 | grep -E "FEHLER|Skripte" || true +"$GODOT" --headless --path . -s tests/lint.gd > /dev/null 2>&1 "$GODOT" --headless --path . -s tests/test_runner.gd 2>&1 | tee /tmp/dinger_tests.log | grep -E "FEHLER|Prüfungen|SCRIPT ERROR" || true grep -q " 0 fehlgeschlagen" /tmp/dinger_tests.log diff --git a/tests/strength_probe.gd b/tests/strength_probe.gd index 92bfc05..ed39896 100644 --- a/tests/strength_probe.gd +++ b/tests/strength_probe.gd @@ -34,4 +34,8 @@ func _initialize() -> void: wins += 1 results["+".join(a)] = wins * 100 / n print("%-14s %3d%%" % ["+".join(a), wins * 100 / n]) + var f := FileAccess.open(DeckGen.ARCHETYPE_PATH, FileAccess.WRITE) + f.store_string(JSON.stringify(results, "\t")) + f.close() + print("nach %s geschrieben" % DeckGen.ARCHETYPE_PATH) quit() diff --git a/tests/test_runner.gd b/tests/test_runner.gd index d2788a0..4bae64f 100644 --- a/tests/test_runner.gd +++ b/tests/test_runner.gd @@ -111,6 +111,7 @@ func _initialize() -> void: test_kurve_statistik_vorschlag_verbesserung() test_wasser_antworten() test_ki_zeitbudget() + test_uebersetzung_vollstaendig() print("%d Prüfungen bestanden, %d fehlgeschlagen" % [passed, failed]) quit(1 if failed > 0 else 0) @@ -532,15 +533,16 @@ func test_feldkombos() -> void: g.play_card(h, C("w_reiter")) check(strong.frozen and not weak.frozen, "Dampf friert das stärkste Wesen") check(g.log.any(func(l): return l.begins_with("Feldkombo Dampf")), "Feldkombo im Verlauf") - # Lava: 1 Schaden an alle gegnerischen Wesen + # Lava: 1 Schaden an das stärkste gegnerische Wesen s = setup(["e_golem"]) g = s[0] h = s[1] o = s[2][0] put(h, "f_wolf") var k := put(o, "f_kobold") + var big := put(o, "e_troll") g.play_card(h, C("e_golem")) - check(not (k in o.board), "Lava tötet den 2/1 Kobold") + check(k in o.board and big.health == 5, "Lava trifft nur das stärkste Wesen") # Blitz: 2 Schaden an den schwächsten Gegner s = setup(["l_wind"], [], 2) g = s[0] @@ -1119,7 +1121,7 @@ func test_neue_fallen() -> void: g.end_turn() g.play_card(o, C("e_troll")) check(o.board.size() == 1 and o.board[0].frozen and h.traps.size() == 1, "Sandfalle friert ein, eine Falle je Auslöser") - g.play_card(o, C("w_reiter")) # Wasser neben Erde: Schlamm heilt nur, der Windgeist überlebt + g.play_card(o, C("w_reiter")) # Wasser neben Erde: Schlamm heilt nur check(h.board.size() == 1 and h.board[0].name == "Windgeist", "Spiegelfalle beschwört Windgeist beim zweiten Wesen") check(h.traps.is_empty(), "beide Fallen verbraucht") # Nebelfalle bricht ab und zieht, Dornenfalle verletzt und heilt @@ -1240,7 +1242,7 @@ func test_deckstaerke_und_schwierigkeit() -> void: func test_starter_gegen_passende_gegner() -> void: check(Game.archetypes("leicht").size() == 5 and Game.archetypes("schwer").size() == 5 and Game.archetypes().size() == 10, "Archetypen geteilt") - var table: Dictionary = DeckGen.ARCHETYPE_STRENGTH + var table: Dictionary = DeckGen.strength() var keys: Array = table.keys() keys.sort_custom(func(a, b): return table[a] < table[b]) check(keys[0] in Game.archetypes("leicht") and keys[-1] in Game.archetypes("schwer"), "schwächster Archetyp in Leicht, stärkster in Schwer") @@ -1358,3 +1360,37 @@ func test_ki_zeitbudget() -> void: check(Cards.by_id("wl_gischt").keywords == ["Flink"] and Cards.by_id("wl_gischt").combo() == "Sturm", "Gischtläufer für Sturm") var v := Profile.from_dict(JSON.parse_string(JSON.stringify(Profile.starter().to_dict()))) check(v.settings.has("big_font") and not v.tutorial2_done and v.records.margins is Array, "Version 5 mit neuen Feldern") + + +## Jeder tr("...")-Text in der Oberfläche und jeder Anleitungsschritt hat einen englischen Eintrag. +func test_uebersetzung_vollstaendig() -> void: + var keys := {} + var csv := FileAccess.get_file_as_string("res://translations/ui.csv") + for line in csv.split("\n"): + if line.is_empty() or line.begins_with("keys,"): + continue + var key := line + if line.begins_with("\""): + key = line.substr(1, line.find("\",", 1) - 1) + else: + key = line.substr(0, line.find(",")) + keys[key] = true + var missing := [] + var d := DirAccess.open("res://scripts/ui") + d.list_dir_begin() + var name := d.get_next() + while name != "": + if name.ends_with(".gd"): + var src := FileAccess.get_file_as_string("res://scripts/ui/" + name) + var rx := RegEx.new() + rx.compile("tr\\(\"([^\"]+)\"\\)") + for m in rx.search_all(src): + var k: String = m.get_string(1) + if not keys.has(k) and not (k in missing): + missing.append(k) + name = d.get_next() + for stepset in [BattleTutorial.STEPS1, BattleTutorial.STEPS2]: + for st in stepset: + if not keys.has(st[0]): + missing.append(st[0]) + check(missing.is_empty(), "fehlende Übersetzungen: %s" % [missing]) diff --git a/tests/ui_test.gd b/tests/ui_test.gd index a9e5aa0..18bafa0 100644 --- a/tests/ui_test.gd +++ b/tests/ui_test.gd @@ -162,7 +162,7 @@ func _ready() -> void: add_child(main) await frames(3) var fast := OS.get_environment("DINGER_FAST") != "" - var suite := [test_menu_und_regeln, 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] if not fast: suite += [test_hotseat, test_netzwerk, test_kampf.bind(2, 3), test_kampf_abbrechen] @@ -513,6 +513,34 @@ func test_werkstatt() -> void: check(screen() is MenuScreen, "zurück im Menü") +func test_gamepad_menue() -> void: + check(get_viewport().gui_get_focus_owner() == null, "ohne Gamepad kein Fokus") + await pad(JOY_BUTTON_DPAD_DOWN) + var f := get_viewport().gui_get_focus_owner() + check(f is Button and main.pad_mode, "Gamepad setzt den Fokus auf einen Knopf") + var down := InputEventJoypadButton.new() + down.button_index = JOY_BUTTON_DPAD_DOWN + down.pressed = true + Input.parse_input_event(down) + await frames(2) + var f2 := get_viewport().gui_get_focus_owner() + check(f2 is Button and f2 != f, "Steuerkreuz wechselt den Knopf") + var target_text: String = f2.text + var accept := InputEventJoypadButton.new() + accept.button_index = JOY_BUTTON_A + accept.pressed = true + get_viewport().push_input(accept) + await frames(1) + var release := InputEventJoypadButton.new() + release.button_index = JOY_BUTTON_A + release.pressed = false + get_viewport().push_input(release) + await frames(3) + check(not (screen() is MenuScreen) or target_text == "Beenden", "A öffnet den fokussierten Bildschirm (%s)" % target_text) + await hover(main) + check(not main.pad_mode and get_viewport().gui_get_focus_owner() == null, "Maus nimmt den Fokus wieder") + + func test_anleitung2() -> void: check(main.profile.tutorial_done and not main.profile.tutorial2_done, "Teil 1 erledigt, Teil 2 offen") check(find_button("Anleitung 2: Fallen") != null, "Menü bietet Teil 2 an") @@ -535,6 +563,15 @@ func test_anleitung2() -> void: check(idx >= 0 and idx < 9, "Falle auf der Hand") await key(KEY_1 + idx) check(g.human.traps.size() == 1 and scr.tutorial_step == 2, "Falle gelegt, Schritt 3") + # Mara läuft in die Falle: zwei Züge abwarten + for i in 2: + await clear_mulligan(scr) + if g.current() == g.human and g.winner == "": + await key(KEY_SPACE) + await wait_for_human(scr) + if scr.tutorial_step >= 3: + break + check(scr.tutorial_step >= 3 and g.log.any(func(l): return l.begins_with("Falle von Du")), "Mara löst die Frostfalle aus, Schritt 4") await give_up() check(main.profile.tutorial2_done and find_button("Anleitung") != null, "Teil 2 erledigt") diff --git a/translations/ui.csv b/translations/ui.csv index 8c9d183..473779b 100644 --- a/translations/ui.csv +++ b/translations/ui.csv @@ -114,3 +114,22 @@ Große Schrift,Large text Verwerfen,Discard Weiter bearbeiten,Keep editing Ungespeicherte Änderungen,Unsaved changes +"Willkommen! Unten liegt deine Hand. Jede Karte kostet Energie (◆), du hast im ersten Zug 1. Spiele ein Wesen mit Kosten 1 per Klick.",Welcome! Your hand is at the bottom. Every card costs energy (◆); you have 1 on your first turn. Click a creature that costs 1 to play it. +Wesen greifen erst ab dem nächsten Zug an. Beende deinen Zug mit dem Knopf rechts oder der Leertaste.,Creatures attack from the next turn on. End your turn with the button on the right or the space bar. +Grüne Rahmen zeigen angriffsbereite Wesen. Klicke ein Wesen und dann Fenn oder eines seiner Wesen. Wächter müssen zuerst fallen.,"Green frames mark creatures ready to attack. Click a creature, then Fenn or one of his creatures. Guardians must fall first." +Resonanz: Wesen mit gleichem Element geben sich gegenseitig Angriff. Spiele ein zweites Feuer- oder Erde-Wesen.,Resonance: creatures sharing an element boost each other's attack. Play a second fire or earth creature. +Feldkombo: Ein Wesen neben einem eigenen Wesen mit anderem Element löst einen Effekt aus. Lava macht 1 Schaden an das stärkste gegnerische Wesen.,Field combo: a creature next to one of yours with a different element triggers an effect. Lava deals 1 damage to the strongest enemy creature. +"Zauber wirken sofort. Funke oder Feuerball treffen auch Fenn direkt. Ziele wählst du per Klick, Rechtsklick bricht ab.",Spells resolve immediately. Spark or Fireball can hit Fenn directly. Pick targets by clicking; right-click cancels. +Fenn verliert bei 0 Leben. Tausche im ersten Zug einer Partie schwache Starthände (Mulligan). Viel Erfolg!,Fenn loses at 0 life. Swap weak opening hands on your first turn (mulligan). Good luck! +"Mulligan: Im ersten Zug darfst du einmal Handkarten zurücklegen und neu ziehen. Entscheide in der Maske, was du behältst.",Mulligan: on your first turn you may put cards back once and redraw. Decide in the dialog what to keep. +"Fallen liegen verdeckt und lösen im gegnerischen Zug aus. Lege die Frostfalle, sobald du 2 Energie hast.",Traps lie face down and trigger during enemy turns. Set the Frost Trap once you have 2 energy. +"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. +"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 +%d am Tisch,%d at the table +%d Spieler am Tisch,%d players at the table +%d Münzen,%d coins +Stärke %.1f · %d Münzen,Strength %.1f · %d coins diff --git a/translations/ui.en.translation b/translations/ui.en.translation index 8e157412561ff9d1c14f546e6256081ff5cfeabf..a24c325385fff65c725ea3362af5631f87187cd5 100644 GIT binary patch delta 2060 zcmY*Z4NOy46n?KQr9ZUb&-zM%VuL{6(nhfxENvAC(u!ECEeMuGh>Qg&;tzJQ8mZu- z2y7Z(R@|bQ8|Z#OLD(W@b8)(Wy zd3`QDJ!Yf$RhXiH*6VME?NfM?^nOlz`#^yb7o%WMB&@ zIAbS~Sl%|Via1;#%K;mST}vcR*hcgnL^enHu>TFP+X06F z?*fhkP6GM>gMbTw5I_tITL2gX9exz_KeSggO&dYM1{E{>q{boziFLwZ{;Q{7ocRWb za?sx#@{9MRT;<$!YymeKC*hV$5){_PlAsET%>_>)w^p)?7I4=k zjG}7uM9NRliVOII+=Qe;@%+llA1;y_wOp3eluo*%9z98@!LOnO0HvtOkSFUs$jt@4 zj`K^KW?SzGT>EwpgcXCw4~Xxa4C9#nX=X7l{KLAz+Pykkp7JVonk(FS> z%rZvt@IlhxHPT%jHzE5R?&6r7QBYWrMy3q@&RKTO+yK_tP8h&Nuo|;cxb2r`G3SOi31dI7a_- z*FM%lxhqCCQz$D$xo_18q1N1WMq}=1gd3N1*hSmn=R2=da z;S##v?d$n6(T6>yS!6S@1E&|1ZpVjb`;k4v*`i^LJ!yt5(3}vLa%HD#+K`zqBip#G zTGLzhEX^YpJ9;PE@9spIeZs2&=+BSV9I$s+`f?4Dtphuz+HpYv&bLVhwLYUqjLiIm zc!sLe3t)O?-QgC4;rJS;*T2m&eJ7{aV7MvtqgA3!-Sy>g3nxUUrd^89E~QR8dOM8v z*^2J5BaCiHwfPleL(8uZ{Ky@!I*~V}7>?3;wiIbf7s06}wndjL1n-)?$g6Zt3z@(p z&}_(pFH1O@jPJZi)25Q~O`2GhxUw$|Exs->2%zK*>wOW_9Fy8cmcGrBRk28K%s3*k zXb=lmB^GN{8OdRRR~Mj=V}>`+u9bCH{A0I%UF1|Vo=M~lwdH7a>ehTeT7L6Sc^!R1 zQZ7DrasPVc{lkhIC~37aU2?;uTrJLLW0i0p#p<99k1Y&dfPMu1^N_8{!3T!g$V;@O zw&xAV_bE5%1h=!J zmQ;n@g(4$t>c%kd$Ghr=h2jEya5KH^f9v44?g`v&ud75`GfZu`R_W5Jus~nRD&Yc~ z@$55ucUMrTz9-Fkz@68rN|G*nqD2%BLpq$IFb#jFbZve-Q`g;; zzwnYBrOTjrx>hAV*&IzR_tmUyz>fX2QsgP)W8jdazW#YviRp{SsqRZA%P%Os5oMXf U^u@C{g?I2@2K-fUxm^YS0eTtz^Z)<= delta 772 zcmYk4O(;ZR6o&728QpQmy%VD;NfgZ_Ns@)t6eh|-qeLhvR+O-mpKK;d>c~PCl4LPT zjg6n3-<2pEidZR0l$7%Ko}8J!ulvmVp7)&ZyZ6q_MfG}kJ8Ta9YHP|cj9HM17Q`<> zSH!Q0ZbJ9K5jdBO_g%r?Xfu7uKq z3ixYa>OcfUC7f)5;pui!_NmpaWdoWY(>%{kNFCSmdH6-G=DB69Uj6rtod0g1NN7#Y z_E4XJbIGrucifK=Vc7OGV4I9?w=uT?>tXC};sO z&;k0uAjrb+Q4B`laT$1yftl^ L{`|zTwpjfi