Files
cards/scripts/cards.gd
Team3 4c6bf96eeb Kampf fortsetzen, Gamepad im Deckbau, Deck-Codes, Kapitel 3, Elementbooster, englische Karten, Host-Wechsel und Zuschauer
- KI-Bewertung mit Bedrohung und Führenden-Bremse (62:38 gegen die alte im Duell, gleichauf zu viert)
- Balance: Sand +1/+1 statt Schild, Erde+Luft und Erde geschwächt, Wasser angehoben, Messlauf mit N=200 möglich
- Einzelspieler-Kämpfe werden laufend gespeichert (voller Spielstand, viewer -1) und im Menü fortgesetzt
- Karten, Deckzeilen und Kartenrücken fokussierbar: Gamepad in Deckbau, Werkstatt und Booster
- Kartennamen (name_en) und Kartentexte übersetzbar, Übersetzungstest deckt cards.gd mit ab
- Deck-Code DINGER1:id*anzahl mit Zwischenablage; acht neue Karten; Kampagne Kapitel 3; Elementbooster 120 Münzen
- Netzwerk: Nachfolge-Host mit vollem Spielstand, Neuverbindung der anderen, Zuschauer (Sicht -2); Bot kann hosten
- Große Schrift: Kartennamen, Kartentext, Knopfleisten, Gegnervorschau, Dialogtext
- Anleitung 1 garantiert ein 1-Energie-Wesen; Tests: DINGER_ONLY für einzelne Oberflächentests
- Oberflächentest auch auf Xephyr mit llvmpipe (wie Xvfb in der CI) grün

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
2026-09-16 18:50:09 +02:00

317 lines
11 KiB
GDScript

