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:
54
.github/workflows/tests.yml
vendored
54
.github/workflows/tests.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
12
data/archetypen.json
Normal 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
|
||||||
|
}
|
||||||
@@ -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":
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
29
tests/lint.gd
Normal 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
1
tests/lint.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c65qyffqln67m
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
|
Binary file not shown.
Reference in New Issue
Block a user