update
Tests / headless (push) Waiting to run
Tests / fenster-schnell (push) Waiting to run
Tests / fenster-voll (push) Skipped
Tests / balance (push) Skipped

This commit is contained in:
team3 committed 2026-10-15 17:16:01 +02:00
1 parent 1d563b9eec
commit 04fe9716e8
22 files changed
+764 -151

No files matched your search

+35 -15
View File
@@ -1,11 +1,13 @@
class_name Campaign
## Kampagne: fünf Kapitel mit je zehn benannten Magiern, der zehnte ist der Boss. Kapitel 1 spielt
## neutrale Karten, Kapitel 2 bis 5 je ein Element; jeder Gegner spielt 30 Karten seines Elements
## nach Manakurve (DeckGen.curve_deck) mit steigendem Kostendeckel (COST_CAP), in der Stufe seines Kapitels (Kapitel 1 und 2 Common, dann je Kapitel eine höher, Kapitel 5
## Ultra Rare). Mit jedem offenen
## Kapitel gibt es den Booster seines Elements zu kaufen (Profile.booster_unlocked).
## Kampagne: zehn Kapitel mit je zehn benannten Magiern, der zehnte ist der Boss. Kapitel 1 spielt
## neutrale Karten, Kapitel 2 bis 5 je ein Element; Kapitel 6 bis 10 wiederholen die Elemente mit
## Effektkarten (Eigenschaften, DeckGen.effect_deck). Jeder Gegner spielt 30 Karten seines Elements
## nach Manakurve mit steigendem Kostendeckel (COST_CAP), in der Stufe seines Kapitels (rarity_of).
## Mit jedem offenen Kapitel der ersten Runde gibt es den Booster seines Elements
## (Profile.booster_unlocked); nach Kapitel 5 öffnet die Werkstatt (Profile.workshop_unlocked).
const STAGES_PER_CHAPTER := 10
const ROUNDS := 2 # Runde 1: Grundkarten (Kapitel 1 bis 5), Runde 2: Effektkarten (6 bis 10)
const CHAPTER_ELEMENTS: Array[String] = [Cards.NEUTRAL, "Feuer", "Wasser", "Luft", "Erde"]
const NAMES: Array[String] = ["Fenn", "Mara", "Borin", "Liska", "Vulkar", "Nyx", "Ignis", "Thalassa", "Zephyra", "Archimagier"]
const TITLES := {
@@ -22,8 +24,12 @@ const COST_CAP: Array[int] = [3, 4, 4, 5, 5, 6, 7, 8, 9, 10]
const PERK_ORDER: Array[String] = ["feuer_angriff", "leben_35", "wesen_angriff", "ziehen_gratis", "wesen_leben", "extra_mana", "kein_nachteil", "alle_flink", "alle_flink", "start_energie"]
static func chapters() -> int:
return CHAPTER_ELEMENTS.size() * ROUNDS
static func total() -> int:
return STAGES_PER_CHAPTER * CHAPTER_ELEMENTS.size()
return STAGES_PER_CHAPTER * chapters()
static func chapter(stage: int) -> int:
@@ -31,12 +37,19 @@ static func chapter(stage: int) -> int:
static func element_of(chapter_no: int) -> String:
return CHAPTER_ELEMENTS[chapter_no - 1]
return CHAPTER_ELEMENTS[(chapter_no - 1) % CHAPTER_ELEMENTS.size()]
## Merkmale je Gegner: eins, ab Kapitel 3 zwei, in Kapitel 5 drei.
## Merkmale je Gegner: eins, ab Kapitel 3 zwei, ab Kapitel 5 drei.
static func perk_count(chapter_no: int) -> int:
return (chapter_no + 1) / 2
return mini(3, (chapter_no + 1) / 2)
## Eigenschaften je Gegnerkarte: Kapitel 1 bis 5 keine, 6 und 7 eine, ab 8 zwei.
static func keyword_count(chapter_no: int) -> int:
if chapter_no <= CHAPTER_ELEMENTS.size():
return 0
return 1 if chapter_no <= CHAPTER_ELEMENTS.size() + 2 else 2
## Stufe i: Name, Titel, Leben, Belohnung (Kapitel 1: 100 bis 300, Boss 500; je Kapitel 250 mehr),
@@ -48,25 +61,32 @@ static func stage(i: int) -> Dictionary:
for n in perk_count(c):
perks.append(PERK_ORDER[(k + 3 * n) % STAGES_PER_CHAPTER])
var base := 100 + 250 * (c - 1)
return {"name": NAMES[k], "title": TITLES[element_of(c)][k], "life": LIFE[k],
var title: String = TITLES[element_of(c)][k] + (" II" if c > CHAPTER_ELEMENTS.size() else "")
return {"name": NAMES[k], "title": title, "life": LIFE[k],
"reward": base + (400 if k == STAGES_PER_CHAPTER - 1 else 50 * (k / 2)),
"perks": perks, "element": element_of(c), "max_cost": COST_CAP[k]}
"perks": perks, "element": element_of(c), "max_cost": COST_CAP[k], "keywords": keyword_count(c)}
## Gegner einer Stufe: Leben und Merkmale der Stufe, 30 Karten seines Elements nach Manakurve bis
## max_cost, in der Stufe des Kapitels (rarity_of).
## max_cost (ab Kapitel 6 Effektkarten), in der Stufe des Kapitels (rarity_of).
static func opponent(stage_index: int, r: RandomNumberGenerator = null) -> Game.Player:
var st := stage(stage_index)
var boss := Game.Player.new(st.name, false, int(st.life), st.title)
boss.perks = Array(st.perks)
if r == null:
r = Game.make_rng()
boss.deck = DeckGen.curve_deck(st.element, r, int(st.max_cost))
if int(st.keywords) > 0:
boss.deck = DeckGen.effect_deck(st.element, r, int(st.max_cost), int(st.keywords))
else:
boss.deck = DeckGen.curve_deck(st.element, r, int(st.max_cost))
for id in boss.deck:
boss.looks[id] = rarity_of(chapter(stage_index))
return boss
## Seltenheitsstufe der Gegnerkarten: Kapitel 1 und 2 Common, dann je Kapitel eine höher.
## Seltenheitsstufe der Gegnerkarten: Kapitel 1 und 2 Common, dann je Kapitel eine höher bis Ultra
## Rare; Kapitel 6 bis 9 Ultra Rare, Kapitel 10 Ultimate Rare.
static func rarity_of(chapter_no: int) -> int:
return maxi(0, chapter_no - 2)
if chapter_no >= chapters():
return Cards.RARITIES.size() - 1
return mini(3, maxi(0, chapter_no - 2))
+81 -8
View File
@@ -109,9 +109,74 @@ static func best_element(own: Array, other: Array) -> String:
return best
## Kosten einer Karte: zwei Drittel Angriff plus ein Drittel Leben, abgerundet, höchstens 10 (0/2 kostet nichts).
## 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 mini(10, (2 * attack + health) / 3)
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
@@ -172,9 +237,11 @@ class Card extends RefCounted:
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 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:
@@ -189,17 +256,23 @@ class Card extends RefCounted:
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.
## 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_of(attack, health) - cost + 1) * 100
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)
return attack - maxi(look, 0) - boost_attack
func base_health() -> int:
return health - maxi(look, 0)
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:
+40 -2
View File
@@ -30,9 +30,14 @@ static func element_deck(element: String) -> Array:
## je Kosten so viele wie CURVE vorgibt, zufällig gewählt. Was über dem Deckel fehlt, füllen die
## erlaubten Kosten reihum von billig nach teuer auf.
static func curve_deck(element: String, r: RandomNumberGenerator, max_cost: int = 10) -> Array:
return curve_pick(Cards.all().filter(func(c): return c.elements[0] == element), r, max_cost)
## Wählt aus cards (Karten) nach der Manakurve bis max_cost, siehe curve_deck; liefert IDs.
static func curve_pick(cards: Array, r: RandomNumberGenerator, max_cost: int) -> Array:
var by_cost := {}
for c in Cards.all():
if c.elements[0] == element and c.cost <= max_cost:
for c in cards:
if c.cost <= max_cost:
if not by_cost.has(c.cost):
by_cost[c.cost] = []
by_cost[c.cost].append(c.id)
@@ -56,6 +61,39 @@ static func curve_deck(element: String, r: RandomNumberGenerator, max_cost: int
return out
## Stapel mit Effektkarten (Kapitel 6 bis 10): wie curve_deck, aber jede Karte trägt keyword_count
## Eigenschaften. Zur Wahl stehen alle Grundkarten des Elements mit jeder Eigenschaftskombination,
## deren Kosten (Cards.cost_with) unter max_cost bleiben; die gewählten werden registriert (IDs g_…).
static func effect_deck(element: String, r: RandomNumberGenerator, max_cost: int, keyword_count: int) -> Array:
var pool := {}
for b in Cards.all():
if b.elements[0] != element:
continue
for combo in combos(Array(Cards.KEYWORDS), keyword_count):
var kws := Cards.KEYWORDS.filter(func(k): return k in combo)
var short := "".join(kws.map(func(k): return k.substr(0, 1)))
var c := Cards.custom("g_%s_%s" % [b.id, short], b.name, b.elements, b.attack, b.health, kws)
c.name_en = b.name_en
pool[c.id] = c
var out := curve_pick(pool.values(), r, max_cost)
for id in out:
Cards.register(pool[id])
return out
## Alle Teilmengen von items mit k Einträgen, in der Reihenfolge von items.
static func combos(items: Array, k: int) -> Array:
if k == 0:
return [[]]
var out := []
for i in items.size():
for rest in combos(items.slice(i + 1), k - 1):
var combo := [items[i]]
combo.append_array(rest)
out.append(combo)
return out
## Füllt deck aus pool (Karten-IDs) bis limit auf, die wertvollsten zuerst (Card.value in der Stufe
## aus levels: ID -> Stufe); bei gleichem Wert zuerst, was der Manakurve (CURVE, auf limit
## skaliert) noch fehlt, dann die billigeren, dann nach ID; nichts doppelt.
+24 -3
View File
@@ -137,6 +137,9 @@ class Player extends RefCounted:
var cards_played := 0 # ausgespielte Karten (Münzen nach dem Kampf)
var draws := 0 # in diesem Zug gezogene Karten; die n-te kostet n-1 Mana
var fatigue := 0 # Züge ohne Karten im Stapel: so viel Leben kostet der nächste
var bonus_attack := 0 # Skill Stark: so viel mehr Angriff auf jeder gezogenen Karte
var bonus_health := 0 # Skill Zäh: so viel mehr Leben
var cheap_from := 99 # Skill Sparsam: Karten ab diesen Kosten sind 1 Mana billiger
func _init(n: String, h: bool, l: int, t: String = "") -> void:
name = n
@@ -148,12 +151,24 @@ class Player extends RefCounted:
func alive() -> bool:
return life > 0
## Die oberste Karte des eigenen Stapels als Exemplar in der eigenen Stufe; null, wenn er leer ist.
## Die oberste Karte des eigenen Stapels als Exemplar in der eigenen Stufe und mit den Skillboni
## (Stark, Zäh, Sparsam); null, wenn er leer ist.
func draw_from_deck() -> Cards.Card:
if deck.is_empty():
return null
var id: String = deck.pop_back()
return Cards.by_id(id).leveled(int(looks.get(id, 0)))
return boost(Cards.by_id(id).leveled(int(looks.get(id, 0))))
## Legt die Skillboni auf ein Exemplar: Stark und Zäh getrennt von den Grundwerten (Fusionen
## rechnen ohne sie), Sparsam macht teure Karten 1 Mana billiger.
func boost(c: Cards.Card) -> Cards.Card:
c.boost_attack = bonus_attack
c.boost_health = bonus_health
c.attack += bonus_attack
c.health += bonus_health
if c.cost >= cheap_from:
c.cost -= 1
return c
## Offene eigene Wesen mit diesem Element.
func element_count(e: String) -> int:
@@ -177,6 +192,9 @@ class Player extends RefCounted:
p.deck = deck.duplicate()
p.draws = draws
p.fatigue = fatigue
p.bonus_attack = bonus_attack
p.bonus_health = bonus_health
p.cheap_from = cheap_from
for c in board:
p.board.append(c.clone(p))
return p
@@ -489,7 +507,7 @@ func can_fuse(p: Player, a, b) -> Array:
## Die Karte, die aus a und b entsteht (siehe Cards.fused), in der höheren Stufe der Eltern.
static func fusion_result(p: Player, a, b) -> Cards.Card:
return Cards.fused(card_of(a), card_of(b), maxi(look_of(p, card_of(a)), look_of(p, card_of(b))))
return p.boost(Cards.fused(card_of(a), card_of(b), maxi(look_of(p, card_of(a)), look_of(p, card_of(b)))))
## Verschmilzt zwei eigene Karten zu einer neuen Handkarte: Wesen verlassen das Brett, die neue
@@ -633,6 +651,9 @@ static func new_battle_with(opp: Player, r: RandomNumberGenerator = null, fixed_
human.perks = Array(rules.get("human_perks", []))
human.looks = rules.get("looks", {})
human.deck = rules.get("deck", []).duplicate()
human.bonus_attack = int(rules.get("bonus_attack", 0))
human.bonus_health = int(rules.get("bonus_health", 0))
human.cheap_from = int(rules.get("cheap_from", 99))
var g := Game.new(human, opp, r)
for p in g.players:
if "leben_35" in p.perks:
+66 -11
View File
@@ -33,6 +33,7 @@ const DUST_UPGRADE := {"Rare": 100, "Super Rare": 200, "Ultra Rare": 500, "Ultim
var coins := 0
var dust := 0
var xp := 0 # Erfahrung: alle je verdienten Münzen (Skills.level_for)
var customs: Array = [] # Eigenbauten aus der Werkstatt (Card.to_dict), beim Laden registriert
var skills := {} # Skill-ID -> gelernte Stufe (Skills.SKILLS)
var collection := {} # eigene Karten: id -> Seltenheitsstufe (Index in Cards.RARITIES)
var deck: Array = [] # Karten-IDs des eigenen Stapels (Game.DECK_MIN bis DECK_MAX)
@@ -84,6 +85,11 @@ static func from_dict(d: Dictionary) -> Profile:
if Skills.SKILLS.has(id):
p.skills[id] = clampi(int(sk[id]), 0, int(Skills.SKILLS[id].max))
p.campaign_stage = int(d.get("campaign_stage", 0))
for cd in d.get("customs", []):
if cd is Dictionary and cd.has("id") and str(cd.id).begins_with("c_"):
var c := Cards.Card.from_dict(cd)
Cards.register(c)
p.customs.append(c.to_dict())
if int(d.get("version", 1)) >= SAVE_VERSION:
var coll = d.get("collection", {})
for cid in coll:
@@ -110,7 +116,7 @@ static func from_dict(d: Dictionary) -> Profile:
func to_dict() -> Dictionary:
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "xp": xp, "skills": skills, "collection": collection,
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "xp": xp, "skills": skills, "customs": customs, "collection": collection,
"deck": deck, "campaign_stage": campaign_stage, "settings": settings, "perk": perk}
@@ -225,16 +231,17 @@ func booster_unlocked(element: String) -> bool:
return campaign_stage >= Campaign.STAGES_PER_CHAPTER * Campaign.CHAPTER_ELEMENTS.find(element)
## Fünf Karten des Elements mit gewürfelter Seltenheit; luck (Packglück) verschiebt die Chancen.
static func roll_booster(r: RandomNumberGenerator, element: String, luck: int = 0) -> Array:
## Fünf Karten des Elements mit gewürfelter Seltenheit; luck (Packglück) verschiebt die Chancen,
## mindestens eine Karte hat die Seltenheit floor (Rare, mit Garantie Super Rare).
static func roll_booster(r: RandomNumberGenerator, element: String, luck: int = 0, floor: String = "Rare") -> Array:
var rarities := RARITY_WEIGHTS.keys()
var weights := Skills.pack_weights(luck)
var pool := Cards.all().filter(func(c): return c.elements[0] == element)
var out := []
for i in BOOSTER_SIZE:
var rar: String = rarities[r.rand_weighted(weights)]
if i == BOOSTER_SIZE - 1 and out.all(func(x): return x.rarity == "Common"):
rar = "Rare" # mindestens eine Rare pro Booster
if i == BOOSTER_SIZE - 1 and out.all(func(x): return Cards.RARITIES.find(x.rarity) < Cards.RARITIES.find(floor)):
rar = floor
out.append({"card": pool[r.randi_range(0, pool.size() - 1)], "rarity": rar})
return out
@@ -261,7 +268,7 @@ func open_boosters(element: String, n: int, r: RandomNumberGenerator = null) ->
func _open_one(element: String, r: RandomNumberGenerator) -> Array:
coins -= pack_price(element)
var out := roll_booster(r, element, skill("packglueck"))
var out := roll_booster(r, element, skill("packglueck"), Skills.pack_floor(skill("garantie")))
for res in out:
var lvl: int = Cards.RARITIES.find(res.rarity)
res["new"] = not owns(res.card.id)
@@ -330,10 +337,14 @@ func skill_points() -> int:
return player_level() - spent
## Lernt die nächste Stufe eines Skills für einen Punkt. Gibt "" zurück oder eine Fehlermeldung.
## Lernt die nächste Stufe eines Skills für einen Punkt; der Skill davor im Pfad braucht eine Stufe.
## Gibt "" zurück oder eine Fehlermeldung.
func learn(id: String) -> String:
if skill(id) >= int(Skills.SKILLS[id].max):
return "Schon auf der höchsten Stufe"
var prev := Skills.previous(id)
if prev != "" and skill(prev) == 0:
return "Erst den Skill davor lernen"
if skill_points() <= 0:
return "Kein Skillpunkt frei"
skills[id] = skill(id) + 1
@@ -379,15 +390,59 @@ func opponent_level() -> int:
return clampi(deck_level() + int(difficulty().level), 0, Cards.RARITIES.size() - 1)
## Elemente der offenen Kampagnenkapitel (Neutral immer).
## Elemente der offenen Kampagnenkapitel (Neutral immer, ab Kapitel 5 alle).
func unlocked_elements() -> Array:
return Campaign.CHAPTER_ELEMENTS.slice(0, campaign_stage / Campaign.STAGES_PER_CHAPTER + 1)
return Campaign.CHAPTER_ELEMENTS.slice(0, mini(Campaign.CHAPTER_ELEMENTS.size(), campaign_stage / Campaign.STAGES_PER_CHAPTER + 1))
## Alle Karten der offenen Elemente: was Sammlung, Deck und Booster zeigen.
## Alle Karten der offenen Elemente plus die eigenen Eigenbauten: was Sammlung und Deck zeigen.
func unlocked_cards() -> Array:
var open := unlocked_elements()
return Cards.all().filter(func(c): return c.elements[0] in open)
var out: Array = Cards.all().filter(func(c): return c.elements[0] in open)
for id in collection:
if Cards.has_id(id) and Cards.by_id(id).is_custom():
out.append(Cards.by_id(id))
return out
# --- Werkstatt ----------------------------------------------------------------------
## Die Werkstatt öffnet nach dem Boss von Kapitel 5.
func workshop_unlocked() -> bool:
return campaign_stage >= Campaign.CHAPTER_ELEMENTS.size() * Campaign.STAGES_PER_CHAPTER
## Preis eines Eigenbaus in Staub: der doppelte Kartenwert, plus 100 je weiterem Element.
func build_price(card: Cards.Card) -> int:
return 2 * card.value() + 100 * maxi(card.elements.size() - 1, 0)
## Baut eine Karte (Werkstatt): beliebig viele Elemente, Angriff 0 bis 10, Leben 1 bis 10, beliebig
## viele Eigenschaften, Kosten nach Cards.cost_with. Sie kommt als Common in Sammlung und Stapel.
## Gibt "" zurück oder eine Fehlermeldung.
func build_card(name: String, elements: Array, attack: int, health: int, keywords: Array) -> String:
if not workshop_unlocked():
return "Werkstatt noch gesperrt"
if name.strip_edges() == "":
return "Name fehlt"
if elements.is_empty():
return "Mindestens ein Element"
if attack < 0 or attack > 10 or health < 1 or health > 10:
return "Angriff 0 bis 10, Leben 1 bis 10"
var n := 1
for cd in customs:
n = maxi(n, int(str(cd.id).trim_prefix("c_")) + 1)
var c := Cards.custom("c_%d" % n, name.strip_edges(), elements, attack, health, keywords)
var price := build_price(c)
if dust < price:
return "Nicht genug Staub (%d nötig)" % price
dust -= price
Cards.register(c)
customs.append(c.to_dict())
collection[c.id] = 0
if deck.size() < Game.DECK_MAX:
deck.append(c.id)
return ""
## Einzelspieler-Gegner: ein Element aus den offenen Kapiteln, 30 Karten nach Manakurve in opponent_level;
+52 -13
View File
@@ -1,18 +1,30 @@
class_name Skills
## Skilltree: Erfahrung sind die verdienten Münzen, je Level ein Skillpunkt, Punkte kaufen Stufen
## der Skills in drei Ästen. Münzen kaufen keine Punkte.
## der Skills in vier Ästen (PATHS). Jeder Ast ist ein Pfad: der nächste Skill öffnet, sobald der
## davor mindestens eine Stufe hat. Münzen kaufen keine Punkte.
const BRANCHES: Array[String] = ["Sammeln", "Wirtschaft", "Kampf"]
## id -> Name, Ast, Höchststufe, Text.
const SKILLS := {
"packglueck": {"name": "Packglück", "branch": "Sammeln", "max": 3, "text": "Seltenere Karten in Boostern"},
"staub": {"name": "Staubsammler", "branch": "Sammeln", "max": 3, "text": "Mehr Staub aus Doppelten und Zerlegen"},
"muenzen": {"name": "Münzsegen", "branch": "Wirtschaft", "max": 3, "text": "Mehr Münzen nach jedem Kampf"},
"packpreis": {"name": "Händler", "branch": "Wirtschaft", "max": 2, "text": "Booster werden billiger"},
"gratisziehen": {"name": "Freies Ziehen", "branch": "Kampf", "max": 1, "text": "Das zweite Ziehen je Zug ist auch gratis"},
"startmana": {"name": "Starker Start", "branch": "Kampf", "max": 1, "text": "Mehr Mana im ersten Zug"},
const BRANCHES: Array[String] = ["Glück", "Wirtschaft", "Kraft", "Weisheit"]
## Ast -> Skills in Pfadreihenfolge (oben nach unten).
const PATHS := {
"Glück": ["packglueck", "garantie"],
"Wirtschaft": ["packpreis", "muenzen", "staub"],
"Kraft": ["zaeh", "stark"],
"Weisheit": ["gratisziehen", "startmana", "sparsam"],
}
## Kampfskills geben ein Merkmal (Game.PERKS) im Kampf.
## id -> Name, Höchststufe, Text.
const SKILLS := {
"packglueck": {"name": "Packglück", "max": 3, "text": "Seltenere Karten in Boostern"},
"garantie": {"name": "Garantie", "max": 1, "text": "Mindestens eine Super Rare je Booster"},
"packpreis": {"name": "Händler", "max": 3, "text": "Booster werden billiger"},
"muenzen": {"name": "Münzsegen", "max": 3, "text": "Mehr Münzen nach jedem Kampf"},
"staub": {"name": "Staubsammler", "max": 3, "text": "Mehr Staub aus Doppelten und Zerlegen"},
"zaeh": {"name": "Zäh", "max": 3, "text": "Eigene Karten haben mehr Leben"},
"stark": {"name": "Stark", "max": 3, "text": "Eigene Karten haben mehr Angriff"},
"gratisziehen": {"name": "Freies Ziehen", "max": 1, "text": "Zweites Ziehen je Zug auch gratis"},
"startmana": {"name": "Starker Start", "max": 1, "text": "Mehr Mana im ersten Zug"},
"sparsam": {"name": "Sparsam", "max": 3, "text": "Teure Karten kosten 1 Mana weniger"},
}
## Diese Skills geben ein Merkmal (Game.PERKS) im Kampf.
const PERK_OF := {"gratisziehen": "ziehen_gratis", "startmana": "start_energie"}
## Packglück je Stufe: Common gibt ab, die höheren Stufen gewinnen.
const LUCK_SHIFT := {"Common": -8.0, "Rare": 3.0, "Super Rare": 2.0, "Ultra Rare": 2.0, "Ultimate Rare": 1.0}
@@ -32,7 +44,26 @@ static func level_for(xp: int) -> int:
static func in_branch(branch: String) -> Array:
return SKILLS.keys().filter(func(id): return SKILLS[id].branch == branch)
return PATHS[branch]
## Der Skill davor im Pfad, "" für den ersten eines Astes.
static func previous(id: String) -> String:
for b in PATHS:
var i: int = PATHS[b].find(id)
if i > 0:
return PATHS[b][i - 1]
return ""
## Sparsam: ab diesen Kosten ist eine Karte 1 Mana billiger (Stufe 1 ab 7, 2 ab 5, 3 ab 3); 99 = nie.
static func cheap_from(rank: int) -> int:
return 9 - 2 * rank if rank > 0 else 99
## Mindestseltenheit je Booster: Rare, mit Garantie Super Rare.
static func pack_floor(garantie: int) -> String:
return "Super Rare" if garantie > 0 else "Rare"
## Boosterchancen je Stufe (Reihenfolge Cards.RARITIES) mit Packglück luck.
@@ -49,14 +80,22 @@ static func effect(id: String, rank: int) -> String:
"packglueck":
var w := pack_weights(rank)
return TranslationServer.translate("Chancen %d/%d/%d/%d/%d %%") % [int(w[0]), int(w[1]), int(w[2]), int(w[3]), int(w[4])]
"garantie":
return TranslationServer.translate("Super Rare je Pack sicher" if rank > 0 else "Rare je Pack sicher")
"staub":
return TranslationServer.translate("+%d %% Staub") % (25 * rank)
"muenzen":
return TranslationServer.translate("+%d %% Münzen") % (10 * rank)
"packpreis":
return TranslationServer.translate("Neutral %d, Element %d Münzen") % [roundi(Profile.BOOSTER_COST * (1.0 - 0.1 * rank)), roundi(Profile.ELEMENT_BOOSTER_COST * (1.0 - 0.1 * rank))]
return TranslationServer.translate("Neutral %d, Element %d") % [roundi(Profile.BOOSTER_COST * (1.0 - 0.1 * rank)), roundi(Profile.ELEMENT_BOOSTER_COST * (1.0 - 0.1 * rank))]
"zaeh":
return TranslationServer.translate("+%d Leben") % rank
"stark":
return TranslationServer.translate("+%d Angriff") % rank
"gratisziehen":
return TranslationServer.translate("zwei Karten je Zug gratis" if rank > 0 else "eine Karte je Zug gratis")
"startmana":
return TranslationServer.translate("erster Zug: %d Mana") % (Game.START_MANA + (2 if rank > 0 else 0))
"sparsam":
return TranslationServer.translate("ab %d Mana 1 billiger") % cheap_from(rank) if rank > 0 else TranslationServer.translate("voller Preis")
return ""
+1 -1
View File
@@ -113,7 +113,7 @@ func _ready() -> void:
## Eigener Stapel und Kartenstufen und das eigene Merkmal (nach Kapitel 1).
func own_rules() -> Dictionary:
var p: Profile = app.profile
var rules := {"looks": p.looks(), "deck": p.deck_cards()}
var rules := {"looks": p.looks(), "deck": p.deck_cards(), "bonus_attack": p.skill("stark"), "bonus_health": p.skill("zaeh"), "cheap_from": Skills.cheap_from(p.skill("sparsam"))}
var perks := p.battle_perks()
if not perks.is_empty():
rules["human_perks"] = perks
+9 -7
View File
@@ -12,7 +12,7 @@ var node_buttons: Array = []
func _ready() -> void:
var p: Profile = app.profile
chapter = mini(Campaign.chapter(p.campaign_stage), Campaign.CHAPTER_ELEMENTS.size())
chapter = mini(Campaign.chapter(p.campaign_stage), Campaign.chapters())
selected = mini(p.campaign_stage, Campaign.total() - 1)
build()
@@ -24,16 +24,18 @@ func build() -> void:
Style.header(self, "Kampagne", func(): app.show_screen(MenuScreen.new()))
var n := Campaign.STAGES_PER_CHAPTER
var x := 40
for c in Campaign.CHAPTER_ELEMENTS.size():
var b := Style.button(self, "Kapitel %d" % (c + 1), Rect2(x, 88, 130, 38), set_chapter.bind(c + 1), Style.PRIMARY if chapter == c + 1 else null)
for c in Campaign.chapters():
var b := Style.button(self, "Kapitel %d" % (c + 1), Rect2(x, 88, 96, 38), set_chapter.bind(c + 1), Style.PRIMARY if chapter == c + 1 else null)
b.add_theme_font_size_override("font_size", 14)
b.disabled = p.campaign_stage < c * n
x += 138
x += 104
var element := Campaign.element_of(chapter)
Style.label(self, tr(element), 20, Cards.ELEMENT_COLORS[element], Rect2(0, 136, 1280, 30), "center", true)
var round_text := tr(element) if chapter <= Campaign.CHAPTER_ELEMENTS.size() else tr("%s mit Effektkarten") % tr(element)
Style.label(self, round_text, 20, Cards.ELEMENT_COLORS[element], Rect2(0, 136, 700, 30), "center", true)
if p.perk_unlocked():
Style.label(self, "Dein Merkmal:", 15, Style.MUTED, Rect2(740, 96, 150, 24), "right")
Style.label(self, "Dein Merkmal:", 15, Style.MUTED, Rect2(740, 140, 150, 24), "right")
perk_box = OptionButton.new()
perk_box.position = Vector2(900, 88)
perk_box.position = Vector2(900, 132)
perk_box.size = Vector2(340, 38)
perk_box.add_item("keins")
var keys: Array = Game.PERKS.keys()
+4 -3
View File
@@ -24,6 +24,7 @@ func _ready() -> void:
["Kapitel 3 frei", func(): app.profile.campaign_stage = maxi(app.profile.campaign_stage, 20)],
["Kapitel 4 frei", func(): app.profile.campaign_stage = maxi(app.profile.campaign_stage, 30)],
["Kapitel 5 frei", func(): app.profile.campaign_stage = maxi(app.profile.campaign_stage, 40)],
["Werkstatt frei", func(): app.profile.campaign_stage = maxi(app.profile.campaign_stage, 50)],
["Kampagne zurücksetzen", func(): app.profile.campaign_stage = 0]]],
["Sammlung", [["Alle Karten (Common)", func(): _give_all(0)], ["Alle Karten Ultimate Rare", func(): _give_all(4)],
["Eigene Karten +1 Stufe", func():
@@ -36,9 +37,9 @@ func _ready() -> void:
Style.label(self, g[0], 18, Style.TEXT, Rect2(40, y, 200, 30), "left", true)
var x := 250
for entry in g[1]:
var b := Style.button(self, entry[0], Rect2(x, y - 4, 190, 40), _apply.bind(entry[1]))
b.add_theme_font_size_override("font_size", 14)
x += 198
var b := Style.button(self, entry[0], Rect2(x, y - 4, 160, 40), _apply.bind(entry[1]))
b.add_theme_font_size_override("font_size", 13)
x += 168
y += 70
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(40, y + 10, 1200, 24), "left")
refresh()
+6 -6
View File
@@ -11,7 +11,7 @@ const MAX_COST := 10
var app
var filter := "" # Element oder ""
var cost_filter := -1 # Kosten 0 bis 10 oder -1
var cost_filter := -1 # Kosten 0 bis 9, MAX_COST für 10 und mehr, oder -1
var grid: CardGrid
var all_button: Button
var element_buttons := {} # Element -> Knopf mit Symbol
@@ -37,7 +37,7 @@ class ElementButton extends Button:
Style.element_icon(self, element, size / 2, size.y * 0.6, Style.lighter(Cards.ELEMENT_COLORS[element], 0.4))
## Manakurve: ein Balken je Kosten 0 bis 10 mit der Zahl der Stapelkarten.
## Manakurve: ein Balken je Kosten 0 bis 9 und einer für 10 und mehr, mit der Zahl der Stapelkarten.
class CurveView extends Control:
var counts: Array = []
@@ -67,7 +67,7 @@ class CurveView extends Control:
if v > 0:
draw_rect(Rect2(x, base - h, bw, h), Style.BLUE)
Style.text(self, str(v), 11, Style.TEXT, Vector2(x + bw / 2, base - h - 8), "center", true)
Style.text(self, str(i), 11, Style.MUTED, Vector2(x + bw / 2, base + 8), "center")
Style.text(self, str(i) if i < n - 1 else str(i) + "+", 11, Style.MUTED, Vector2(x + bw / 2, base + 8), "center")
## Elementanteile des Stapels: je Element eine farbige Kachel mit der Kartenzahl.
@@ -113,7 +113,7 @@ func _ready() -> void:
add_child(fb)
element_buttons[e] = fb
for c in MAX_COST + 1:
var cb := Style.button(self, str(c), Rect2(330 + c * 40, 86, 36, 34), set_cost.bind(c))
var cb := Style.button(self, str(c) if c < MAX_COST else str(c) + "+", Rect2(330 + c * 40, 86, 36, 34), set_cost.bind(c))
cb.add_theme_font_size_override("font_size", 14)
cost_buttons.append(cb)
grid = CardGrid.new(Rect2(40, 132, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 650))
@@ -205,7 +205,7 @@ func cards() -> Array:
if filter != "":
out = out.filter(func(c): return filter in c.elements)
if cost_filter >= 0:
out = out.filter(func(c): return c.cost == cost_filter)
out = out.filter(func(c): return c.cost == cost_filter or (cost_filter == MAX_COST and c.cost > MAX_COST))
out.sort_custom(func(a, b): return sort_key(a) < sort_key(b))
return out
@@ -242,7 +242,7 @@ func refresh() -> void:
var per_element := {}
for id in p.deck:
var c := Cards.by_id(id)
counts[clampi(c.cost, 0, 10)] += 1
counts[mini(c.cost, MAX_COST)] += 1
per_element[c.elements[0]] = int(per_element.get(c.elements[0], 0)) + 1
curve.set_counts(counts)
share.set_counts(per_element)
+4 -2
View File
@@ -1,7 +1,7 @@
class_name MenuScreen
extends Control
## Hauptmenü: links Titel und Spielarten (darunter Booster, Sammlung, Skilltree, dann Einstellungen und
## Beenden), rechts drei Karten der eigenen Sammlung als Schaufenster, unten rechts die Cheats.
## Hauptmenü: links Titel und Spielarten (darunter Booster, Sammlung, Skilltree, nach Kapitel 5 die
## Werkstatt, dann Einstellungen und Beenden), rechts drei Karten der eigenen Sammlung als Schaufenster, unten rechts die Cheats.
var app
var coins_label: Label
@@ -29,6 +29,8 @@ func _ready() -> void:
["Skilltree", func(): app.show_screen(SkillScreen.new())]]:
Style.button(self, entry[0], Rect2(x, y, 166, 44), entry[1])
x += 176
if p.workshop_unlocked():
Style.button(self, "Werkstatt", Rect2(x, y, 166, 44), func(): app.show_screen(WorkshopScreen.new()), Color("5a4628"))
y += 56
x = 80
for entry in [["Einstellungen", func(): app.show_screen(SettingsScreen.new())],
+95 -26
View File
@@ -1,43 +1,92 @@
class_name SkillScreen
extends Control
## Skilltree: oben Level, Erfahrung bis zum nächsten Level und freie Skillpunkte; darunter drei
## Äste (Skills.BRANCHES) mit ihren Skills als Karten: Name, Wirkung, aktuelle und nächste Stufe
## in Zahlen (Skills.effect), Stufe und „Lernen“.
## Skilltree: oben Level, Erfahrungsbalken bis zum nächsten Level und freie Skillpunkte; darunter
## vier Äste (Skills.BRANCHES) als senkrechte Pfade: Knoten mit Name, Wirkung, Rangpunkten, „Jetzt“
## und „Nächste“ in Zahlen (Skills.effect), durch Linien verbunden. Der nächste Knoten öffnet, sobald
## der davor eine Stufe hat; „Lernen“ steht nur, wenn lernbar, fertige Knoten zeigen „Gelernt“.
const COL_W := 285
const COL_STEP := 300
const NODE_TOP := 218
const NODE_H := 150
const NODE_STEP := 172
var app
var level_label: Label
var xp_bar: ProgressBar
var xp_label: Label
var points_label: Label
var msg_label: Label
var nodes := {} # Skill-ID -> SkillNode
var learn_buttons := {} # Skill-ID -> Knopf
var rank_labels := {} # Skill-ID -> Label
var now_labels := {} # Skill-ID -> Wirkung jetzt
var next_labels := {} # Skill-ID -> Wirkung der nächsten Stufe
var state_labels := {} # Skill-ID -> „Gelernt“, „Erst … lernen“ oder „Kein Skillpunkt frei“
var links := {} # Skill-ID -> Linie zum Knoten davor
## Ein Knoten des Baums: Rahmen und Rangpunkte zeichnet er selbst, Texte und Knopf sind Kinder.
class SkillNode extends Control:
var rank := 0
var max_rank := 1
var open := true # der Skill davor hat eine Stufe (oder es gibt keinen)
func _init(rect: Rect2, mx: int) -> void:
max_rank = mx
position = rect.position
size = rect.size
func _draw() -> void:
var done := rank >= max_rank
var bg: Color = Style.PANEL_LIGHT if open else Style.darker(Style.PANEL, 0.9)
var border: Color = Style.GREEN if done else (Style.GOLD if rank > 0 else (Style.MUTED if open else Style.darker(Style.MUTED, 0.5)))
draw_style_box(Style.box(bg, 8, border, 2), Rect2(Vector2.ZERO, size))
for i in max_rank: # Rangpunkte oben rechts
var c := Vector2(size.x - 16 - (max_rank - 1 - i) * 18, 21)
draw_circle(c, 6, Style.GOLD if i < rank else Style.darker(Style.MUTED, 0.6))
func _ready() -> void:
Style.header(self, "Skilltree", func(): app.show_screen(MenuScreen.new()))
level_label = Style.chip(self, "", Rect2(40, 86, 220, 38))
xp_label = Style.chip(self, "", Rect2(276, 86, 400, 38), Style.BLUE)
points_label = Style.chip(self, "", Rect2(692, 86, 300, 38), Style.GREEN)
Style.label(self, "Erfahrung sind deine verdienten Münzen. Jedes Level bringt einen Skillpunkt.", 14, Style.MUTED, Rect2(40, 134, 1200, 22), "left")
level_label = Style.chip(self, "", Rect2(40, 86, 160, 38))
xp_bar = ProgressBar.new()
xp_bar.position = Vector2(216, 86)
xp_bar.size = Vector2(520, 38)
xp_bar.show_percentage = false
xp_bar.add_theme_stylebox_override("background", Style.box(Style.PANEL_LIGHT, 19))
xp_bar.add_theme_stylebox_override("fill", Style.box(Style.darker(Style.BLUE, 0.7), 19))
add_child(xp_bar)
xp_label = Style.label(self, "", 16, Style.TEXT, Rect2(216, 86, 520, 38), "center", true)
points_label = Style.chip(self, "", Rect2(752, 86, 260, 38), Style.GREEN)
Style.label(self, "Erfahrung sind deine verdienten Münzen. Jedes Level bringt einen Skillpunkt. Jeder Ast öffnet sich von oben nach unten.", 14, Style.MUTED, Rect2(40, 134, 1200, 22), "left")
for b in Skills.BRANCHES.size():
var branch: String = Skills.BRANCHES[b]
var x := 40 + b * 410
Style.panel(self, Rect2(x, 170, 390, 580))
Style.label(self, branch, 22, Style.GOLD, Rect2(x, 184, 390, 32), "center", true)
var y := 236
for id in Skills.in_branch(branch):
var x := 40 + b * COL_STEP
Style.panel(self, Rect2(x, 166, COL_W, 600))
Style.label(self, branch, 20, Style.GOLD, Rect2(x, 178, COL_W, 30), "center", true)
var path: Array = Skills.in_branch(branch)
for i in path.size():
var id: String = path[i]
var sk: Dictionary = Skills.SKILLS[id]
Style.panel(self, Rect2(x + 16, y, 358, 176), Style.PANEL_LIGHT, 8)
Style.label(self, sk.name, 18, Style.TEXT, Rect2(x + 32, y + 10, 326, 26), "left", true)
Style.label(self, sk.text, 14, Style.MUTED, Rect2(x + 32, y + 38, 326, 22), "left")
now_labels[id] = Style.label(self, "", 14, Style.TEXT, Rect2(x + 32, y + 64, 326, 22), "left")
next_labels[id] = Style.label(self, "", 14, Style.GREEN, Rect2(x + 32, y + 86, 326, 22), "left")
rank_labels[id] = Style.label(self, "", 15, Style.GOLD, Rect2(x + 32, y + 126, 150, 30), "left", true)
learn_buttons[id] = Style.button(self, "Lernen", Rect2(x + 214, y + 122, 144, 40), learn.bind(id), Style.PRIMARY)
y += 192
msg_label = Style.label(self, "", 15, Style.RED, Rect2(0, 760, 1280, 24), "center")
var y := NODE_TOP + i * NODE_STEP
if i > 0:
var line := ColorRect.new()
line.position = Vector2(x + COL_W / 2.0 - 2, y - NODE_STEP + NODE_H)
line.size = Vector2(4, NODE_STEP - NODE_H)
add_child(line)
links[id] = line
var node := SkillNode.new(Rect2(x + 14, y, COL_W - 28, NODE_H), int(sk.max))
add_child(node)
nodes[id] = node
var w := COL_W - 28 - 28
Style.label(node, sk.name, 17, Style.TEXT, Rect2(14, 8, w - 60, 26), "left", true)
var text := Style.label(node, sk.text, 12, Style.MUTED, Rect2(14, 34, w, 34), "left")
text.autowrap_mode = TextServer.AUTOWRAP_WORD
now_labels[id] = Style.label(node, "", 12, Style.TEXT, Rect2(14, 70, w, 20), "left")
next_labels[id] = Style.label(node, "", 12, Style.GREEN, Rect2(14, 88, w, 20), "left")
learn_buttons[id] = Style.button(node, "Lernen", Rect2(14, 110, w, 32), learn.bind(id), Style.PRIMARY)
state_labels[id] = Style.label(node, "", 14, Style.MUTED, Rect2(14, 112, w, 28), "center", true)
msg_label = Style.label(self, "", 15, Style.RED, Rect2(0, 770, 1280, 24), "center")
refresh()
@@ -60,11 +109,31 @@ func refresh() -> void:
var lvl := p.player_level()
var base := Skills.xp_for_level(lvl)
level_label.text = tr("Level %d") % lvl
xp_bar.max_value = Skills.xp_for_level(lvl + 1) - base
xp_bar.value = p.xp - base
xp_label.text = tr("%d / %d EP bis Level %d") % [p.xp - base, Skills.xp_for_level(lvl + 1) - base, lvl + 1]
points_label.text = tr("Skillpunkte: %d") % p.skill_points()
for id in Skills.SKILLS:
var sk: Dictionary = Skills.SKILLS[id]
rank_labels[id].text = tr("Stufe %d / %d") % [p.skill(id), int(sk.max)]
now_labels[id].text = tr("Jetzt: %s") % Skills.effect(id, p.skill(id))
next_labels[id].text = tr("Nächste Stufe: %s") % Skills.effect(id, p.skill(id) + 1) if p.skill(id) < int(sk.max) else tr("Höchste Stufe erreicht")
learn_buttons[id].disabled = p.skill_points() <= 0 or p.skill(id) >= int(sk.max)
var node: SkillNode = nodes[id]
var prev := Skills.previous(id)
node.rank = p.skill(id)
node.open = prev == "" or p.skill(prev) > 0
node.modulate = Color(1, 1, 1, 1 if node.open else 0.55)
node.queue_redraw()
var done := node.rank >= int(sk.max)
now_labels[id].text = tr("Jetzt: %s") % Skills.effect(id, node.rank)
next_labels[id].text = "" if done else tr("Nächste: %s") % Skills.effect(id, node.rank + 1)
var learnable := node.open and not done and p.skill_points() > 0
learn_buttons[id].visible = learnable
var state := ""
if done:
state = tr("Gelernt")
elif not node.open:
state = tr("Erst %s lernen") % tr(Skills.SKILLS[prev].name)
elif not learnable:
state = tr("Kein Skillpunkt frei")
state_labels[id].text = state
state_labels[id].add_theme_color_override("font_color", Style.GREEN if done else Style.MUTED)
if links.has(id):
links[id].color = Style.GOLD if node.open else Style.darker(Style.MUTED, 0.5)
+4 -15
View File
@@ -534,7 +534,7 @@ static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius:
## bei Ultimate Rare golden und als Relief.
static func picture(ci: CanvasItem, card, c: Vector2, r: float, rar: String, seed: int) -> void:
var src = card.art_card()
var ring := lighter(Cards.ELEMENT_COLORS[src.elements[0]], 0.3)
var ring := lighter(Cards.ELEMENT_COLORS[Cards.strongest_element(src.elements, hash(src.id))], 0.3)
ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.8))
ci.draw_circle(c, r, Color(0.09, 0.09, 0.13, 0.9))
glyph(ci, src, c, r * 1.3, 0.95, rarity_relief(rar))
@@ -616,23 +616,12 @@ static func glyph(ci: CanvasItem, card, center: Vector2, size: float, alpha: flo
## Symbol in einer festen Farbe (Relief) oder, bei durchsichtigem tint, in Elementfarben.
static func _glyph_plain(ci: CanvasItem, card, center: Vector2, size: float, tint: Color, alpha: float = 0.95) -> void:
var variant := absi(hash(card.id)) % 3
var col := lighter(Cards.ELEMENT_COLORS[card.elements[0]], 0.38)
var e := Cards.strongest_element(card.elements, hash(card.id)) # mehrere Elemente: nur das stärkste
var col := lighter(Cards.ELEMENT_COLORS[e], 0.38)
col.a = alpha
if tint.a > 0:
col = tint
var elems: Array = card.elements
if elems.size() == 1:
_element_glyph(ci, elems[0], center, size, variant, col)
return
# Verschmolzen: die ersten beiden Elemente nebeneinander, ab dem dritten ein Ring
_element_glyph(ci, elems[0], center + Vector2(-size * 0.28, 0), size * 0.62, variant, col)
var col2 := lighter(Cards.ELEMENT_COLORS[elems[1]], 0.38)
col2.a = alpha
if tint.a > 0:
col2 = tint
_element_glyph(ci, elems[1], center + Vector2(size * 0.28, 0), size * 0.62, variant, col2)
if elems.size() >= 3:
ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2)
_element_glyph(ci, e, center, size, variant, col)
## Elementsymbol in einer Farbe (Filterknöpfe, Listen), Grundvariante.
+158
View File
@@ -0,0 +1,158 @@
class_name WorkshopScreen
extends Control
## Werkstatt (nach Kapitel 5): eigene Karten aus Staub bauen. Links Elemente (beliebig viele),
## Angriff, Leben, Eigenschaften (beliebig viele) und der Name; rechts die Vorschau mit Kosten
## (Cards.cost_with), Wert und Preis (Profile.build_price). „Bauen“ legt die Karte als Common in
## Sammlung und Stapel.
var app
var elements: Array = []
var keywords: Array = []
var attack_slider: HSlider
var health_slider: HSlider
var attack_label: Label
var health_label: Label
var element_buttons := {}
var keyword_boxes := {}
var name_box: LineEdit
var info_label: Label
var msg_label: Label
var dust_label: Label
var preview_holder: Control
var built := 0 # für den Vorschlagsnamen
func _ready() -> void:
Style.header(self, "Werkstatt", func(): app.show_screen(MenuScreen.new()))
dust_label = Style.chip(self, "", Rect2(860, 26, 240, 38), Style.BLUE)
Style.panel(self, Rect2(40, 86, 700, 696))
Style.label(self, "Baue eigene Karten aus Staub: so viele Elemente und Eigenschaften du willst, jede kostet Mana.", 14, Style.MUTED, Rect2(60, 98, 660, 22), "left")
Style.label(self, "Elemente", 16, Style.TEXT, Rect2(60, 134, 140, 30), "left", true)
var all_elements: Array = Cards.ELEMENTS + [Cards.NEUTRAL]
for i in all_elements.size():
var e: String = all_elements[i]
var b := DeckScreen.ElementButton.new(e)
b.position = Vector2(200 + i * 48, 130)
b.size = Vector2(40, 40)
b.pressed.connect(toggle_element.bind(e))
Style.juice(b)
add_child(b)
element_buttons[e] = b
Style.label(self, "Angriff", 16, Style.TEXT, Rect2(60, 194, 140, 30), "left", true)
attack_slider = _slider(Rect2(200, 198, 400, 24), 0, 10, 3)
attack_label = Style.label(self, "", 18, Style.AMBER, Rect2(620, 192, 60, 30), "center", true)
Style.label(self, "Leben", 16, Style.TEXT, Rect2(60, 244, 140, 30), "left", true)
health_slider = _slider(Rect2(200, 248, 400, 24), 1, 10, 3)
health_label = Style.label(self, "", 18, Style.LIFE, Rect2(620, 242, 60, 30), "center", true)
Style.label(self, "Eigenschaften", 16, Style.TEXT, Rect2(60, 300, 200, 30), "left", true)
for k in Cards.KEYWORDS.size():
var kw: String = Cards.KEYWORDS[k]
var cb := CheckButton.new()
cb.text = tr(kw)
cb.position = Vector2(200, 300 + k * 44)
cb.size = Vector2(220, 36)
cb.toggled.connect(func(on: bool): toggle_keyword(kw, on))
add_child(cb)
keyword_boxes[kw] = cb
Style.label(self, tr(Cards.KEYWORD_TEXT[kw]), 13, Style.MUTED, Rect2(430, 306 + k * 44, 300, 24), "left")
Style.label(self, "Name", 16, Style.TEXT, Rect2(60, 490, 140, 30), "left", true)
name_box = LineEdit.new()
name_box.position = Vector2(200, 486)
name_box.size = Vector2(400, 36)
name_box.max_length = 24
name_box.text_changed.connect(func(_t: String): refresh())
add_child(name_box)
info_label = Style.label(self, "", 16, Style.GOLD, Rect2(60, 546, 660, 30), "left", true)
Style.button(self, "Bauen", Rect2(60, 600, 300, 50), build, Style.PRIMARY)
msg_label = Style.label(self, "", 15, Style.RED, Rect2(60, 666, 660, 48), "left")
msg_label.autowrap_mode = TextServer.AUTOWRAP_WORD
Style.panel(self, Rect2(760, 86, 480, 696))
preview_holder = Control.new()
preview_holder.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(preview_holder)
_suggest_name()
refresh()
func _slider(rect: Rect2, lo: int, hi: int, value: int) -> HSlider:
var s := HSlider.new()
s.position = rect.position
s.size = rect.size
s.min_value = lo
s.max_value = hi
s.step = 1
s.value = value
s.value_changed.connect(func(_v: float): refresh())
add_child(s)
return s
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel") and not name_box.has_focus():
app.show_screen(MenuScreen.new())
func _suggest_name() -> void:
name_box.text = tr("Eigenbau %d") % (app.profile.customs.size() + 1)
## Ein Element an- oder abwählen.
func toggle_element(e: String) -> void:
if e in elements:
elements.erase(e)
else:
elements.append(e)
say("")
refresh()
func toggle_keyword(kw: String, on: bool) -> void:
if on and not (kw in keywords):
keywords.append(kw)
elif not on:
keywords.erase(kw)
keywords = Cards.KEYWORDS.filter(func(k): return k in keywords)
refresh()
func say(m: String, ok: bool = false) -> void:
msg_label.text = m
msg_label.add_theme_color_override("font_color", Style.GREEN if ok else Style.RED)
## Die Karte, wie sie gebaut würde (für die Vorschau ohne Element neutral).
func preview_card() -> Cards.Card:
var els := elements if not elements.is_empty() else [Cards.NEUTRAL]
return Cards.custom("c_vorschau", name_box.text.strip_edges(), els, int(attack_slider.value), int(health_slider.value), keywords)
func build() -> void:
var p: Profile = app.profile
var err: String = p.build_card(name_box.text, elements, int(attack_slider.value), int(health_slider.value), keywords)
if err != "":
say(tr(err))
return
app.save()
app.sfx.play("combo")
say(tr("Gebaut: %s") % name_box.text.strip_edges(), true)
_suggest_name()
refresh()
func refresh() -> void:
var p: Profile = app.profile
dust_label.text = "Staub: %d" % p.dust
attack_label.text = str(int(attack_slider.value))
health_label.text = str(int(health_slider.value))
for e in element_buttons:
var active: bool = e in elements
var base: Color = Style.darker(Cards.ELEMENT_COLORS[e], 0.8)
element_buttons[e].add_theme_stylebox_override("normal", Style.box(Style.lighter(base) if active else base, 8, Style.GOLD if active else Style.MUTED, 2 if active else 1))
var c := preview_card()
info_label.text = tr("Kosten %d Mana · Wert %d · Preis %d Staub") % [c.cost, c.value(), p.build_price(c)]
Style.clear(preview_holder)
var v := CardView.new(c, Vector2(240, 336), false, "Common")
v.position = Vector2(880, 160)
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
preview_holder.add_child(v)
Style.label(preview_holder, c.text(), 15, Style.MUTED, Rect2(780, 520, 440, 30), "center")
+1
View File
@@ -0,0 +1 @@
uid://cwy4h3joivrfr