- 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
317 lines
11 KiB
GDScript
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)
|