class_name Cards
## Elemente, Kombos und der Kartenpool.
const ELEMENTS: Array[String] = ["Feuer", "Wasser", "Erde", "Luft"]
const ELEMENT_COLORS := {
"Feuer": Color("be3c23"),
"Wasser": Color("2d64be"),
"Erde": Color("73552d"),
"Luft": Color("78aac8"),
}
# Jede Paarung ist eine Kombo, auch gleiche Elemente.
const COMBOS := {
"Feuer+Feuer": "Inferno",
"Wasser+Wasser": "Tiefsee",
"Erde+Erde": "Berg",
"Luft+Luft": "Orkan",
"Feuer+Wasser": "Dampf",
"Feuer+Erde": "Lava",
"Feuer+Luft": "Blitz",
"Wasser+Erde": "Schlamm",
"Wasser+Luft": "Sturm",
"Erde+Luft": "Sand",
# Dreifach-Kombos: die mythische Stufe
"Feuer+Feuer+Feuer": "Sonne",
"Wasser+Wasser+Wasser": "Ozean",
"Erde+Erde+Erde": "Gebirge",
"Luft+Luft+Luft": "Himmel",
}
# 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 das stärkste gegnerische Wesen",
"Blitz": "2 Schaden an den schwächsten Gegner",
"Schlamm": "Heile dich um 3",
"Sturm": "Ziehe 1 Karte",
"Sand": "Das neue Wesen erhält +1/+1",
}
const RESONANCE_TEXT := "Resonanz: Ein Wesen bekommt Angriff für eigene Wesen mit gleichem Element: +1, +2, +3, +3 bei ein bis vier Partnern."
const RARITIES: Array[String] = ["gewöhnlich", "selten", "episch", "legendär", "mythisch"]
const RARITY_COLORS := {
"gewöhnlich": Color("aaaaaa"),
"selten": Color("4682e6"),
"episch": Color("aa46dc"),
"legendär": Color("f0aa28"),
"mythisch": Color("ff5a8c"),
}
const KEYWORDS: Array[String] = ["Wächter", "Flink", "Schild", "Doppelschlag"]
const KEYWORD_HELP := {
"Wächter": "Muss zuerst angegriffen werden.",
"Flink": "Kann sofort angreifen.",
"Schild": "Blockt den nächsten Schaden.",
"Doppelschlag": "Greift zweimal pro Zug an.",
}
const CHOSEN_TARGETS: Array[String] = ["enemy_creature", "enemy_any", "friendly_creature"]
# Näherung der Kartenstärke: Seltenheit plus Zuschläge für Schlüsselwörter und Effekte.
const RARITY_POINTS := {"gewöhnlich": 1.0, "selten": 2.0, "episch": 3.0, "legendär": 4.0, "mythisch": 5.0}
const DATA_PATH := "res://data/karten.json"
static func combo_key(elements: Array) -> String:
var sorted := elements.duplicate()
sorted.sort_custom(func(a, b): return ELEMENTS.find(a) < ELEMENTS.find(b))
return "+".join(sorted)
static func combo_name(elements: Array) -> String:
if elements.size() == 1:
return elements[0]
return COMBOS[combo_key(elements)]
static func field_combo_effect(name: String) -> Effect:
match name:
"Dampf":
return Effect.new("freeze", "strongest_enemy_creature")
"Lava":
return Effect.new("damage", "strongest_enemy_creature", 1)
"Blitz":
return Effect.new("damage", "weakest_enemy_hero", 2)
"Schlamm":
return Effect.new("heal", "self_hero", 3)
"Sturm":
return Effect.new("draw", "none", 1)
"Sand":
return Effect.new("buff", "self_creature", 1, 1)
return null
const TRIGGER_TEXT := {
"attacked": "Wenn ein gegnerisches Wesen dich oder eines deiner Wesen angreift",
"creature_played": "Wenn ein Gegner ein Wesen ausspielt",
}
class Effect extends RefCounted:
var kind: String # damage, heal, draw, freeze, bounce, buff, shield, summon, cancel
var target: String # siehe CHOSEN_TARGETS und Game.resolve()
var amount: int
var amount2: int
var card_id: String # für summon
func _init(k: String, t: String, a: int = 0, a2: int = 0, cid: String = "") -> void:
kind = k
target = t
amount = a
amount2 = a2
card_id = cid
func to_dict() -> Dictionary:
var d := {"kind": kind, "target": target}
if amount != 0:
d["amount"] = amount
if amount2 != 0:
d["amount2"] = amount2
if card_id != "":
d["card_id"] = card_id
return d
static func from_dict(d: Dictionary) -> Effect:
return Effect.new(d.kind, d.get("target", "none"), int(d.get("amount", 0)), int(d.get("amount2", 0)), str(d.get("card_id", "")))
## Kartentext in der aktuellen Sprache. trigger: bei Fallen der Auslöser (Wortwahl).
func text(trigger: String = "") -> String:
var n := str(amount)
if target == "trigger_source":
var played := trigger == "creature_played"
match kind:
"damage":
if played:
return tr("%s Schaden an dieses Wesen") % n
return tr("%s Schaden an den Angreifer") % n
"freeze":
if played:
return tr("Es wird eingefroren")
return tr("Der Angreifer wird eingefroren")
"bounce":
return tr("Es geht zurück auf die Hand")
match kind:
"cancel":
return tr("Der Angriff verpufft")
"summon":
return tr("Beschwöre einen %s") % Cards.by_id(card_id).title()
"damage":
match target:
"enemy_creature":
return tr("%s Schaden an ein gegnerisches Wesen") % n
"enemy_any":
return tr("%s Schaden an ein Ziel") % n
"all_enemy_creatures":
return tr("%s Schaden an alle gegnerischen Wesen") % n
"all_enemy_heroes":
return tr("%s Schaden an alle Gegner") % n
"all_enemies":
return tr("%s Schaden an alle Gegner und ihre Wesen") % n
"heal":
return tr("Heile dich um %s") % n
"draw":
if amount == 1:
return tr("Ziehe 1 Karte")
return tr("Ziehe %s Karten") % n
"freeze":
if target == "enemy_creature":
return tr("Friere ein gegnerisches Wesen ein")
if target == "strongest_enemy_creature":
return tr("Friere das stärkste gegnerische Wesen ein")
return tr("Friere alle gegnerischen Wesen ein")
"bounce":
if target == "enemy_creature":
return tr("Ein gegnerisches Wesen zurück auf die Hand")
return tr("Alle gegnerischen Wesen zurück auf die Hand")
"buff":
var b := "+%d/+%d" % [amount, amount2]
if target == "friendly_creature":
return tr("Ein befreundetes Wesen erhält %s") % b
return tr("Alle befreundeten Wesen erhalten %s") % b
"shield":
if target == "friendly_creature":
return tr("Ein befreundetes Wesen erhält Schild")
if target == "self_creature":
return tr("Erhält Schild")
return tr("Alle befreundeten Wesen erhalten Schild")
return ""
class Card extends RefCounted:
var id: String
var name: String # deutscher Name (auch im Verlauf und in Tests)
var name_en := "" # englischer Name
var elements: Array
var cost: int
var rarity: String
var kind: String # "Wesen", "Zauber" oder "Falle"
var attack: int = 0
var health: int = 0
var keywords: Array = []
var effects: Array = []
var trigger: String = "" # nur Fallen: attacked, creature_played
func is_creature() -> bool:
return kind == "Wesen"
func is_trap() -> bool:
return kind == "Falle"
func to_dict() -> Dictionary:
var d := {"id": id, "name": name, "name_en": name_en, "elements": elements, "cost": cost, "rarity": rarity, "kind": kind}
if is_creature():
d["attack"] = attack
d["health"] = health
if not keywords.is_empty():
d["keywords"] = keywords
if not effects.is_empty():
d["effects"] = effects.map(func(e): return e.to_dict())
if trigger != "":
d["trigger"] = trigger
return d
static func from_dict(d: Dictionary) -> Card:
var c := Card.new()
c.id = d.id
c.name = d.name
c.name_en = str(d.get("name_en", ""))
c.elements = Array(d.elements)
c.cost = int(d.cost)
c.rarity = d.rarity
c.kind = d.kind
c.attack = int(d.get("attack", 0))
c.health = int(d.get("health", 0))
c.keywords = Array(d.get("keywords", []))
for e in d.get("effects", []):
c.effects.append(Effect.from_dict(e))
c.trigger = str(d.get("trigger", ""))
return c
## Anzeigename in der aktuellen Sprache.
func title() -> String:
if name_en != "" and TranslationServer.get_locale().begins_with("en"):
return name_en
return name
func is_combo() -> bool:
return elements.size() >= 2
func combo() -> String:
return Cards.combo_name(elements)
func target_type() -> String:
for fx in effects:
if fx.target in Cards.CHOSEN_TARGETS:
return fx.target
return ""
func text() -> String:
var parts: Array[String] = []
if not keywords.is_empty():
var kws: Array[String] = []
for k in Cards.KEYWORDS:
if k in keywords:
kws.append(tr(k))
parts.append(", ".join(kws) + ".")
if not effects.is_empty():
var fx: Array[String] = []
for f in effects:
fx.append(f.text(trigger))
var s := ", ".join(fx) + "."
if is_trap():
parts.append(tr("Falle. %s: %s") % [tr(Cards.TRIGGER_TEXT[trigger]), s])
elif is_creature():
parts.append(tr("Beim Ausspielen: %s") % s)
else:
parts.append(s)
return " ".join(parts)
static var _all: Array = []
static var _by_id: Dictionary = {}
static func all() -> Array:
if _all.is_empty():
_build()
return _all
static func by_id(id: String) -> Card:
if _all.is_empty():
_build()
return _by_id[id]
static func has_id(id) -> bool:
if _all.is_empty():
_build()
return _by_id.has(id)
static func card_value(c: Card) -> float:
return RARITY_POINTS[c.rarity] + 0.3 * c.keywords.size() + 0.3 * c.effects.size()
static func of_rarity(r: String) -> Array:
return all().filter(func(c): return c.rarity == r)
## Lädt den Kartenpool aus data/karten.json.
static func _build() -> void:
var json := JSON.new()
var ok := json.parse(FileAccess.get_file_as_string(DATA_PATH))
assert(ok == OK and json.data is Dictionary and json.data.has("karten"), "data/karten.json fehlt oder ist ungültig")
_all = []
_by_id = {}
for d in json.data.karten:
var c := Card.from_dict(d)
assert(not _by_id.has(c.id), "Karten-ID doppelt: " + c.id)
_by_id[c.id] = c
_all.append(c)