Lava abgeschwächt, Archetypen aus Datei mit CI-Messlauf, Kartentext folgt Schriftgröße, Gamepad in Menüs, Anleitung 2 mit sicherem Angriff, Übersetzungsprüfung, Lint und gestaffelte CI

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
Team3
2026-09-16 17:02:59 +02:00
parent f68ac07550
commit 77d1b318d6
19 changed files with 285 additions and 36 deletions

View File

@@ -1,6 +1,11 @@
name: Tests name: Tests
on: [push, pull_request] on:
push:
pull_request:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
jobs: jobs:
headless: headless:
@@ -13,10 +18,11 @@ jobs:
unzip -q godot.zip unzip -q godot.zip
mv Godot_v4.7.2-stable_linux.x86_64 godot mv Godot_v4.7.2-stable_linux.x86_64 godot
chmod +x godot chmod +x godot
- name: Tests - name: Statische Prüfung und Tests
run: ./tests/run_headless.sh ./godot run: ./tests/run_headless.sh ./godot
fenster: fenster-schnell:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -29,7 +35,47 @@ 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 (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 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
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

View File

@@ -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 - 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) - 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 - 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
`tests/run_headless.sh` führt Import und Headless-Prüfungen aus. `.github/workflows/tests.yml` läuft beides in CI: `tests/run_headless.sh` führt Import, statische Prüfung (`tests/lint.gd`) und Headless-Prüfungen aus. `.github/workflows/tests.yml`:
die Headless-Prüfungen direkt, den Fenster-Test unter Xvfb (lokal ohne Xvfb 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 Hotseat, Netzwerk und lange 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.

12
data/archetypen.json Normal file
View File

@@ -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
}

View File

