update
This commit is contained in:
1 parent
1d563b9eec
commit
04fe9716e8
22 files changed
+764
-151
No files matched your search
+35
-15
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 ""
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
@@ -0,0 +1 @@
|
||||
uid://cwy4h3joivrfr
|
||||
Reference in new issue
Block a user