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

@@ -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":

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).
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":

View File

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

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"],
["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:

View File

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

View File

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

View File

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

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("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:",