@@ -29,7 +29,7 @@ const COMBOS := {
# Feldkombos: entstehen, wenn ein Wesen neben einem Wesen mit anderem Element ausgespielt wird. # Feldkombos: entstehen, wenn ein Wesen neben einem Wesen mit anderem Element ausgespielt wird.
const FIELD_COMBO_TEXT := { const FIELD_COMBO_TEXT := {
"Dampf": "Friere das stärkste gegnerische Wesen ein", "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", "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",
@@ -74,7 +74,7 @@ static func field_combo_effect(name: String) -> Effect:
"Dampf": "Dampf":
return Effect.new("freeze", "strongest_enemy_creature") return Effect.new("freeze", "strongest_enemy_creature")
"Lava": "Lava":
return Effect.new("damage", "all_enemy_creatures", 1) return Effect.new("damage", "strongest_enemy_creature", 1)
"Blitz": "Blitz":
return Effect.new("damage", "weakest_enemy_hero", 2) return Effect.new("damage", "weakest_enemy_hero", 2)
"Schlamm": "Schlamm":

View File

@@ -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). # Gemessene Stärke der Elementkombinationen (Siegquote gegen das Feld, KI gegen KI, 60 Partien je Archetyp, tests/strength_probe.gd).
const ARCHETYPE_STRENGTH := { const ARCHETYPE_STRENGTH := {
"Feuer": 38, "Wasser": 38, "Erde": 55, "Luft": 55, "Feuer": 36, "Wasser": 40, "Erde": 55, "Luft": 55,
"Feuer+Wasser": 45, "Feuer+Erde": 78, "Feuer+Luft": 46, "Feuer+Wasser": 50, "Feuer+Erde": 65, "Feuer+Luft": 46,
"Wasser+Erde": 50, "Wasser+Luft": 45, "Erde+Luft": 78, "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. ## Elementkombinationen einer Stufe: "leicht" die schwächere Hälfte, "schwer" die stärkere, sonst alle.
static func archetypes(tier: String = "") -> Array: static func archetypes(tier: String = "") -> Array:
var keys: Array = ARCHETYPE_STRENGTH.keys() var table := strength()
keys.sort_custom(func(a, b): return ARCHETYPE_STRENGTH[a] < ARCHETYPE_STRENGTH[b]) var keys: Array = table.keys()
keys.sort_custom(func(a, b): return table[a] < table[b])
var half: int = keys.size() / 2 var half: int = keys.size() / 2
match tier: match tier:
"leicht": "leicht":

View File

@@ -196,7 +196,7 @@ func _ready() -> void:
var opps := Campaign.opponents(guide.stage_index(), r, Game.deck_rating(app.profile.deck_cards())) var opps := Campaign.opponents(guide.stage_index(), r, Game.deck_rating(app.profile.deck_cards()))
opps[0].perks = [] opps[0].perks = []
game = Game.new_battle_with(app.profile.deck_cards(), opps, r, true) 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(): elif not challenge.is_empty():
var rules: Dictionary = challenge.rules.duplicate() var rules: Dictionary = challenge.rules.duplicate()
rules["target_rating"] = Game.deck_rating(app.profile.deck_cards()) rules["target_rating"] = Game.deck_rating(app.profile.deck_cards())

View File

@@ -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"], ["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"], ["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"], ["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"], ["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!", ""], ["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 return 0 if part == 1 else 1
## Teil 2 bekommt die Lehrkarten auf die Hand. ## Teil 2: Lehrkarten auf die eigene Hand; Mara bekommt Wesen, damit sie sicher in die Falle läuft.
func inject_cards(hand: Array) -> void: func inject_cards(g: Game) -> void:
if part == 2: if part == 2:
hand.append(Cards.by_id("w_falle")) g.human.hand.append(Cards.by_id("w_falle"))
hand.append(Cards.by_id("w_reiter")) 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: func condition(kind: String) -> bool:
@@ -86,7 +88,7 @@ func next() -> bool:
func text() -> String: 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: func finished() -> bool:

View File

@@ -46,17 +46,22 @@ func _draw() -> void:
if mid: if mid:
var sub: String = (card.combo() + " · " + card.kind) if card.is_combo() else card.kind 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") 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 # Bild
var band_top: float = 2 * r + 4 + (f_text + 6 if mid else 0) 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: if band_bottom - band_top > 14:
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)
# 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_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)), 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, f_text, 6 if big else 4, Style.TEXT) card.text(), HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, tsize, -1, 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)

View File

@@ -54,6 +54,41 @@ func show_screen(s: Control) -> void:
s.app = self s.app = self
s.set_anchors_preset(Control.PRESET_FULL_RECT) s.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(s) 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: func save() -> void:

View File

@@ -34,7 +34,7 @@ func _ready() -> void:
var r := Style.row(col, 16) var r := Style.row(col, 16)
r.alignment = BoxContainer.ALIGNMENT_CENTER r.alignment = BoxContainer.ALIGNMENT_CENTER
Style.cbutton(r, "%d Gegner" % n, Vector2(150, 46), func(): start(n), Color("285a3c")) 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) 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)
Style.spacer(col, 4) 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") % [ 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")] p.deck_rating(), p.target_rating(), arche, d.reward, tr("Vorausschau") if d.smart else tr("einfach")]
var tip := p.suggest_difficulty() 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 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 ("%d Niederlagen in Folge, versuche %s" % [p.records.loss_streak, Profile.DIFFICULTIES[tip].name]))) if tip != "" else "" else (tr("%d Niederlagen in Folge, versuche %s") % [p.records.loss_streak, tr(Profile.DIFFICULTIES[tip].name)]))) if tip != "" else ""
b_tip.visible = tip != "" b_tip.visible = tip != ""
if reroll: if reroll:
var r := Game.make_rng() var r := Game.make_rng()

View File

@@ -48,7 +48,7 @@ static func make_theme() -> Theme:
t.set_stylebox("hover", "Button", box(lighter(PANEL_LIGHT), 8, MUTED, 2)) 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("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("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"]: 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(name, "Button", TEXT)
t.set_color("font_disabled_color", "Button", MUTED) 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.", "Kombokarten sind seltener und stärker, Dreifach-Kombos sind mythisch. Ein Deck darf höchstens 2 Elemente enthalten.",
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 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.", " an den schwächsten Gegner, Schlamm heilt dich um 3, Sturm zieht 1 Karte, Sand gibt dem neuen Wesen Schild.",
"", "",
"Schlüsselwörter:", "Schlüsselwörter:",

29
tests/lint.gd Normal file
View File

@@ -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)

1
tests/lint.gd.uid Normal file
View File

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

View File

@@ -1,8 +1,10 @@
#!/usr/bin/env bash #!/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 set -euo pipefail
GODOT="${1:-godot}" GODOT="${1:-godot}"
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
"$GODOT" --headless --path . --import > /dev/null 2>&1 || true "$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 "$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 grep -q " 0 fehlgeschlagen" /tmp/dinger_tests.log

