update
Some checks failed
Tests / headless (push) Has been cancelled
Tests / fenster-schnell (push) Has been cancelled
Tests / fenster-voll (push) Has been cancelled
Tests / balance (push) Has been cancelled

This commit is contained in:
Team3
2026-09-22 13:23:02 +02:00
parent 8944fdd017
commit dfcd3cb713
46 changed files with 1525 additions and 4141 deletions

View File

@@ -8,7 +8,8 @@ const ELEMENT_COLORS := {
"Erde": Color("73552d"),
"Luft": Color("78aac8"),
}
# Jede Paarung ist eine Kombo, auch gleiche Elemente.
# Jede Karte hat genau ein Element. Kombos entstehen erst im Spiel: beim Verschmelzen
# (der Name des neuen Wesens) und als Feldkombo zwischen benachbarten Elementen.
const COMBOS := {
"Feuer+Feuer": "Inferno",
"Wasser+Wasser": "Tiefsee",
@@ -20,11 +21,6 @@ const COMBOS := {
"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 := {
@@ -32,10 +28,21 @@ const FIELD_COMBO_TEXT := {
"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",
"Sturm": "Erhalte 1 Mana",
"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."
# Elementkreis: Wasser löscht Feuer, Feuer verbrennt Luft, Luft trägt Erde ab, Erde saugt Wasser auf.
const BEATS := {"Wasser": "Feuer", "Feuer": "Luft", "Luft": "Erde", "Erde": "Wasser"}
# Das Feld gehört allen am Tisch: jede gespielte Karte lädt ihre Elemente, bei FIELD_CHARGE kippt es.
const FIELD_CHARGE := 4
const FIELD_NAMES := {"Feuer": "Vulkan", "Wasser": "Flut", "Erde": "Fels", "Luft": "Wirbel"}
const FIELD_TEXT := {
"Feuer": "Feuerwesen haben +1 Angriff",
"Wasser": "Wasserwesen haben +1 Angriff",
"Erde": "Erdwesen haben +1 Angriff",
"Luft": "Luftwesen haben +1 Angriff",
}
# Seltenheit ist reine Optik: jede Karte gibt es in jeder Stufe, die Werte bleiben gleich.
const RARITIES: Array[String] = ["gewöhnlich", "selten", "episch", "legendär", "mythisch"]
const RARITY_COLORS := {
"gewöhnlich": Color("aaaaaa"),
@@ -45,15 +52,7 @@ const RARITY_COLORS := {
"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"
@@ -63,10 +62,54 @@ static func combo_key(elements: Array) -> String:
return "+".join(sorted)
## Name der Elementkombination eines verschmolzenen Wesens; drei oder vier Elemente sind eine Chimäre.
static func combo_name(elements: Array) -> String:
if elements.size() == 1:
return elements[0]
return COMBOS[combo_key(elements)]
return COMBOS.get(combo_key(elements), "Chimäre")
## Elementvorteil aus Sicht von own: +1 im Vorteil, -1 im Nachteil, sonst 0.
## Mehrfachelemente nehmen den für sie besseren Wert.
static func matchup(own: Array, other: Array) -> int:
for e in own:
if BEATS.get(e, "") in other:
return 1
for e in other:
if BEATS.get(e, "") in own:
return -1
return 0
## Mana für eine Verschmelzung: Durchschnitt der Kartenkosten, aufgerundet.
static func fusion_cost(a: Card, b: Card) -> int:
return ceili((a.cost + b.cost) / 2.0)
## Name des verschmolzenen Wesens: die Kombo seiner Elemente (ein Element zählt doppelt).
static func fusion_name(elements: Array) -> String:
return combo_name(elements if elements.size() >= 2 else [elements[0], elements[0]])
## Karte des verschmolzenen Wesens: Elemente und Schlüsselwörter beider Karten,
## Werte kommen vom Spiel (Durchschnitt plus 1, siehe Game.fuse).
static func fused(a: Card, b: Card, attack: int, health: int) -> Card:
var els := []
for e in a.elements + b.elements:
if not (e in els):
els.append(e)
els.sort_custom(func(x, y): return ELEMENTS.find(x) < ELEMENTS.find(y))
var c := Card.new()
c.id = "fus_%s_%s" % [a.id, b.id]
c.name = fusion_name(els)
c.elements = els
c.cost = fusion_cost(a, b)
c.attack = attack
c.health = health
for k in KEYWORDS:
if k in a.keywords or k in b.keywords:
c.keywords.append(k)
return c
static func field_combo_effect(name: String) -> Effect:
@@ -80,31 +123,23 @@ static func field_combo_effect(name: String) -> Effect:
"Schlamm":
return Effect.new("heal", "self_hero", 3)
"Sturm":
return Effect.new("draw", "none", 1)
return Effect.new("mana", "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 kind: String # damage, heal, mana, freeze, bounce, buff, shield
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:
func _init(k: String, t: String, a: int = 0, a2: int = 0) -> void:
kind = k
target = t
amount = a
amount2 = a2
card_id = cid
func to_dict() -> Dictionary:
var d := {"kind": kind, "target": target}
@@ -112,34 +147,15 @@ class Effect extends RefCounted:
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", "")))
return Effect.new(d.kind, d.get("target", "none"), int(d.get("amount", 0)), int(d.get("amount2", 0)))
## Kartentext in der aktuellen Sprache. trigger: bei Fallen der Auslöser (Wortwahl).
func text(trigger: String = "") -> String:
## Kartentext in der aktuellen Sprache.
func text() -> 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":
@@ -154,10 +170,10 @@ class Effect extends RefCounted:
return tr("%s Schaden an alle Gegner und ihre Wesen") % n
"heal":
return tr("Heile dich um %s") % n
"draw":
"mana":
if amount == 1:
return tr("Ziehe 1 Karte")
return tr("Ziehe %s Karten") % n
return tr("Erhalte 1 Mana")
return tr("Erhalte %s Mana") % n
"freeze":
if target == "enemy_creature":
return tr("Friere ein gegnerisches Wesen ein")
@@ -188,31 +204,18 @@ class Card extends RefCounted:
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
var d := {"id": id, "name": name, "name_en": name_en, "elements": elements, "cost": cost,
"attack": attack, "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:
@@ -222,14 +225,11 @@ class Card extends RefCounted:
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.
@@ -261,14 +261,8 @@ class Card extends RefCounted:
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)
fx.append(f.text())
parts.append(tr("Beim Ausspielen: %s") % (", ".join(fx) + "."))
return " ".join(parts)
@@ -294,14 +288,6 @@ static func has_id(id) -> bool:
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()