374 lines
14 KiB
GDScript
374 lines
14 KiB
GDScript
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
|
|
|
|
|
|
## Das stärkste Element einer Karte (für das Bild): das, das die meisten der anderen Elemente der
|
|
## Karte schlägt; bei Gleichstand entscheidet seed (fest je Karte, damit das Bild nicht wechselt).
|
|
static func strongest_element(elements: Array, seed: int) -> String:
|
|
var best: Array = []
|
|
var best_score := -99
|
|
for e in elements:
|
|
var score := 0
|
|
for o in elements:
|
|
score += duel(e, o)
|
|
if score > best_score:
|
|
best = [e]
|
|
best_score = score
|
|
elif score == best_score:
|
|
best.append(e)
|
|
return best[absi(seed) % best.size()]
|
|
|
|
|
|
## Kosten einer Karte: zwei Drittel Angriff plus ein Drittel Leben, abgerundet, ohne Obergrenze (0/2 kostet nichts).
|
|
static func cost_of(attack: int, health: int) -> int:
|
|
return (2 * attack + health) / 3
|
|
|
|
|
|
## Mehrkosten einer Eigenschaft: Doppelschlag bezahlt den Angriff noch einmal (2·Angriff/3), Flink ein
|
|
## Drittel Angriff, Regeneration ein Drittel Leben, Spott 1; abgerundet, nie unter 1.
|
|
static func keyword_cost(kw: String, attack: int, health: int) -> int:
|
|
match kw:
|
|
"Doppelschlag":
|
|
return maxi(1, 2 * attack / 3)
|
|
"Flink":
|
|
return maxi(1, attack / 3)
|
|
"Regeneration":
|
|
return maxi(1, health / 3)
|
|
return 1
|
|
|
|
|
|
## Mehrkosten weiterer Elemente: das zweite 1, das dritte 2, das vierte 3 … (zusammen 1, 3, 6, 10).
|
|
static func element_cost(count: int) -> int:
|
|
return count * (count - 1) / 2
|
|
|
|
|
|
## Kosten einer Karte mit Eigenschaften und Elementen: Grundkosten plus keyword_cost je Eigenschaft
|
|
## plus element_cost, ohne Obergrenze (Werkstatt und Effektkarten der Kapitel 6 bis 10).
|
|
static func cost_with(attack: int, health: int, keywords: Array, elements: Array) -> int:
|
|
var c := cost_of(attack, health) + element_cost(elements.size())
|
|
for k in keywords:
|
|
c += keyword_cost(k, attack, health)
|
|
return c
|
|
|
|
|
|
## Eigene oder erzeugte Karte (IDs c_… aus der Werkstatt, g_… für Gegner) mit Kosten nach cost_with.
|
|
static func custom(id: String, name: String, elements: Array, attack: int, health: int, keywords: Array) -> Card:
|
|
var c := Card.new()
|
|
c.id = id
|
|
c.name = name
|
|
c.name_en = name
|
|
c.elements = elements.duplicate()
|
|
c.attack = attack
|
|
c.health = health
|
|
c.keywords = keywords.duplicate()
|
|
c.cost = cost_with(attack, health, keywords, elements)
|
|
return c
|
|
|
|
|
|
## Nimmt eine Karte ins Register (by_id) auf, ohne sie in den Pool (all) zu legen.
|
|
static func register(c: Card) -> void:
|
|
if _all.is_empty():
|
|
_build()
|
|
_by_id[c.id] = c
|
|
|
|
|
|
## 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, plus Eigenschaften und zweites Element
|
|
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 boost_attack := 0 # Skill Stark auf diesem Exemplar, kein Grundwert
|
|
var boost_health := 0 # Skill Zäh
|
|
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 (gegen die fairen Kosten cost_with),
|
|
## mal 100, plus 100 je Eigenschaft. Grundkarten und Eigenbauten ohne Eigenschaften liegen bei 100,
|
|
## jede Seltenheitsstufe gibt 100 dazu (Ultimate Rare 500); Fusionen bringen ihr zweites Element
|
|
## und geerbte Eigenschaften umsonst mit und sind entsprechend mehr wert.
|
|
func value() -> int:
|
|
return (Cards.cost_with(attack, health, keywords, elements) - cost + 1) * 100 + 100 * keywords.size()
|
|
|
|
## Eigenbau (Werkstatt) oder erzeugte Gegnerkarte, nicht aus dem Pool.
|
|
func is_custom() -> bool:
|
|
return id.begins_with("c_") or id.begins_with("g_")
|
|
|
|
## Grundwerte ohne den Stufenbonus.
|
|
func base_attack() -> int:
|
|
return attack - maxi(look, 0) - boost_attack
|
|
|
|
func base_health() -> int:
|
|
return health - maxi(look, 0) - boost_health
|
|
|
|
## 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)
|