View File

@@ -34,4 +34,8 @@ func _initialize() -> void:
wins += 1 wins += 1
results["+".join(a)] = wins * 100 / n results["+".join(a)] = wins * 100 / n
print("%-14s %3d%%" % ["+".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() quit()

View File

@@ -111,6 +111,7 @@ func _initialize() -> void:
test_kurve_statistik_vorschlag_verbesserung() test_kurve_statistik_vorschlag_verbesserung()
test_wasser_antworten() test_wasser_antworten()
test_ki_zeitbudget() test_ki_zeitbudget()
test_uebersetzung_vollstaendig()
print("%d Prüfungen bestanden, %d fehlgeschlagen" % [passed, failed]) print("%d Prüfungen bestanden, %d fehlgeschlagen" % [passed, failed])
quit(1 if failed > 0 else 0) quit(1 if failed > 0 else 0)
@@ -532,15 +533,16 @@ func test_feldkombos() -> void:
g.play_card(h, C("w_reiter")) g.play_card(h, C("w_reiter"))
check(strong.frozen and not weak.frozen, "Dampf friert das stärkste Wesen") 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") 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"]) s = setup(["e_golem"])
g = s[0] g = s[0]
h = s[1] h = s[1]
o = s[2][0] o = s[2][0]
put(h, "f_wolf") put(h, "f_wolf")
var k := put(o, "f_kobold") var k := put(o, "f_kobold")
var big := put(o, "e_troll")
g.play_card(h, C("e_golem")) 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 # Blitz: 2 Schaden an den schwächsten Gegner
s = setup(["l_wind"], [], 2) s = setup(["l_wind"], [], 2)
g = s[0] g = s[0]
@@ -1119,7 +1121,7 @@ func test_neue_fallen() -> void:
g.end_turn() g.end_turn()
g.play_card(o, C("e_troll")) 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") 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.board.size() == 1 and h.board[0].name == "Windgeist", "Spiegelfalle beschwört Windgeist beim zweiten Wesen")
check(h.traps.is_empty(), "beide Fallen verbraucht") check(h.traps.is_empty(), "beide Fallen verbraucht")
# Nebelfalle bricht ab und zieht, Dornenfalle verletzt und heilt # 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: 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") 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() var keys: Array = table.keys()
keys.sort_custom(func(a, b): return table[a] < table[b]) 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") 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") 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()))) 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") 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])

View File

@@ -162,7 +162,7 @@ func _ready() -> void:
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_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]
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_kampf.bind(2, 3), test_kampf_abbrechen]
@@ -513,6 +513,34 @@ func test_werkstatt() -> void:
check(screen() is MenuScreen, "zurück im Menü") 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: func test_anleitung2() -> void:
check(main.profile.tutorial_done and not main.profile.tutorial2_done, "Teil 1 erledigt, Teil 2 offen") 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") 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") check(idx >= 0 and idx < 9, "Falle auf der Hand")
await key(KEY_1 + idx) await key(KEY_1 + idx)
check(g.human.traps.size() == 1 and scr.tutorial_step == 2, "Falle gelegt, Schritt 3") 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() await give_up()
check(main.profile.tutorial2_done and find_button("Anleitung") != null, "Teil 2 erledigt") check(main.profile.tutorial2_done and find_button("Anleitung") != null, "Teil 2 erledigt")

View File

@@ -114,3 +114,22 @@ Große Schrift,Large text
Verwerfen,Discard Verwerfen,Discard
Weiter bearbeiten,Keep editing Weiter bearbeiten,Keep editing
Ungespeicherte Änderungen,Unsaved changes 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
1 keys en
114 Verwerfen Discard
115 Weiter bearbeiten Keep editing
116 Ungespeicherte Änderungen Unsaved changes
117 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.
118 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.
119 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.
120 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.
121 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.
122 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.
123 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!
124 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.
125 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.
126 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.
127 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.
128 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!
129 %d Siege in Folge, versuche %s %d wins in a row, try %s
130 %d Niederlagen in Folge, versuche %s %d losses in a row, try %s
131 Duell Duel
132 %d am Tisch %d at the table
133 %d Spieler am Tisch %d players at the table
134 %d Münzen %d coins
135 Stärke %.1f · %d Münzen Strength %.1f · %d coins

Binary file not shown.