class_name Cards ## Elemente, Kombonamen, Schlüsselwörter und der Kartenpool. const ELEMENTS: Array[String] = ["Feuer", "Wasser", "Erde", "Luft"] # Neutral ist kein Element: nie im Vorteil oder Nachteil, zählt nicht für Paare und Zähler. # Verschmolzen mit einem Element nimmt das Wesen im schlechten Matchup die neutrale Seite. const NEUTRAL := "Neutral" const NEUTRAL_TEXT := "Nie im Vorteil oder Nachteil" const ELEMENT_COLORS := { "Feuer": Color("be3c23"), "Wasser": Color("2d64be"), "Erde": Color("73552d"), "Luft": Color("78aac8"), "Neutral": Color("6e7382"), } # Jede Karte hat genau ein Element (oder ist neutral). Kombos sind die Namen verschmolzener Wesen. const COMBOS := { "Neutral+Neutral": "Veteran", "Feuer+Neutral": "Feuerritter", "Wasser+Neutral": "Wasserritter", "Erde+Neutral": "Erdritter", "Luft+Neutral": "Luftritter", "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", } # 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"} # Seltenheit ist reine Optik: jede Karte gibt es in jeder Stufe, die Werte bleiben gleich. # Stufen wie bei Yu-Gi-Oh: Common grau, Rare silber, Super Rare blau, Ultra Rare gold, # Ultimate Rare schillert durch alle Farben. const RARITIES: Array[String] = ["Common", "Rare", "Super Rare", "Ultra Rare", "Ultimate Rare"] const RARITY_COLORS := { "Common": Color("aaaaaa"), "Rare": Color("c8d0dc"), "Super Rare": Color("4682e6"), "Ultra Rare": Color("f0aa28"), "Ultimate Rare": Color("ff5a8c"), } # Eigenschaften haben Karten nicht von Haus aus; sie kommen allein vom Elementpaar der eigenen # Seite (ELEMENT_KEYWORD, siehe Game.Creature.has). const KEYWORDS: Array[String] = ["Spott", "Flink", "Regeneration", "Doppelschlag"] const KEYWORD_TEXT := { "Spott": "Muss zuerst angegriffen werden", "Flink": "Greift sofort an", "Regeneration": "Heilt sich am Zugende ganz", "Doppelschlag": "Greift zweimal je Zug", } const ELEMENT_KEYWORD := {"Erde": "Spott", "Luft": "Flink", "Wasser": "Regeneration", "Feuer": "Doppelschlag"} const DATA_PATH := "res://data/karten.json" ## Reihenfolge der Elemente in Namen und Darstellung: wie ELEMENTS, Neutral zuletzt. static func element_order(e: String) -> int: var i := ELEMENTS.find(e) return i if i >= 0 else ELEMENTS.size() static func combo_key(elements: Array) -> String: var sorted := elements.duplicate() sorted.sort_custom(func(a, b): return element_order(a) < element_order(b)) 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.get(combo_key(elements), "Chimäre") ## Elementvorteil aus Sicht von own: +1 im Vorteil, -1 im Nachteil, sonst 0. ## Hat ein Wesen mehrere Elemente, kämpft es mit dem, das gegen die Gegenseite am besten steht. static func matchup(own: Array, other: Array) -> int: var mine := best_element(own, other) var theirs := best_element(other, own) if mine == "" or theirs == "": return 0 return duel(mine, theirs) ## +1, wenn a b schlägt, -1, wenn b a schlägt, sonst 0. static func duel(a: String, b: String) -> int: if BEATS.get(a, "") == b: return 1 if BEATS.get(b, "") == a: return -1 return 0 ## Das Element aus own, das gegen die Elemente in other am besten steht. static func best_element(own: Array, other: Array) -> String: var best := "" var best_score := -9 for e in own: var score := 0 for o in other: score += duel(e, o) if score > best_score: best = e best_score = score return best ## Kosten einer Karte: zwei Drittel Angriff plus ein Drittel Leben, abgerundet, höchstens 10 (0/2 kostet nichts). static func cost_of(attack: int, health: int) -> int: return mini(10, (2 * attack + health) / 3) ## Wert eines verschmolzenen Wesens aus den Grundwerten der Eltern: die aufgerundete Hälfte der ## Summe plus eins. Ein starker und ein schwacher Partner ergeben also weniger als der starke. static func fusion_value(a: int, b: int) -> int: return ceili((a + b) / 2.0) + 1 ## 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]]) ## Verschmelzung zweier Karten: Elemente (sortiert) und Eigenschaften beider, Angriff und Leben ## nach fusion_value aus den Grundwerten, Kosten daraus wie bei jeder Karte; dann die Stufe level ## mit ihrem +1/+1 (ohne Angabe die höhere der Eltern). Das Bild trägt der stärkere Elternteil ## (Angriff plus Leben). static func fused(a: Card, b: Card, level: int = -1) -> Card: var attack := fusion_value(a.base_attack(), b.base_attack()) var health := fusion_value(a.base_health(), b.base_health()) var els := [] for e in a.elements + b.elements: if not (e in els): els.append(e) els.sort_custom(func(x, y): return element_order(x) < element_order(y)) var c := Card.new() c.id = "fus_%s_%s" % [a.id, b.id] c.name = fusion_name(els) c.elements = els c.attack = attack c.health = health c.cost = cost_of(attack, health) for k in KEYWORDS: if k in a.keywords or k in b.keywords: c.keywords.append(k) var strong := a if a.attack + a.health >= b.attack + b.health else b c.art = strong.art_card().id return c.leveled(maxi(level, maxi(maxi(a.look, b.look), 0))) ## Platzhalter für ein verdecktes gegnerisches Wesen: unbekanntes Element, geschätzte Werte. static func unknown() -> Card: var c := Card.new() c.id = "?" c.name = "Verdeckt" c.name_en = "Face-down" c.elements = [] c.attack = 2 c.health = 3 c.cost = cost_of(2, 3) return c 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 attack: int = 0 var health: int = 0 var cost: int = 0 # abgeleitet: (2 · Angriff + Leben) / 3, abgerundet, höchstens 10 (aus den Grundwerten) var keywords: Array = [] # feste Eigenschaften der Karte (Elementpaare geben ihre zur Laufzeit) var look := -1 # Seltenheitsstufe des Exemplars (gibt +1/+1 je Stufe); -1: Grundkarte aus dem Pool var art := "" # verschmolzen: Karte, deren Bild das Wesen trägt (der stärkere Elternteil) func to_dict() -> Dictionary: var d := {"id": id, "name": name, "name_en": name_en, "elements": elements, "attack": attack, "health": health} if not keywords.is_empty(): d["keywords"] = keywords if look >= 0: d["look"] = look if art != "": d["art"] = art if cost != Cards.cost_of(attack, health): d["cost"] = cost return d ## Wert der Karte: was sie über ihre Manakosten hinaus bringt, mal 100. Grundkarten liegen ## bei 100, jede Seltenheitsstufe gibt 100 dazu (Ultimate Rare 500); Fusionen aus ihren Werten. func value() -> int: return (Cards.cost_of(attack, health) - cost + 1) * 100 ## Grundwerte ohne den Stufenbonus. func base_attack() -> int: return attack - maxi(look, 0) func base_health() -> int: return health - maxi(look, 0) ## Stufe dieser Karte für einen Besitzer mit dieser Sammlung (Game.Player.looks). func rarity_in(looks: Dictionary) -> String: return Cards.RARITIES[look if look >= 0 else int(looks.get(id, 0))] 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.attack = int(d.get("attack", 0)) c.health = int(d.get("health", 0)) c.cost = int(d.get("cost", Cards.cost_of(c.attack, c.health))) c.keywords = Array(d.get("keywords", [])) c.look = int(d.get("look", -1)) c.art = str(d.get("art", "")) return c ## Karte, deren Bild gezeichnet wird: bei Verschmelzungen der stärkere Elternteil, sonst sie selbst. func art_card() -> Card: return Cards.by_id(art) if art != "" and Cards._by_id.has(art) else self ## Eigenes Exemplar (Hand, Brett): Schlüsselwörter gehören dem Exemplar, nicht dem Pool. func copy() -> Card: return Card.from_dict(to_dict()) ## Exemplar in einer Seltenheitsstufe: +1/+1 je Stufe, die Kosten bleiben die der Grundkarte. func leveled(level: int) -> Card: var c := copy() c.look = level c.attack += level c.health += level c.cost = cost 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 is_neutral() -> bool: return Cards.NEUTRAL in elements ## Kartentext: Neutral und die festen Eigenschaften. func text() -> String: var kws: Array[String] = [] if is_neutral(): kws.append(tr(Cards.NEUTRAL)) for k in Cards.KEYWORDS: if k in keywords: kws.append(tr(k)) if kws.is_empty(): return "" return ", ".join(kws) + "." 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) ## 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)