update
This commit is contained in:
1 parent
f2c609a08b
commit
1d563b9eec
34 files changed
+5486
-643
No files matched your search
+51
-58
@@ -1,79 +1,72 @@
|
||||
class_name Campaign
|
||||
## Kampagne: zehn benannte Magier in drei Kapiteln. Die Bosse haben weniger Leben als du,
|
||||
## dafür Merkmale; je Kapitel kommt eins dazu.
|
||||
## 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).
|
||||
|
||||
const STAGES := [
|
||||
{"name": "Fenn", "title": "Kobold-Zähmer", "life": 22, "reward": 100, "perk": "feuer_angriff"},
|
||||
{"name": "Mara", "title": "Gezeitenhexe", "life": 22, "reward": 100, "perk": "leben_35"},
|
||||
{"name": "Borin", "title": "Steinfaust", "life": 24, "reward": 150, "perk": "wesen_angriff"},
|
||||
{"name": "Liska", "title": "Windläuferin", "life": 24, "reward": 150, "perk": "ziehen_gratis"},
|
||||
{"name": "Vulkar", "title": "Herr der Lava", "life": 24, "reward": 200, "perk": "wesen_leben"},
|
||||
{"name": "Nyx", "title": "Sturmruferin", "life": 26, "reward": 200, "perk": "extra_mana"},
|
||||
{"name": "Ignis", "title": "Inferno-Priester", "life": 26, "reward": 250, "perk": "element_doppelt"},
|
||||
{"name": "Thalassa", "title": "Tiefseekönigin", "life": 26, "reward": 250, "perk": "alle_flink"},
|
||||
{"name": "Zephyra", "title": "Blitzkaiserin", "life": 28, "reward": 300, "perk": "alle_flink"},
|
||||
{"name": "Archimagier", "title": "Meister der Elemente", "life": 28, "reward": 500, "perk": "start_energie"},
|
||||
]
|
||||
const STAGES_PER_CHAPTER := 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 := {
|
||||
"Neutral": ["Kobold-Zähmer", "Wanderhexe", "Steinfaust", "Pfadfinderin", "Söldnerfürst", "Schattenruferin", "Grauer Priester", "Händlerkönigin", "Stahlkaiserin", "Meister der Lehre"],
|
||||
"Feuer": ["Flammenzähmer", "Glutweberin", "Schmiedefaust", "Funkenläuferin", "Herr der Lava", "Aschenruferin", "Inferno-Priester", "Glutkönigin", "Flammenkaiserin", "Meister des Feuers"],
|
||||
"Wasser": ["Gezeitenzähmer", "Gezeitenhexe", "Eisfaust", "Nebelläuferin", "Herr der Brandung", "Regenruferin", "Tiefsee-Priester", "Tiefseekönigin", "Flutkaiserin", "Meister des Wassers"],
|
||||
"Luft": ["Windzähmer", "Wolkenhexe", "Sturmfaust", "Windläuferin", "Herr der Böen", "Sturmruferin", "Himmelspriester", "Wolkenkönigin", "Blitzkaiserin", "Meister der Lüfte"],
|
||||
"Erde": ["Wurzelzähmer", "Moorhexe", "Felsenfaust", "Hügelläuferin", "Herr der Schluchten", "Bebenruferin", "Erzpriester", "Kristallkönigin", "Bergkaiserin", "Meister der Erde"],
|
||||
}
|
||||
const LIFE := [22, 22, 24, 24, 24, 26, 26, 26, 28, 28]
|
||||
## Höchste Kartenkosten im Stapel je Stufe eines Kapitels.
|
||||
const COST_CAP: Array[int] = [3, 4, 4, 5, 5, 6, 7, 8, 9, 10]
|
||||
## Merkmale je Stufe: das erste nach dieser Liste, weitere je drei Plätze dahinter.
|
||||
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"]
|
||||
|
||||
|
||||
## Kapitel 2: die Magier kehren mit zwei Merkmalen und mehr Leben zurück.
|
||||
const STAGES2 := [
|
||||
{"name": "Fenn", "title": "Flammenzähmer", "life": 22, "reward": 300, "perks": ["feuer_angriff", "start_energie"]},
|
||||
{"name": "Mara", "title": "Herrin der Gezeiten", "life": 22, "reward": 300, "perks": ["leben_35", "extra_mana"]},
|
||||
{"name": "Borin", "title": "Fels der Ewigkeit", "life": 24, "reward": 350, "perks": ["wesen_angriff", "alle_flink"]},
|
||||
{"name": "Liska", "title": "Sturmläuferin", "life": 24, "reward": 350, "perks": ["ziehen_gratis", "wesen_angriff"]},
|
||||
{"name": "Vulkar", "title": "Aschekönig", "life": 24, "reward": 400, "perks": ["wesen_leben", "feuer_angriff"]},
|
||||
{"name": "Nyx", "title": "Gewitterruferin", "life": 26, "reward": 400, "perks": ["extra_mana", "ziehen_gratis"]},
|
||||
{"name": "Ignis", "title": "Inferno-Erzpriester", "life": 26, "reward": 450, "perks": ["element_doppelt", "wesen_leben"]},
|
||||
{"name": "Thalassa", "title": "Kaiserin der Tiefe", "life": 26, "reward": 450, "perks": ["alle_flink", "leben_35"]},
|
||||
{"name": "Zephyra", "title": "Blitzkaiserin", "life": 28, "reward": 500, "perks": ["alle_flink", "start_energie"]},
|
||||
{"name": "Archimagier", "title": "Herr aller Elemente", "life": 28, "reward": 800, "perks": ["start_energie", "element_doppelt"]},
|
||||
]
|
||||
|
||||
|
||||
## Kapitel 3: drei Merkmale und volle Tische.
|
||||
const STAGES3 := [
|
||||
{"name": "Fenn", "title": "Aschefürst", "life": 20, "reward": 600, "perks": ["feuer_angriff", "start_energie", "wesen_leben"]},
|
||||
{"name": "Mara", "title": "Sturmflut", "life": 20, "reward": 600, "perks": ["leben_35", "extra_mana", "alle_flink"]},
|
||||
{"name": "Borin", "title": "Berg selbst", "life": 22, "reward": 700, "perks": ["wesen_angriff", "alle_flink", "leben_35"]},
|
||||
{"name": "Liska", "title": "Windbraut", "life": 22, "reward": 700, "perks": ["ziehen_gratis", "wesen_angriff", "extra_mana"]},
|
||||
{"name": "Vulkar", "title": "Magmakaiser", "life": 22, "reward": 800, "perks": ["wesen_leben", "feuer_angriff", "wesen_angriff"]},
|
||||
{"name": "Nyx", "title": "Orkanmutter", "life": 24, "reward": 800, "perks": ["extra_mana", "ziehen_gratis", "element_doppelt"]},
|
||||
{"name": "Ignis", "title": "Sonnenpriester", "life": 24, "reward": 900, "perks": ["element_doppelt", "wesen_leben", "start_energie"]},
|
||||
{"name": "Thalassa", "title": "Herz der Tiefe", "life": 24, "reward": 900, "perks": ["alle_flink", "leben_35", "element_doppelt"]},
|
||||
{"name": "Zephyra", "title": "Gewitterkaiserin", "life": 26, "reward": 1000, "perks": ["alle_flink", "start_energie", "wesen_leben"]},
|
||||
{"name": "Archimagier", "title": "Ewiger Meister", "life": 26, "reward": 1200, "perks": ["start_energie", "element_doppelt", "leben_35"]},
|
||||
]
|
||||
|
||||
const CHAPTERS := [STAGES, STAGES2, STAGES3]
|
||||
|
||||
static func total() -> int:
|
||||
return STAGES.size() * CHAPTERS.size()
|
||||
return STAGES_PER_CHAPTER * CHAPTER_ELEMENTS.size()
|
||||
|
||||
|
||||
static func chapter(stage: int) -> int:
|
||||
return stage / STAGES.size() + 1
|
||||
return stage / STAGES_PER_CHAPTER + 1
|
||||
|
||||
|
||||
## Stufe mit einheitlichem Feld "perks".
|
||||
static func element_of(chapter_no: int) -> String:
|
||||
return CHAPTER_ELEMENTS[chapter_no - 1]
|
||||
|
||||
|
||||
## Merkmale je Gegner: eins, ab Kapitel 3 zwei, in Kapitel 5 drei.
|
||||
static func perk_count(chapter_no: int) -> int:
|
||||
return (chapter_no + 1) / 2
|
||||
|
||||
|
||||
## Stufe i: Name, Titel, Leben, Belohnung (Kapitel 1: 100 bis 300, Boss 500; je Kapitel 250 mehr),
|
||||
## Merkmale und Element.
|
||||
static func stage(i: int) -> Dictionary:
|
||||
if i < STAGES.size():
|
||||
var st: Dictionary = STAGES[i].duplicate()
|
||||
st["perks"] = [st.perk]
|
||||
return st
|
||||
return CHAPTERS[chapter(i) - 1][i % STAGES.size()]
|
||||
var c := chapter(i)
|
||||
var k := i % STAGES_PER_CHAPTER
|
||||
var perks: Array = []
|
||||
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],
|
||||
"reward": base + (400 if k == STAGES_PER_CHAPTER - 1 else 50 * (k / 2)),
|
||||
"perks": perks, "element": element_of(c), "max_cost": COST_CAP[k]}
|
||||
|
||||
|
||||
## Boss einer Stufe mit dem Leben, den Merkmalen und einem Stapel der Stufe; ab Kapitel 2 sind
|
||||
## seine Karten eine Seltenheitsstufe höher (+1/+1), ab Kapitel 3 zwei.
|
||||
## Gegner einer Stufe: Leben und Merkmale der Stufe, 30 Karten seines Elements nach Manakurve bis
|
||||
## max_cost, 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()
|
||||
var ch := chapter(stage_index)
|
||||
boss.deck = DeckGen.opponent_deck(r, ch)
|
||||
boss.deck = DeckGen.curve_deck(st.element, r, int(st.max_cost))
|
||||
for id in boss.deck:
|
||||
boss.looks[id] = ch - 1
|
||||
boss.looks[id] = rarity_of(chapter(stage_index))
|
||||
return boss
|
||||
|
||||
|
||||
## Seltenheitsstufe der Gegnerkarten: Kapitel 1 und 2 Common, dann je Kapitel eine höher.
|
||||
static func rarity_of(chapter_no: int) -> int:
|
||||
return maxi(0, chapter_no - 2)
|
||||
+61
-24
@@ -1,7 +1,10 @@
|
||||
class_name DeckGen
|
||||
## Stapel: die vier Startdecks, Auffüllen aus der Sammlung, zufällige Stapel für Gegner.
|
||||
## Stapel: Elementstapel nach Manakurve (Startstapel und Gegner), Auffüllen aus der Sammlung,
|
||||
## zufällige Stapel für Tests und Simulation.
|
||||
|
||||
const CHEAP_IN_RANDOM := 12 # so viele im ersten Zug bezahlbare Karten hat ein zufälliger Stapel mindestens
|
||||
## Manakurve eines Stapels mit DECK_MIN Karten: so viele Karten je Kosten 0 bis 10.
|
||||
const CURVE: Array[int] = [2, 5, 6, 6, 5, 3, 2, 1, 0, 0, 0]
|
||||
|
||||
|
||||
## Zufälliger Stapel aus DECK_MIN verschiedenen Karten bis max_cost, mit genug billigen für den Anfang.
|
||||
@@ -18,41 +21,75 @@ static func random_deck(r: RandomNumberGenerator, max_cost: int = 99) -> Array:
|
||||
return out
|
||||
|
||||
|
||||
## Stapel eines Kampagnengegners: Kapitel 1 bis Kosten 3, Kapitel 2 bis 5, Kapitel 3 alles.
|
||||
static func opponent_deck(r: RandomNumberGenerator, chapter: int) -> Array:
|
||||
return random_deck(r, [3, 5, 99][clampi(chapter - 1, 0, 2)])
|
||||
## Alle Karten eines Elements (oder die neutralen) in Datenreihenfolge: die Startsammlung.
|
||||
static func element_deck(element: String) -> Array:
|
||||
return Cards.all().filter(func(c): return c.elements[0] == element).map(func(c): return c.id)
|
||||
|
||||
|
||||
## Startdeck für ein Element: die 25 Karten des Elements bis Angriff 4, dazu je fünf billige der
|
||||
## anderen Elemente (DECK_MIN Karten).
|
||||
static func start_deck(element: String) -> Array:
|
||||
var out := []
|
||||
## Stapel eines Elements nach der Manakurve: DECK_MIN verschiedene Karten des Elements bis max_cost,
|
||||
## 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:
|
||||
var by_cost := {}
|
||||
for c in Cards.all():
|
||||
if c.elements[0] == element and c.attack <= 4:
|
||||
out.append(c.id)
|
||||
for e in Cards.ELEMENTS:
|
||||
if e == element:
|
||||
continue
|
||||
var cheap := Cards.all().filter(func(c): return c.elements[0] == e and c.cost <= Game.START_MANA)
|
||||
cheap.sort_custom(func(a, b): return a.cost < b.cost if a.cost != b.cost else a.attack > b.attack)
|
||||
for c in cheap.slice(0, 5):
|
||||
out.append(c.id)
|
||||
if c.elements[0] == element and c.cost <= max_cost:
|
||||
if not by_cost.has(c.cost):
|
||||
by_cost[c.cost] = []
|
||||
by_cost[c.cost].append(c.id)
|
||||
for cost in by_cost:
|
||||
Game.shuffle(by_cost[cost], r)
|
||||
var out := []
|
||||
for cost in CURVE.size():
|
||||
for k in CURVE[cost]:
|
||||
if by_cost.has(cost) and not by_cost[cost].is_empty():
|
||||
out.append(by_cost[cost].pop_back())
|
||||
while out.size() < Game.DECK_MIN:
|
||||
var added := false
|
||||
for cost in range(0, max_cost + 1):
|
||||
if out.size() >= Game.DECK_MIN:
|
||||
break
|
||||
if by_cost.has(cost) and not by_cost[cost].is_empty():
|
||||
out.append(by_cost[cost].pop_back())
|
||||
added = true
|
||||
if not added:
|
||||
break
|
||||
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 die teuersten; nichts doppelt.
|
||||
## 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.
|
||||
static func fill(deck: Array, pool: Array, limit: int, levels: Dictionary = {}) -> Array:
|
||||
var out := deck.duplicate()
|
||||
var cards := pool.filter(func(id): return not (id in out)).map(func(id): return Cards.by_id(id).leveled(int(levels.get(id, 0))))
|
||||
cards.sort_custom(func(a, b):
|
||||
if a.value() != b.value():
|
||||
return a.value() > b.value()
|
||||
return a.cost > b.cost if a.cost != b.cost else a.id < b.id)
|
||||
for c in cards:
|
||||
if out.size() >= limit:
|
||||
break
|
||||
out.append(c.id)
|
||||
return a.cost < b.cost if a.cost != b.cost else a.id < b.id)
|
||||
var have := {} # Kosten -> Karten im Stapel
|
||||
for id in out:
|
||||
var slot := clampi(Cards.by_id(id).cost, 0, CURVE.size() - 1)
|
||||
have[slot] = int(have.get(slot, 0)) + 1
|
||||
var i := 0
|
||||
while i < cards.size() and out.size() < limit:
|
||||
var j := i
|
||||
while j < cards.size() and cards[j].value() == cards[i].value():
|
||||
j += 1
|
||||
var rest := []
|
||||
for c in cards.slice(i, j):
|
||||
if out.size() >= limit:
|
||||
break
|
||||
var slot := clampi(c.cost, 0, CURVE.size() - 1)
|
||||
if int(have.get(slot, 0)) < CURVE[slot] * limit / Game.DECK_MIN:
|
||||
out.append(c.id)
|
||||
have[slot] = int(have.get(slot, 0)) + 1
|
||||
else:
|
||||
rest.append(c)
|
||||
for c in rest:
|
||||
if out.size() >= limit:
|
||||
break
|
||||
out.append(c.id)
|
||||
i = j
|
||||
return out
|
||||
|
||||
|
||||
|
||||
+28
-85
@@ -6,11 +6,10 @@ extends RefCounted
|
||||
const MAX_BOARD := 5
|
||||
const START_HAND := 5 # Startkarten: die obersten des eigenen Stapels
|
||||
const START_MANA := 1 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt
|
||||
const DECK_MIN := 40 # Karten je Stapel: mindestens so viele (Gegner und Startdecks genau so viele)
|
||||
const DECK_MIN := 30 # Karten je Stapel: mindestens so viele (Gegner genau so viele: alle Karten eines Elements)
|
||||
const DECK_MAX := 60 # und höchstens so viele
|
||||
const START_LIFE := 30
|
||||
const ELEMENT_PAIR := 2 # so viele eigene offene Wesen eines Elements: Sondereffekt des Elements
|
||||
const ELEMENT_COUNT := 3 # je so viele ausgespielte Karten eines Elements: +1/+1 für die eigenen Wesen des Elements
|
||||
const DISCARD_GAIN := 1 # Abwerfen einer Handkarte bringt so viel Mana
|
||||
|
||||
# Merkmale der Kampagnen-Bosse (Player.perks).
|
||||
@@ -23,7 +22,7 @@ const PERKS := {
|
||||
"leben_35": "Beginnt mit 5 Leben mehr",
|
||||
"alle_flink": "Wesen greifen sofort an",
|
||||
"start_energie": "Hat im ersten Zug 2 Mana mehr",
|
||||
"element_doppelt": "Elementbonus zählt doppelt",
|
||||
"kein_nachteil": "Wesen haben keinen Elementnachteil",
|
||||
}
|
||||
|
||||
## Für wiederholbare Tests: 0 = echter Zufall, sonst Startwert (zählt je Aufruf hoch).
|
||||
@@ -50,8 +49,7 @@ class Creature extends RefCounted:
|
||||
var max_health: int
|
||||
var attacks_done := 0 # Angriffe in diesem Zug
|
||||
var ready := false # lag schon zu Beginn des eigenen Zugs: darf angreifen und aufgedeckt werden
|
||||
var hidden := false # verdeckt gelegt: greift nicht an, zählt nicht für die Elementboni
|
||||
var aura := 0 # Elementbonus (+1 je Element mit vollem Stapel); das Leben ist schon angepasst
|
||||
var hidden := false # verdeckt gelegt: greift nicht an, zählt nicht fürs Elementpaar
|
||||
|
||||
func _init(c: Cards.Card, o, id: int) -> void:
|
||||
card = c
|
||||
@@ -90,9 +88,9 @@ class Creature extends RefCounted:
|
||||
out.append(k)
|
||||
return out
|
||||
|
||||
## Angriffswert inklusive Elementbonus und Boss-Merkmalen.
|
||||
## Angriffswert inklusive Boss-Merkmalen.
|
||||
func power() -> int:
|
||||
var v := attack + aura
|
||||
var v := attack
|
||||
var o = owner()
|
||||
if o != null:
|
||||
if "feuer_angriff" in o.perks and "Feuer" in card.elements:
|
||||
@@ -118,7 +116,6 @@ class Creature extends RefCounted:
|
||||
c.attacks_done = attacks_done
|
||||
c.ready = ready
|
||||
c.hidden = hidden
|
||||
c.aura = aura
|
||||
return c
|
||||
|
||||
|
||||
@@ -137,7 +134,7 @@ class Player extends RefCounted:
|
||||
var perks: Array = [] # Boss-Merkmale (siehe PERKS)
|
||||
var looks := {} # Karten-ID -> Seltenheitsstufe, reine Optik (siehe Profile)
|
||||
var turns := 0 # begonnene eigene Züge
|
||||
var played := {} # Element -> bisher ausgespielte Karten (je ELEMENT_COUNT: +1/+1)
|
||||
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
|
||||
|
||||
@@ -147,8 +144,6 @@ class Player extends RefCounted:
|
||||
is_human = h
|
||||
life = l
|
||||
max_life = l
|
||||
for e in Cards.ELEMENTS:
|
||||
played[e] = 0
|
||||
|
||||
func alive() -> bool:
|
||||
return life > 0
|
||||
@@ -178,7 +173,7 @@ class Player extends RefCounted:
|
||||
p.perks = perks.duplicate()
|
||||
p.looks = looks
|
||||
p.turns = turns
|
||||
p.played = played.duplicate()
|
||||
p.cards_played = cards_played
|
||||
p.deck = deck.duplicate()
|
||||
p.draws = draws
|
||||
p.fatigue = fatigue
|
||||
@@ -406,14 +401,12 @@ func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
|
||||
cr.health += 1
|
||||
cr.max_health += 1
|
||||
p.board.append(cr)
|
||||
for e in card.elements:
|
||||
p.played[e] = int(p.played.get(e, 0)) + 1
|
||||
p.cards_played += 1
|
||||
if face_down:
|
||||
say("%s legt ein Wesen verdeckt." % p.name)
|
||||
else:
|
||||
say("%s spielt %s." % [p.name, card.name])
|
||||
emit({"type": "play", "player": players.find(p), "uid": cr.uid, "card": card, "hidden": face_down})
|
||||
apply_auras()
|
||||
cleanup()
|
||||
check_end()
|
||||
return ""
|
||||
@@ -463,12 +456,11 @@ func reveal(p: Player, cr: Creature) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
## Deckt ein Wesen auf; die Elementboni gelten sofort.
|
||||
## Deckt ein Wesen auf; das Elementpaar gilt sofort.
|
||||
func _flip(cr: Creature) -> void:
|
||||
cr.hidden = false
|
||||
say("%s wird aufgedeckt." % cr.name)
|
||||
emit({"type": "reveal", "uid": cr.uid, "card": cr.card})
|
||||
apply_auras()
|
||||
|
||||
|
||||
# --- Verschmelzen ------------------------------------------------------------
|
||||
@@ -523,49 +515,6 @@ func fuse(p: Player, a, b) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
# --- Elementboni ---------------------------------------------------------------
|
||||
|
||||
## +1/+1 je volle ELEMENT_COUNT ausgespielte Karten eines Elements des Spielers; mehrere Elemente
|
||||
## zählen alle, auch Neutral (eigener Zähler, aber kein Paar).
|
||||
static func aura_of(elements: Array, played: Dictionary) -> int:
|
||||
var a := 0
|
||||
for e in elements:
|
||||
a += int(played.get(e, 0)) / ELEMENT_COUNT
|
||||
return a
|
||||
|
||||
|
||||
## Bonus eines offenen Wesens aus den ausgespielten Karten seines Besitzers; verdeckte bekommen nichts.
|
||||
func aura_for(c: Creature) -> int:
|
||||
var o = c.owner()
|
||||
if c.hidden or o == null:
|
||||
return 0
|
||||
var a := aura_of(c.card.elements, o.played)
|
||||
if "element_doppelt" in o.perks:
|
||||
a *= 2
|
||||
return a
|
||||
|
||||
|
||||
## Rechnet die Elementboni neu. Kommt ein Bonus, steigen Leben und Höchstleben; geht er
|
||||
## (beim Verdecken), sinkt nur das Höchstleben, das Leben wird gekappt.
|
||||
func apply_auras() -> void:
|
||||
for p in players:
|
||||
for c in p.board:
|
||||
var a := aura_for(c)
|
||||
if a != c.aura:
|
||||
var delta: int = a - c.aura
|
||||
c.max_health += delta
|
||||
c.health = mini(c.health + maxi(delta, 0), c.max_health)
|
||||
c.aura = a
|
||||
|
||||
|
||||
## Vorschau fürs Ausspielen: der Elementbonus, den die Karte offen gelegt bekäme (sie zählt selbst mit).
|
||||
func forecast(p: Player, card: Cards.Card) -> int:
|
||||
var played := p.played.duplicate()
|
||||
for e in card.elements:
|
||||
played[e] = int(played.get(e, 0)) + 1
|
||||
return aura_of(card.elements, played) * (2 if "element_doppelt" in p.perks else 1)
|
||||
|
||||
|
||||
# --- Kampf -------------------------------------------------------------------
|
||||
|
||||
## Leben lassen sich erst angreifen, wenn der Gegner kein Wesen mehr hat; Wesen mit Spott
|
||||
@@ -582,7 +531,7 @@ func can_attack(cr: Creature, target) -> Array:
|
||||
var tgt_owner = target.owner() if target is Creature else target
|
||||
if not (tgt_owner in enemies_of(cr.owner())):
|
||||
return [false, "Kein gültiges Ziel"]
|
||||
if cr.power() + matchup_bonus(cr, target) <= 0: # 0 Angriff reicht nur mit Elementvorteil
|
||||
if cr.power() + matchup_bonus(cr, target) <= 0: # 0 Angriff macht auch im Vorteil nichts
|
||||
return [false, "%s würde keinen Schaden machen" % cr.name]
|
||||
if not (target is Creature) and not tgt_owner.board.is_empty():
|
||||
return [false, "Erst die Wesen"]
|
||||
@@ -609,17 +558,18 @@ func attack(cr: Creature, target) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
## Elementvorteil beim Zuschlagen und beim Konter: im Vorteil zählt der Stapelbonus des Ziels
|
||||
## nicht (so viel Schaden mehr), im Nachteil der eigene nicht (so viel weniger). Gegen Helden
|
||||
## gibt es keinen.
|
||||
## Elementvorteil beim Zuschlagen und beim Konter, als Verschiebung des Schadens: im Vorteil
|
||||
## doppelt (+ Angriff), im Nachteil halb, aufgerundet (− Angriff / 2). Gegen Helden gibt es keinen;
|
||||
## mit dem Merkmal kein_nachteil entfällt der Nachteil.
|
||||
static func matchup_bonus(source: Creature, target) -> int:
|
||||
if not (target is Creature):
|
||||
return 0
|
||||
var m := Cards.matchup(source.card.elements, target.card.elements)
|
||||
if m > 0:
|
||||
return target.aura
|
||||
return source.power()
|
||||
if m < 0:
|
||||
return -source.aura
|
||||
var o = source.owner()
|
||||
return 0 if o != null and "kein_nachteil" in o.perks else -(source.power() / 2)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -634,24 +584,17 @@ func damage(target, amount: int) -> void:
|
||||
emit({"type": "damage", "target": ref_of(target), "amount": amount})
|
||||
|
||||
|
||||
## Räumt Tote ab; danach gelten die Elementboni neu, was weitere Tote nach sich ziehen kann.
|
||||
## Räumt Tote ab; wer besiegt ist, nimmt seine Wesen mit vom Tisch.
|
||||
func cleanup() -> void:
|
||||
for round in 8:
|
||||
var died := false
|
||||
for p in players:
|
||||
for c in p.board.duplicate():
|
||||
# Wer besiegt ist, nimmt seine Wesen mit vom Tisch.
|
||||
if c.health <= 0 or not p.alive():
|
||||
p.board.erase(c)
|
||||
if c.health > 0:
|
||||
say("%s verschwindet mit %s." % [c.name, p.name])
|
||||
else:
|
||||
say("%s stirbt." % c.name)
|
||||
emit({"type": "death", "target": ref_of(c)})
|
||||
died = true
|
||||
if not died:
|
||||
return
|
||||
apply_auras()
|
||||
for p in players:
|
||||
for c in p.board.duplicate():
|
||||
if c.health <= 0 or not p.alive():
|
||||
p.board.erase(c)
|
||||
if c.health > 0:
|
||||
say("%s verschwindet mit %s." % [c.name, p.name])
|
||||
else:
|
||||
say("%s stirbt." % c.name)
|
||||
emit({"type": "death", "target": ref_of(c)})
|
||||
|
||||
|
||||
## Ende: einer von beiden hat kein Leben mehr.
|
||||
@@ -676,11 +619,11 @@ static func shuffle(arr: Array, r: RandomNumberGenerator) -> void:
|
||||
arr[j] = tmp
|
||||
|
||||
|
||||
## Duell gegen einen zufälligen Gegner. rules: {"human_perks": [...], "opponent_life": int, "looks": {...}, "deck": [ids]}
|
||||
## Duell gegen einen zufälligen Gegner (Tests, Simulation). rules: {"human_perks": [...], "looks": {...}, "deck": [ids]}
|
||||
static func new_battle(r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game:
|
||||
if r == null:
|
||||
r = make_rng()
|
||||
return new_battle_with(DeckGen.random_opponent(int(rules.get("opponent_life", START_LIFE))), r, false, rules)
|
||||
return new_battle_with(DeckGen.random_opponent(), r, false, rules)
|
||||
|
||||
|
||||
static func new_battle_with(opp: Player, r: RandomNumberGenerator = null, fixed_order: bool = false, rules: Dictionary = {}) -> Game:
|
||||
|
||||
+185
-46
@@ -1,24 +1,30 @@
|
||||
class_name Profile
|
||||
extends RefCounted
|
||||
## Spielstand: Münzen, Staub, Sammlung, Stapel, Kampagne, Einstellungen.
|
||||
## Man startet mit einem von vier Startdecks (je ein Element im Mittelpunkt); Booster und die
|
||||
## Werkstatt bringen den Rest. Jede eigene Karte hat eine Seltenheitsstufe, die +1/+1 je Stufe
|
||||
## Man startet mit allen neutralen Karten (Stapel: 30 nach Manakurve); die Elemente schaltet die Kampagne frei (je Kapitel einen
|
||||
## Booster), die Werkstatt bringt den Rest. Jede eigene Karte hat eine Seltenheitsstufe, die +1/+1 je Stufe
|
||||
## gibt (Cards.Card.leveled); kommt eine seltenere Version, ersetzt sie die alte, die zu Staub wird.
|
||||
|
||||
const SAVE_VERSION := 7
|
||||
const SAVE_VERSION := 8
|
||||
const CRAFT_COST := 200 # Staub für eine neue Karte (Common)
|
||||
const SAVE_PATH := "user://spielstand.json"
|
||||
const BOOSTER_COST := 100
|
||||
const BOOSTER_COST := 100 # neutraler Booster
|
||||
const ELEMENT_BOOSTER_COST := 200 # Booster eines Elements (ab seinem Kapitel)
|
||||
const BOOSTER_SIZE := 5
|
||||
const RARITY_WEIGHTS := {"Common": 45.0, "Rare": 25.0, "Super Rare": 15.0, "Ultra Rare": 10.0, "Ultimate Rare": 5.0}
|
||||
const START_COINS := 150
|
||||
const REWARD_WIN := 100
|
||||
const REWARD_LOSS := 50
|
||||
# Schwierigkeit: Leben der Gegner, KI und Münzen.
|
||||
# Münzen nach dem Kampf: je eigener Zug, je ausgespielte Karte und je Zug unter FAST_TURN_SEC
|
||||
# Sekunden; ein Sieg verdoppelt alles, die Schwierigkeit multipliziert (siehe battle_coins).
|
||||
const COINS_TURN := 5
|
||||
const COINS_CARD := 5
|
||||
const COINS_FAST := 5
|
||||
const FAST_TURN_SEC := 15.0
|
||||
# Schwierigkeit im Einzelspieler: Stufe der Gegnerkarten relativ zu deiner Deckstärke (level),
|
||||
# KI (smart: Vorausschau, sonst gierig), ein zufälliges Merkmal (perk) und der Münzfaktor.
|
||||
const DIFFICULTIES := {
|
||||
"leicht": {"name": "Leicht", "life": 25, "reward": 0.75, "smart": false},
|
||||
"normal": {"name": "Normal", "life": 30, "reward": 1.0, "smart": true},
|
||||
"schwer": {"name": "Schwer", "life": 35, "reward": 1.5, "smart": true},
|
||||
"leicht": {"name": "Leicht", "level": -1, "smart": false, "perk": false, "reward": 1.0},
|
||||
"normal": {"name": "Normal", "level": 0, "smart": true, "perk": false, "reward": 1.5},
|
||||
"schwer": {"name": "Schwer", "level": 1, "smart": true, "perk": true, "reward": 2.0},
|
||||
}
|
||||
# Sammlung: eine Stufe abgeben bringt Staub, eine Stufe aufwerten kostet Staub.
|
||||
const DUST_DISMANTLE := {"Common": 50, "Rare": 100, "Super Rare": 200, "Ultra Rare": 500, "Ultimate Rare": 1000}
|
||||
@@ -26,6 +32,8 @@ 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 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)
|
||||
var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen
|
||||
@@ -34,24 +42,20 @@ var notices: Array[String] = [] # Meldungen für die Oberfläche (Kampagnenfort
|
||||
var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1)
|
||||
|
||||
|
||||
## Neues Profil: noch ohne Karten, das Startdeck wählt der Spieler (choose_start).
|
||||
## Neues Profil: alle neutralen Karten in der Sammlung, 30 davon nach Manakurve im Stapel.
|
||||
static func starter() -> Profile:
|
||||
var p := Profile.new()
|
||||
p.coins = START_COINS
|
||||
p.give_start()
|
||||
return p
|
||||
|
||||
|
||||
## Hat der Spieler schon ein Startdeck gewählt?
|
||||
func started() -> bool:
|
||||
return not collection.is_empty()
|
||||
|
||||
|
||||
## Das Startdeck eines Elements wird Sammlung und Stapel.
|
||||
func choose_start(element: String) -> void:
|
||||
## Alle neutralen Karten in Common werden die Sammlung, ein Kurvenstapel daraus der Stapel.
|
||||
func give_start() -> void:
|
||||
collection = {}
|
||||
deck = DeckGen.start_deck(element)
|
||||
for id in deck:
|
||||
for id in DeckGen.element_deck(Cards.NEUTRAL):
|
||||
collection[id] = 0
|
||||
deck = DeckGen.curve_deck(Cards.NEUTRAL, Game.make_rng())
|
||||
|
||||
|
||||
## Lädt den Spielstand; bei Schaden die Sicherung; sonst ein neues Profil.
|
||||
@@ -74,6 +78,11 @@ static func from_dict(d: Dictionary) -> Profile:
|
||||
var p := Profile.new()
|
||||
p.coins = int(d.get("coins", 0))
|
||||
p.dust = int(d.get("dust", 0))
|
||||
p.xp = int(d.get("xp", 0))
|
||||
var sk = d.get("skills", {})
|
||||
for id in sk:
|
||||
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))
|
||||
if int(d.get("version", 1)) >= SAVE_VERSION:
|
||||
var coll = d.get("collection", {})
|
||||
@@ -85,6 +94,8 @@ static func from_dict(d: Dictionary) -> Profile:
|
||||
if p.collection.has(cid) and not (cid in ids):
|
||||
ids.append(cid)
|
||||
p.deck = ids.slice(0, Game.DECK_MAX)
|
||||
if p.collection.is_empty():
|
||||
p.give_start()
|
||||
var se = d.get("settings", {})
|
||||
p.settings = {"volume": float(se.get("volume", 0.0)), "music": float(se.get("music", 0.0)),
|
||||
"ai_speed": float(se.get("ai_speed", 1.0)), "animations": bool(se.get("animations", true)),
|
||||
@@ -99,8 +110,8 @@ static func from_dict(d: Dictionary) -> Profile:
|
||||
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "collection": collection, "deck": deck,
|
||||
"campaign_stage": campaign_stage, "settings": settings, "perk": perk}
|
||||
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "xp": xp, "skills": skills, "collection": collection,
|
||||
"deck": deck, "campaign_stage": campaign_stage, "settings": settings, "perk": perk}
|
||||
|
||||
|
||||
## Speichert und sichert den vorherigen Stand als .bak.
|
||||
@@ -182,7 +193,7 @@ func dismantle(cid: String) -> String:
|
||||
if not owns(cid):
|
||||
return "Karte nicht in der Sammlung"
|
||||
var lvl := level(cid)
|
||||
dust += DUST_DISMANTLE[Cards.RARITIES[lvl]]
|
||||
dust += dust_gain(DUST_DISMANTLE[Cards.RARITIES[lvl]])
|
||||
if lvl == 0:
|
||||
collection.erase(cid)
|
||||
deck.erase(cid)
|
||||
@@ -204,11 +215,21 @@ func craft(cid: String) -> String:
|
||||
|
||||
# --- Booster ----------------------------------------------------------------------
|
||||
|
||||
## Fünf Karten mit gewürfelter Seltenheit.
|
||||
static func roll_booster(r: RandomNumberGenerator) -> Array:
|
||||
## Preis eines Boosters: neutral BOOSTER_COST, je Element ELEMENT_BOOSTER_COST.
|
||||
static func booster_cost(element: String) -> int:
|
||||
return BOOSTER_COST if element == Cards.NEUTRAL else ELEMENT_BOOSTER_COST
|
||||
|
||||
|
||||
## Der Booster eines Elements ist zu haben, sobald sein Kampagnenkapitel offen ist (neutral immer).
|
||||
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:
|
||||
var rarities := RARITY_WEIGHTS.keys()
|
||||
var weights := PackedFloat32Array(RARITY_WEIGHTS.values())
|
||||
var pool := Cards.all()
|
||||
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)]
|
||||
@@ -218,16 +239,29 @@ static func roll_booster(r: RandomNumberGenerator) -> Array:
|
||||
return out
|
||||
|
||||
|
||||
## Öffnet einen Booster. Neue Karten kommen in Sammlung und Stapel (solange Platz ist). Von jeder
|
||||
## Karte zählt die seltenste Version: eine höhere ersetzt die alte, die zu Staub wird; eine gleiche
|
||||
## oder niedrigere wird selbst zu Staub. Gibt die Ergebnisse zurück, leer wenn die Münzen nicht reichen.
|
||||
func open_booster(r: RandomNumberGenerator = null) -> Array:
|
||||
if coins < BOOSTER_COST:
|
||||
## Öffnet einen Booster des Elements. Neue Karten kommen in Sammlung und Stapel (solange Platz ist).
|
||||
## Von jeder Karte zählt die seltenste Version: eine höhere ersetzt die alte, die zu Staub wird; eine
|
||||
## gleiche oder niedrigere wird selbst zu Staub. Gibt die Ergebnisse zurück, leer wenn der Booster
|
||||
## noch gesperrt ist oder die Münzen nicht reichen.
|
||||
func open_booster(element: String, r: RandomNumberGenerator = null) -> Array:
|
||||
return open_boosters(element, 1, r)
|
||||
|
||||
|
||||
## Öffnet n Booster des Elements auf einmal; leer, wenn gesperrt oder die Münzen für alle nicht reichen.
|
||||
func open_boosters(element: String, n: int, r: RandomNumberGenerator = null) -> Array:
|
||||
if not booster_unlocked(element) or coins < pack_price(element) * n:
|
||||
return []
|
||||
if r == null:
|
||||
r = Game.make_rng()
|
||||
coins -= BOOSTER_COST
|
||||
var out := roll_booster(r)
|
||||
var out := []
|
||||
for i in n:
|
||||
out += _open_one(element, r)
|
||||
return out
|
||||
|
||||
|
||||
func _open_one(element: String, r: RandomNumberGenerator) -> Array:
|
||||
coins -= pack_price(element)
|
||||
var out := roll_booster(r, element, skill("packglueck"))
|
||||
for res in out:
|
||||
var lvl: int = Cards.RARITIES.find(res.rarity)
|
||||
res["new"] = not owns(res.card.id)
|
||||
@@ -239,33 +273,138 @@ func open_booster(r: RandomNumberGenerator = null) -> Array:
|
||||
deck.append(res.card.id)
|
||||
elif lvl > level(res.card.id):
|
||||
res["upgraded"] = true
|
||||
res["dust"] = DUST_DISMANTLE[rarity(res.card.id)]
|
||||
res["dust"] = dust_gain(DUST_DISMANTLE[rarity(res.card.id)])
|
||||
collection[res.card.id] = lvl
|
||||
else:
|
||||
res["dust"] = DUST_DISMANTLE[res.rarity]
|
||||
res["dust"] = dust_gain(DUST_DISMANTLE[res.rarity])
|
||||
dust += res.dust
|
||||
return out
|
||||
|
||||
|
||||
# --- Kampf und Kampagne -----------------------------------------------------------
|
||||
|
||||
## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück.
|
||||
## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei.
|
||||
func record_battle(won: bool, stage: int = -1, reward_factor: float = 1.0) -> int:
|
||||
var reward := REWARD_LOSS
|
||||
## Münzen eines Kampfes: COINS_TURN je eigenem Zug, COINS_CARD je ausgespielter Karte, COINS_FAST
|
||||
## je schnellem Zug; ein Sieg verdoppelt, factor ist der Münzfaktor der Schwierigkeit.
|
||||
static func battle_coins(won: bool, turns: int, cards: int, fast: int, factor: float = 1.0) -> int:
|
||||
var pts := COINS_TURN * turns + COINS_CARD * cards + COINS_FAST * fast
|
||||
if won:
|
||||
reward = int(round(REWARD_WIN * reward_factor))
|
||||
if stage >= 0 and stage == campaign_stage:
|
||||
campaign_stage += 1
|
||||
reward = Campaign.stage(stage).reward
|
||||
notices.append("Kampagne: %s besiegt" % Campaign.stage(stage).name)
|
||||
pts *= 2
|
||||
return int(round(pts * factor))
|
||||
|
||||
|
||||
## Verbucht ein Kampfergebnis mit den verdienten Münzen (battle_coins, mal Münzsegen) und gibt die
|
||||
## Gutschrift zurück; sie zählt auch als Erfahrung, ein neues Level meldet sich. stage >= 0:
|
||||
## Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei und bringt ihre Belohnung obendrauf.
|
||||
func record_battle(won: bool, earned: int, stage: int = -1) -> int:
|
||||
var reward := int(round(earned * coin_factor()))
|
||||
if won and stage >= 0 and stage == campaign_stage:
|
||||
campaign_stage += 1
|
||||
reward += Campaign.stage(stage).reward
|
||||
notices.append("Kampagne: %s besiegt" % Campaign.stage(stage).name)
|
||||
coins += reward
|
||||
var before := player_level()
|
||||
xp += reward
|
||||
if player_level() > before:
|
||||
var gained := player_level() - before
|
||||
notices.append(("Level %d erreicht: +%d Skillpunkt" if gained == 1 else "Level %d erreicht: +%d Skillpunkte") % [player_level(), gained])
|
||||
return reward
|
||||
|
||||
|
||||
# --- Skilltree ----------------------------------------------------------------------
|
||||
|
||||
## Spielerlevel aus der Erfahrung (nicht die Kartenstufe level(cid)).
|
||||
func player_level() -> int:
|
||||
return Skills.level_for(xp)
|
||||
|
||||
|
||||
## Gelernte Stufe eines Skills.
|
||||
func skill(id: String) -> int:
|
||||
return int(skills.get(id, 0))
|
||||
|
||||
|
||||
## Freie Skillpunkte: ein Punkt je Level, abzüglich der gelernten Stufen.
|
||||
func skill_points() -> int:
|
||||
var spent := 0
|
||||
for id in skills:
|
||||
spent += int(skills[id])
|
||||
return player_level() - spent
|
||||
|
||||
|
||||
## Lernt die nächste Stufe eines Skills für einen Punkt. 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"
|
||||
if skill_points() <= 0:
|
||||
return "Kein Skillpunkt frei"
|
||||
skills[id] = skill(id) + 1
|
||||
return ""
|
||||
|
||||
|
||||
## Preis eines Boosters mit Händler-Rabatt.
|
||||
func pack_price(element: String) -> int:
|
||||
return int(round(booster_cost(element) * (1.0 - 0.1 * skill("packpreis"))))
|
||||
|
||||
|
||||
func coin_factor() -> float:
|
||||
return 1.0 + 0.1 * skill("muenzen")
|
||||
|
||||
|
||||
## Staub mit Staubsammler-Bonus.
|
||||
func dust_gain(base: int) -> int:
|
||||
return int(round(base * (1.0 + 0.25 * skill("staub"))))
|
||||
|
||||
|
||||
## Merkmale im Kampf: das gewählte (nach Kapitel 1) und die der Kampfskills.
|
||||
func battle_perks() -> Array:
|
||||
var out := []
|
||||
if perk != "" and perk_unlocked():
|
||||
out.append(perk)
|
||||
for id in Skills.PERK_OF:
|
||||
if skill(id) > 0 and not (Skills.PERK_OF[id] in out):
|
||||
out.append(Skills.PERK_OF[id])
|
||||
return out
|
||||
|
||||
|
||||
func difficulty() -> Dictionary:
|
||||
return DIFFICULTIES.get(settings.get("difficulty", "normal"), DIFFICULTIES.normal)
|
||||
|
||||
|
||||
## Deine Stufe: die Deckstärke des Kampfstapels (100 Common … 500 Ultimate Rare) als Seltenheitsstufe.
|
||||
func deck_level() -> int:
|
||||
return clampi(roundi((DeckGen.strength(deck_cards(), collection) - 100) / 100.0), 0, Cards.RARITIES.size() - 1)
|
||||
|
||||
|
||||
## Stufe der Gegnerkarten im Einzelspieler: deine Stufe plus den Versatz der Schwierigkeit.
|
||||
func opponent_level() -> int:
|
||||
return clampi(deck_level() + int(difficulty().level), 0, Cards.RARITIES.size() - 1)
|
||||
|
||||
|
||||
## Elemente der offenen Kampagnenkapitel (Neutral immer).
|
||||
func unlocked_elements() -> Array:
|
||||
return Campaign.CHAPTER_ELEMENTS.slice(0, campaign_stage / Campaign.STAGES_PER_CHAPTER + 1)
|
||||
|
||||
|
||||
## Alle Karten der offenen Elemente: was Sammlung, Deck und Booster zeigen.
|
||||
func unlocked_cards() -> Array:
|
||||
var open := unlocked_elements()
|
||||
return Cards.all().filter(func(c): return c.elements[0] in open)
|
||||
|
||||
|
||||
## Einzelspieler-Gegner: ein Element aus den offenen Kapiteln, 30 Karten nach Manakurve in opponent_level;
|
||||
## auf Schwer ein zufälliges Merkmal. Leben wie deins.
|
||||
func single_opponent(r: RandomNumberGenerator) -> Game.Player:
|
||||
var elements := unlocked_elements()
|
||||
var element: String = elements[r.randi_range(0, elements.size() - 1)]
|
||||
var opp := Game.Player.new("Gegner", false, Game.START_LIFE, element)
|
||||
opp.deck = DeckGen.curve_deck(element, r)
|
||||
var level := opponent_level()
|
||||
for id in opp.deck:
|
||||
opp.looks[id] = level
|
||||
if bool(difficulty().perk):
|
||||
var keys: Array = Game.PERKS.keys()
|
||||
opp.perks = [keys[r.randi_range(0, keys.size() - 1)]]
|
||||
return opp
|
||||
|
||||
|
||||
func perk_unlocked() -> bool:
|
||||
return campaign_stage >= Campaign.STAGES.size()
|
||||
return campaign_stage >= Campaign.STAGES_PER_CHAPTER
|
||||
@@ -0,0 +1,62 @@
|
||||
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.
|
||||
|
||||
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"},
|
||||
}
|
||||
## Kampfskills 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}
|
||||
|
||||
|
||||
## Erfahrung, die Level n verlangt: 100, 300, 600, 1000, 1500 … (50 · n · (n + 1)).
|
||||
static func xp_for_level(n: int) -> int:
|
||||
return 50 * n * (n + 1)
|
||||
|
||||
|
||||
## Level zu einer Erfahrung.
|
||||
static func level_for(xp: int) -> int:
|
||||
var n := 0
|
||||
while xp >= xp_for_level(n + 1):
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
static func in_branch(branch: String) -> Array:
|
||||
return SKILLS.keys().filter(func(id): return SKILLS[id].branch == branch)
|
||||
|
||||
|
||||
## Boosterchancen je Stufe (Reihenfolge Cards.RARITIES) mit Packglück luck.
|
||||
static func pack_weights(luck: int) -> PackedFloat32Array:
|
||||
var out := PackedFloat32Array()
|
||||
for rar in Cards.RARITIES:
|
||||
out.append(float(Profile.RARITY_WEIGHTS[rar]) + luck * float(LUCK_SHIFT[rar]))
|
||||
return out
|
||||
|
||||
|
||||
## Konkrete Wirkung eines Skills auf einer Stufe, als Text.
|
||||
static func effect(id: String, rank: int) -> String:
|
||||
match id:
|
||||
"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])]
|
||||
"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))]
|
||||
"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))
|
||||
return ""
|
||||
@@ -0,0 +1 @@
|
||||
uid://bbcmk1gtfjoim
|
||||
@@ -68,6 +68,6 @@ static func preview_rect(_anchor: Rect2) -> Rect2:
|
||||
return Rect2(Vector2(1070, 278), PREVIEW_SIZE)
|
||||
|
||||
|
||||
## Zusatzzeile (Elementbonus, Eigenschaften) links unter den Elementzählern.
|
||||
## Zusatzzeile (Merkmale, Eigenschaften) links unter den Elementpaaren.
|
||||
static func info_rect(_preview: Rect2) -> Rect2:
|
||||
return INFO
|
||||
+53
-27
@@ -31,7 +31,10 @@ var ai_timer := 0.0
|
||||
var ai_task := -1 # WorkerThreadPool-Aufgabe der laufenden KI-Berechnung
|
||||
var ai_copy: Game # Spielkopie, auf der die KI rechnet
|
||||
var ai_box := {} # Ergebnis der Berechnung (vom Thread befüllt)
|
||||
var reward := -1
|
||||
var reward := -1 # gutgeschriebene Münzen (mit Kampagnenbonus)
|
||||
var earned := 0 # Münzen aus Zügen, Karten und Tempo (Profile.battle_coins)
|
||||
var fast_turns := 0 # eigene Züge unter Profile.FAST_TURN_SEC
|
||||
var turn_start := 0 # Beginn des laufenden eigenen Zugs (ms)
|
||||
var ended := false
|
||||
var pad: BattleGamepad
|
||||
var gp_zone: String:
|
||||
@@ -72,7 +75,7 @@ var energy_label: Label
|
||||
var msg_label: Label
|
||||
var element_view: ElementView # eigene Elementleiste
|
||||
var opp_element_view: ElementView # die des Gegners darüber
|
||||
var info_label: Label # Vorschau: Elementbonus, Merkmale
|
||||
var info_label: Label # Vorschau: Merkmale, Eigenschaften
|
||||
var menu_layer: Control # Kartenmenü (Rechtsklick)
|
||||
var menu_target = null # Karte oder Wesen, für das das Menü offen ist
|
||||
var arrivals := {} # uid -> Rect2: Wesen, das gerade aus der Hand aufs Brett fliegt
|
||||
@@ -81,6 +84,7 @@ var banner_turn := -1 # Zug, für den die Zugansage schon lief
|
||||
var overlay: Control
|
||||
var result_label: Label
|
||||
var reward_label: Label
|
||||
var detail_label: Label # Aufschlüsselung der Münzen
|
||||
var notice_label: Label
|
||||
var confirm: Control
|
||||
var cursor_layer: Control
|
||||
@@ -99,23 +103,20 @@ func _ready() -> void:
|
||||
pad = BattleGamepad.new(self)
|
||||
ai_delay = 0.75 / float(s.ai_speed)
|
||||
_build_static()
|
||||
if stage >= 0:
|
||||
var r := Game.make_rng()
|
||||
game = Game.new_battle_with(Campaign.opponent(stage, r), r, false, own_rules())
|
||||
else:
|
||||
game = Game.new_battle(Game.make_rng(), own_rules())
|
||||
var r := Game.make_rng()
|
||||
var opp: Game.Player = Campaign.opponent(stage) if stage >= 0 else app.profile.single_opponent(r)
|
||||
game = Game.new_battle_with(opp, r, false, own_rules())
|
||||
smart_ai = bool(app.profile.difficulty().smart)
|
||||
_begin()
|
||||
|
||||
|
||||
## Eigener Stapel und Kartenstufen, Gegnerleben (Schwierigkeit) und das eigene Merkmal (nach Kapitel 1).
|
||||
## 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()}
|
||||
if stage < 0:
|
||||
rules["opponent_life"] = int(p.difficulty().life)
|
||||
if p.perk != "" and p.perk_unlocked():
|
||||
rules["human_perks"] = [p.perk]
|
||||
var perks := p.battle_perks()
|
||||
if not perks.is_empty():
|
||||
rules["human_perks"] = perks
|
||||
return rules
|
||||
|
||||
|
||||
@@ -224,7 +225,8 @@ func _build_static() -> void:
|
||||
overlay = _overlay()
|
||||
result_label = Style.label(overlay, "", 56, Style.GREEN, Rect2(0, 250, 1280, 80), "center", true)
|
||||
reward_label = Style.label(overlay, "", 26, Style.GOLD, Rect2(0, 335, 1280, 40), "center")
|
||||
notice_label = Style.label(overlay, "", 18, Style.GREEN, Rect2(0, 385, 1280, 80), "center")
|
||||
detail_label = Style.label(overlay, "", 17, Style.MUTED, Rect2(0, 372, 1280, 28), "center")
|
||||
notice_label = Style.label(overlay, "", 18, Style.GREEN, Rect2(0, 405, 1280, 70), "center")
|
||||
Style.button(overlay, "Zurück zum Menü", Rect2(530, 480, 220, 50), leave, Color("285a3c"))
|
||||
confirm = Style.confirm(self, "Wirklich aufgeben?", "", "Ja, aufgeben", leave, "Weiterspielen")
|
||||
|
||||
@@ -459,8 +461,11 @@ func _exit_tree() -> void:
|
||||
|
||||
func _finish() -> void:
|
||||
var won := game.winner == "human"
|
||||
if game.current() == game.human:
|
||||
_close_turn() # der Zug, der das Spiel entschied
|
||||
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
|
||||
reward = app.profile.record_battle(won, stage, factor)
|
||||
earned = Profile.battle_coins(won, game.human.turns, game.human.cards_played, fast_turns, factor)
|
||||
reward = app.profile.record_battle(won, earned, stage)
|
||||
app.save()
|
||||
app.sfx.play("win" if won else "lose")
|
||||
refresh()
|
||||
@@ -777,6 +782,7 @@ func end_turn() -> void:
|
||||
return
|
||||
cancel()
|
||||
clear_undo()
|
||||
_close_turn()
|
||||
game.end_turn()
|
||||
ai_timer = ai_delay
|
||||
after_action()
|
||||
@@ -839,11 +845,36 @@ func on_tile_click(cr: Game.Creature) -> void:
|
||||
on_click("enemy", cr)
|
||||
|
||||
|
||||
## Zugansage, wenn der eigene Zug beginnt.
|
||||
## Zählt den laufenden eigenen Zug als schnell, wenn er unter Profile.FAST_TURN_SEC blieb.
|
||||
func _close_turn() -> void:
|
||||
if Time.get_ticks_msec() - turn_start < Profile.FAST_TURN_SEC * 1000:
|
||||
fast_turns += 1
|
||||
|
||||
|
||||
## Aufschlüsselung der Münzen fürs Ergebnisbild.
|
||||
func reward_text() -> String:
|
||||
var h := game.human
|
||||
var s := "%s · %s · %s = %d" % [tr("%d Züge") % h.turns, tr("%d Karten") % h.cards_played, tr("%d schnelle Züge") % fast_turns,
|
||||
Profile.COINS_TURN * h.turns + Profile.COINS_CARD * h.cards_played + Profile.COINS_FAST * fast_turns]
|
||||
if game.winner == "human":
|
||||
s += " · " + tr("Sieg ×2")
|
||||
var d: Dictionary = app.profile.difficulty()
|
||||
if stage < 0 and float(d.reward) != 1.0:
|
||||
s += " · %s %s" % [tr(d.name), Style.factor(float(d.reward))]
|
||||
var boosted := int(round(earned * app.profile.coin_factor()))
|
||||
if boosted != earned:
|
||||
s += " · %s %s" % [tr("Münzsegen"), Style.factor(app.profile.coin_factor())]
|
||||
if reward > boosted:
|
||||
s += " · " + tr("Kampagne +%d") % (reward - boosted)
|
||||
return s
|
||||
|
||||
|
||||
## Zugansage, wenn der eigene Zug beginnt; ab hier läuft die Zeit für den schnellen Zug.
|
||||
func announce_turn() -> void:
|
||||
if game == null or not my_turn() or banner_turn == game.turn:
|
||||
return
|
||||
banner_turn = game.turn
|
||||
turn_start = Time.get_ticks_msec()
|
||||
fx.banner(tr("Dein Zug"), Style.GOLD)
|
||||
fx.sfx("turn")
|
||||
|
||||
@@ -871,23 +902,17 @@ func show_preview(card, entered: bool, owner_looks: Dictionary = {}, info: Strin
|
||||
info_label.size = ir.size
|
||||
|
||||
|
||||
## Eigene Handkarte: zeigt den Elementbonus, den sie offen gelegt bekäme.
|
||||
## Eigene Handkarte in der Vorschau.
|
||||
func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2()) -> void:
|
||||
if drag_ghost != null:
|
||||
return
|
||||
var a := game.forecast(perspective, card)
|
||||
var info := ""
|
||||
if a > 0:
|
||||
info = tr("Elementbonus +%d") % a
|
||||
show_preview(card, entered, perspective.looks, info, anchor)
|
||||
show_preview(card, entered, perspective.looks, "", anchor)
|
||||
|
||||
|
||||
## Was ein Wesen gerade hebt, was es kann und ob es schon angreift.
|
||||
func creature_info(cr: Game.Creature) -> String:
|
||||
var parts: Array[String] = []
|
||||
if cr.aura > 0:
|
||||
parts.append(tr("Elementbonus +%d") % cr.aura)
|
||||
var perk := cr.power() - cr.attack - cr.aura
|
||||
var perk := cr.power() - cr.attack
|
||||
if perk > 0:
|
||||
parts.append(tr("Merkmal +%d") % perk)
|
||||
if cr.card.is_neutral():
|
||||
@@ -902,10 +927,11 @@ func creature_info(cr: Game.Creature) -> String:
|
||||
return " · ".join(parts)
|
||||
|
||||
|
||||
## Elementvorteil des gewählten Angreifers gegen ein mögliches Ziel; gegen Verdeckte unbekannt.
|
||||
## Konter eines möglichen Ziels gegen den gewählten Angreifer (Verschiebung durch sein Element);
|
||||
## gegen Verdeckte unbekannt.
|
||||
func matchup_for(cr: Game.Creature) -> int:
|
||||
if selected != null and not cr.hidden and game.can_attack(selected, cr)[0]:
|
||||
return Game.matchup_bonus(selected, cr)
|
||||
return Game.matchup_bonus(cr, selected)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -1139,7 +1165,6 @@ func refresh() -> void:
|
||||
fx.glide(v, hand_arrivals[card])
|
||||
hand_arrivals.erase(card)
|
||||
v.lift = mine
|
||||
v.bonus = g.forecast(h, card)
|
||||
if fuse_first != null and mine and g.can_fuse(h, fuse_first, card)[0]:
|
||||
v.border = Style.PURPLE
|
||||
elif card == fuse_first:
|
||||
@@ -1185,6 +1210,7 @@ func refresh() -> void:
|
||||
result_label.text = "Sieg!" if won else "Niederlage"
|
||||
result_label.add_theme_color_override("font_color", Style.GREEN if won else Style.RED)
|
||||
reward_label.text = "+%d Münzen" % maxi(reward, 0)
|
||||
detail_label.text = reward_text()
|
||||
var notes: Array[String] = app.profile.take_notices()
|
||||
if not notes.is_empty():
|
||||
notice_label.text = "\n".join(notes)
|
||||
|
||||
+165
-36
@@ -1,19 +1,32 @@
|
||||
class_name BoosterScreen
|
||||
extends Control
|
||||
## Booster kaufen und öffnen: unten die Pack-Kacheln der offenen Kapitel (Neutral und die Elemente in
|
||||
## Kapitelreihenfolge, gesperrte sind unsichtbar) mit ×1, ×5, ×10; die Karten fliegen aus dem Pack in ein Raster
|
||||
## (ein Pack: fünf große, mehrere: zehn je Reihe, beste Stufe zuerst) und liegen verdeckt. Klick
|
||||
## oder „Alle aufdecken“ dreht sie gestaffelt um, seltene blitzen auf; danach steht das Fazit.
|
||||
|
||||
const CARD_W := 200
|
||||
const CARD_H := 280
|
||||
const SMALL_W := 108
|
||||
const SMALL_H := 151
|
||||
const AREA := Rect2(40, 100, 1200, 460)
|
||||
const QUANTITIES := [1, 5, 10]
|
||||
|
||||
var app
|
||||
var cards: Array = [] # {"card", "rarity", "upgraded", "dust"} je Karte
|
||||
var cards: Array = [] # {"card", "rarity", "new", "upgraded", "dust"} je Karte
|
||||
var revealed := {}
|
||||
var coins_label: Label
|
||||
var msg_label: Label
|
||||
var b_buy: Button
|
||||
var summary_label: Label
|
||||
var b_buy: Button # Neutral ×1 (Tests)
|
||||
var b_all: Button
|
||||
var scroll: ScrollContainer
|
||||
var slots: Control
|
||||
var fx_layer: Control
|
||||
var fresh := {} # gerade aufgedeckt: wird umgedreht
|
||||
var pack_tiles := {} # Element -> PackTile (Kapitelreihenfolge)
|
||||
var pack_buttons := {} # Element -> der ×1-Knopf
|
||||
var fresh := {} # gerade aufgedeckt: Index -> Verzögerung fürs Umdrehen
|
||||
var fly_from := Vector2.ZERO # woher die Karten einfliegen (Mitte der gekauften Kachel)
|
||||
var fly := false # beim nächsten refresh einfliegen
|
||||
var last_pack := "" # zuletzt gekauftes Pack (pulsiert)
|
||||
|
||||
|
||||
class CardBack extends Control:
|
||||
@@ -39,28 +52,56 @@ class CardBack extends Control:
|
||||
var rect := Rect2(Vector2.ZERO, size)
|
||||
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD, 5 if has_focus() else 3), rect)
|
||||
Style.border(self, rect.grow(-15), Color("5a3c82"), 2, 6)
|
||||
Style.text(self, "?", 48, Style.GOLD, size / 2, "center", true)
|
||||
Style.text(self, "?", 48 if size.x >= 180 else 30, Style.GOLD, size / 2, "center", true)
|
||||
|
||||
|
||||
func slot_rect(i: int) -> Rect2:
|
||||
var gap := 20
|
||||
var x0 := (1280 - Profile.BOOSTER_SIZE * CARD_W - (Profile.BOOSTER_SIZE - 1) * gap) / 2
|
||||
return Rect2(x0 + i * (CARD_W + gap), 230, CARD_W, CARD_H)
|
||||
## Pack-Kachel eines Elements: Symbol, Name, Preis; die Kaufknöpfe hängt der Bildschirm an.
|
||||
class PackTile extends Control:
|
||||
var element: String
|
||||
var locked := false
|
||||
var hint := ""
|
||||
var pulse := false
|
||||
var buy_buttons: Array = []
|
||||
|
||||
func _init(e: String, rect: Rect2) -> void:
|
||||
element = e
|
||||
position = rect.position
|
||||
size = rect.size
|
||||
pivot_offset = rect.size / 2
|
||||
|
||||
func _draw() -> void:
|
||||
var col: Color = Cards.ELEMENT_COLORS[element]
|
||||
draw_style_box(Style.box(Style.darker(col, 0.5), 10, Style.GOLD if pulse else Color(col, 0.7), 2), Rect2(Vector2.ZERO, size))
|
||||
Style.element_icon(self, element, Vector2(32, 40), 34, Style.lighter(col, 0.4))
|
||||
Style.text(self, tr(element), 16, Style.TEXT, Vector2(60, 30), "midleft", true)
|
||||
Style.text(self, hint, 13, Style.MUTED, Vector2(60, 52), "midleft")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Style.header(self, "Booster", func(): app.show_screen(MenuScreen.new()))
|
||||
coins_label = Style.chip(self, "", Rect2(860, 26, 240, 38))
|
||||
fx_layer = Control.new()
|
||||
fx_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(fx_layer)
|
||||
scroll = ScrollContainer.new()
|
||||
scroll.position = AREA.position
|
||||
scroll.size = AREA.size
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
add_child(scroll)
|
||||
slots = Control.new()
|
||||
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(slots)
|
||||
msg_label = Style.label(self, "", 17, Style.RED, Rect2(0, 640, 1280, 22), "center")
|
||||
b_buy = Style.button(self, "Booster kaufen (%d Münzen)" % Profile.BOOSTER_COST, Rect2(370, 690, 330, 52), buy, Color("5a4628"))
|
||||
b_buy.add_theme_font_size_override("font_size", 20)
|
||||
b_all = Style.button(self, "Alle aufdecken", Rect2(720, 690, 200, 52), reveal_all)
|
||||
slots.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
slots.custom_minimum_size = AREA.size
|
||||
scroll.add_child(slots)
|
||||
summary_label = Style.label(self, "", 16, Style.GOLD, Rect2(0, 566, 1280, 24), "center", true)
|
||||
b_all = Style.button(self, "Alle aufdecken", Rect2(540, 594, 200, 40), reveal_all)
|
||||
for i in Campaign.CHAPTER_ELEMENTS.size():
|
||||
var e: String = Campaign.CHAPTER_ELEMENTS[i]
|
||||
var tile := PackTile.new(e, Rect2(70 + i * 230, 644, 220, 136))
|
||||
add_child(tile)
|
||||
for k in QUANTITIES.size():
|
||||
var b := Style.button(tile, "×%d" % QUANTITIES[k], Rect2(10 + k * 68, 88, 64, 36), buy.bind(e, QUANTITIES[k]))
|
||||
b.add_theme_font_size_override("font_size", 14)
|
||||
tile.buy_buttons.append(b)
|
||||
pack_tiles[e] = tile
|
||||
pack_buttons[e] = tile.buy_buttons[0]
|
||||
b_buy = pack_buttons[Cards.NEUTRAL]
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -69,19 +110,34 @@ func _input(event: InputEvent) -> void:
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func buy() -> void:
|
||||
## Kauft n Packs eines Elements; die Karten liegen verdeckt, mehrere Packs nach Stufe sortiert.
|
||||
func buy(element: String, n: int) -> void:
|
||||
var p: Profile = app.profile
|
||||
var got: Array = p.open_booster()
|
||||
var before: int = p.coins
|
||||
var got: Array = p.open_boosters(element, n)
|
||||
if got.is_empty():
|
||||
msg_label.text = "Nicht genug Münzen"
|
||||
msg_label.add_theme_color_override("font_color", Style.RED)
|
||||
return
|
||||
app.save()
|
||||
app.sfx.play("play")
|
||||
if n > 1:
|
||||
got.sort_custom(func(a, b): return Cards.RARITIES.find(a.rarity) > Cards.RARITIES.find(b.rarity))
|
||||
cards = got
|
||||
revealed = {}
|
||||
msg_label.text = ""
|
||||
fresh = {}
|
||||
fly = true
|
||||
var tile: PackTile = pack_tiles[element]
|
||||
fly_from = tile.position + tile.size / 2
|
||||
last_pack = element
|
||||
scroll.scroll_vertical = 0
|
||||
refresh()
|
||||
_count_coins(before, p.coins)
|
||||
|
||||
|
||||
## Die Münzen zählen sichtbar herunter.
|
||||
func _count_coins(from: int, to: int) -> void:
|
||||
if not Style.animations or from == to:
|
||||
return
|
||||
create_tween().tween_method(func(v: float): coins_label.text = "Münzen: %d" % int(round(v)), float(from), float(to), 0.5)
|
||||
|
||||
|
||||
func reveal(i: int) -> void:
|
||||
@@ -91,13 +147,17 @@ func reveal(i: int) -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
## Deckt alles auf, von vorn nach hinten gestaffelt; ab Ultra Rare bleibt die Karte kurz allein.
|
||||
func reveal_all() -> void:
|
||||
fresh = {}
|
||||
var k := 0
|
||||
for i in Profile.BOOSTER_SIZE:
|
||||
var delay := 0.0
|
||||
var step := 0.12 if cards.size() <= Profile.BOOSTER_SIZE else 0.06
|
||||
for i in cards.size():
|
||||
if not revealed.has(i):
|
||||
fresh[i] = k * 0.12
|
||||
k += 1
|
||||
fresh[i] = delay
|
||||
delay += step
|
||||
if Cards.RARITIES.find(cards[i].rarity) >= 3:
|
||||
delay += 0.35
|
||||
revealed[i] = true
|
||||
app.sfx.play("play")
|
||||
refresh()
|
||||
@@ -113,7 +173,7 @@ func flip(v: CardView, delay: float, rarity: String) -> void:
|
||||
tw.tween_property(v, "scale:x", 1.0, 0.18).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
if rarity == "Common":
|
||||
return
|
||||
var burst := Style.panel(fx_layer, Rect2(v.position, v.size).grow(6), Color(Cards.RARITY_COLORS[rarity], 0.6), 12)
|
||||
var burst := Style.panel(slots, Rect2(v.position, v.size).grow(6), Color(Cards.RARITY_COLORS[rarity], 0.6), 12)
|
||||
burst.pivot_offset = burst.size / 2
|
||||
burst.modulate.a = 0.0
|
||||
var bt := create_tween()
|
||||
@@ -124,13 +184,71 @@ func flip(v: CardView, delay: float, rarity: String) -> void:
|
||||
bt.tween_callback(burst.queue_free)
|
||||
|
||||
|
||||
## Einflug einer verdeckten Karte aus dem Pack auf ihren Platz, reihenweise gestaffelt.
|
||||
func fly_in(back: Control, target: Vector2, i: int, per_row: int) -> void:
|
||||
if not Style.animations:
|
||||
return
|
||||
back.position = fly_from - AREA.position - back.size / 2
|
||||
back.scale = Vector2(0.4, 0.4)
|
||||
var tw := create_tween().set_parallel()
|
||||
var delay := (i / per_row) * 0.1 + (i % per_row) * 0.03
|
||||
tw.tween_property(back, "position", target, 0.3).set_delay(delay).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
tw.tween_property(back, "scale", Vector2.ONE, 0.3).set_delay(delay).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
## Platz der i-ten Karte: ein Pack fünf große in einer Reihe, sonst zehn kleine je Reihe.
|
||||
func slot_rect(i: int) -> Rect2:
|
||||
var single := cards.size() <= Profile.BOOSTER_SIZE
|
||||
var w := CARD_W if single else SMALL_W
|
||||
var h := CARD_H if single else SMALL_H
|
||||
var per_row := 5 if single else 10
|
||||
var gap := 20 if single else 12
|
||||
var x0 := (AREA.size.x - per_row * w - (per_row - 1) * gap) / 2
|
||||
var row_h := h + (60 if single else 44)
|
||||
return Rect2(x0 + (i % per_row) * (w + gap), 10 + (i / per_row) * row_h, w, h)
|
||||
|
||||
|
||||
## Fazit nach dem Aufdecken: neue Karten, Aufwertungen, Staub.
|
||||
func summary() -> String:
|
||||
if cards.is_empty() or revealed.size() < cards.size():
|
||||
return ""
|
||||
var fresh_n := 0
|
||||
var up := 0
|
||||
var dust := 0
|
||||
for res in cards:
|
||||
if res.get("new", false):
|
||||
fresh_n += 1
|
||||
elif res.upgraded:
|
||||
up += 1
|
||||
dust += int(res.dust)
|
||||
return tr("%d neu · %d aufgewertet · +%d Staub") % [fresh_n, up, dust]
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
coins_label.text = "Münzen: %d" % p.coins
|
||||
b_buy.disabled = p.coins < Profile.BOOSTER_COST
|
||||
b_all.disabled = cards.is_empty() or revealed.size() >= Profile.BOOSTER_SIZE
|
||||
var open: Array = p.unlocked_elements()
|
||||
var x0 := (1280 - open.size() * 220 - (open.size() - 1) * 10) / 2
|
||||
for e in pack_tiles:
|
||||
var tile: PackTile = pack_tiles[e]
|
||||
tile.locked = not p.booster_unlocked(e)
|
||||
tile.visible = not tile.locked
|
||||
if not tile.locked:
|
||||
tile.position = Vector2(x0 + open.find(e) * 230, 644)
|
||||
tile.hint = tr("%d Münzen") % p.pack_price(e)
|
||||
for k in QUANTITIES.size():
|
||||
tile.buy_buttons[k].disabled = tile.locked or p.coins < p.pack_price(e) * QUANTITIES[k]
|
||||
tile.pulse = e == last_pack and not cards.is_empty() and revealed.size() >= cards.size()
|
||||
tile.queue_redraw()
|
||||
b_all.disabled = cards.is_empty() or revealed.size() >= cards.size()
|
||||
summary_label.text = summary()
|
||||
Style.clear(slots)
|
||||
for i in Profile.BOOSTER_SIZE:
|
||||
var single := cards.size() <= Profile.BOOSTER_SIZE
|
||||
var per_row := 5 if single else 10
|
||||
var n := maxi(cards.size(), Profile.BOOSTER_SIZE)
|
||||
var rows := (n + per_row - 1) / per_row
|
||||
slots.custom_minimum_size = Vector2(AREA.size.x, maxf(AREA.size.y, slot_rect(n - 1).end.y + 60))
|
||||
for i in n:
|
||||
var r := slot_rect(i)
|
||||
if cards.is_empty():
|
||||
Style.panel(slots, r, Style.PANEL, 8, Style.PANEL_LIGHT, 2)
|
||||
@@ -142,16 +260,27 @@ func refresh() -> void:
|
||||
slots.add_child(v)
|
||||
if fresh.has(i):
|
||||
flip(v, fresh[i], res.rarity)
|
||||
Style.label(slots, tr(res.rarity), 16, Cards.RARITY_COLORS[res.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true)
|
||||
var fs := 16 if single else 12
|
||||
Style.label(slots, tr(res.rarity), fs, Cards.RARITY_COLORS[res.rarity], Rect2(r.position.x - 10, r.end.y + 6, r.size.x + 20, 20), "center", true)
|
||||
var y2 := r.end.y + (30 if single else 24)
|
||||
if res.get("new", false):
|
||||
Style.label(slots, "Neu!", 16, Style.GOLD, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
|
||||
Style.label(slots, "Neu!", fs, Style.GOLD, Rect2(r.position.x, y2, r.size.x, 20), "center", true)
|
||||
elif res.upgraded:
|
||||
Style.label(slots, "Aufgewertet", 16, Style.GREEN, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
|
||||
Style.label(slots, "Aufgewertet", fs, Style.GREEN, Rect2(r.position.x - 10, y2, r.size.x + 20, 20), "center", true)
|
||||
else:
|
||||
Style.label(slots, "+%d Staub" % res.dust, 14, Style.MUTED, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center")
|
||||
Style.label(slots, "+%d Staub" % res.dust, fs - 2, Style.MUTED, Rect2(r.position.x, y2, r.size.x, 20), "center")
|
||||
else:
|
||||
var back := CardBack.new(r.size)
|
||||
back.position = r.position
|
||||
back.pivot_offset = r.size / 2
|
||||
back.clicked.connect(reveal.bind(i))
|
||||
slots.add_child(back)
|
||||
if fly:
|
||||
fly_in(back, r.position, i, per_row)
|
||||
fly = false
|
||||
fresh = {}
|
||||
if rows > 0 and last_pack != "" and pack_tiles[last_pack].pulse and Style.animations:
|
||||
var tile: PackTile = pack_tiles[last_pack]
|
||||
var tw := tile.create_tween().set_loops(3)
|
||||
tw.tween_property(tile, "scale", Vector2(1.04, 1.04), 0.35).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
|
||||
tw.tween_property(tile, "scale", Vector2.ONE, 0.35).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
|
||||
@@ -1,6 +1,7 @@
|
||||
class_name CampaignScreen
|
||||
extends Control
|
||||
## Kampagne als Pfad: zehn Stationen je Kapitel, darunter die gewählte Stufe im Detail.
|
||||
## Kampagne als Pfad: zehn Stationen je Kapitel (fünf Kapitel: Neutral, dann je ein Element),
|
||||
## darunter die gewählte Stufe im Detail.
|
||||
|
||||
var app
|
||||
var chapter := 1
|
||||
@@ -11,7 +12,7 @@ var node_buttons: Array = []
|
||||
|
||||
func _ready() -> void:
|
||||
var p: Profile = app.profile
|
||||
chapter = mini(Campaign.chapter(p.campaign_stage), Campaign.CHAPTERS.size())
|
||||
chapter = mini(Campaign.chapter(p.campaign_stage), Campaign.CHAPTER_ELEMENTS.size())
|
||||
selected = mini(p.campaign_stage, Campaign.total() - 1)
|
||||
build()
|
||||
|
||||
@@ -21,14 +22,16 @@ func build() -> void:
|
||||
node_buttons = []
|
||||
var p: Profile = app.profile
|
||||
Style.header(self, "Kampagne", func(): app.show_screen(MenuScreen.new()))
|
||||
var n := Campaign.STAGES.size()
|
||||
var n := Campaign.STAGES_PER_CHAPTER
|
||||
var x := 40
|
||||
for c in Campaign.CHAPTERS.size():
|
||||
var b := Style.button(self, "Kapitel %d" % (c + 1), Rect2(x, 88, 150, 38), set_chapter.bind(c + 1), Style.PRIMARY if chapter == c + 1 else null)
|
||||
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)
|
||||
b.disabled = p.campaign_stage < c * n
|
||||
x += 160
|
||||
x += 138
|
||||
var element := Campaign.element_of(chapter)
|
||||
Style.label(self, tr(element), 20, Cards.ELEMENT_COLORS[element], Rect2(0, 136, 1280, 30), "center", true)
|
||||
if p.perk_unlocked():
|
||||
Style.label(self, "Dein Merkmal:", 15, Style.MUTED, Rect2(760, 96, 130, 24), "right")
|
||||
Style.label(self, "Dein Merkmal:", 15, Style.MUTED, Rect2(740, 96, 150, 24), "right")
|
||||
perk_box = OptionButton.new()
|
||||
perk_box.position = Vector2(900, 88)
|
||||
perk_box.size = Vector2(340, 38)
|
||||
@@ -109,7 +112,7 @@ func build() -> void:
|
||||
func set_chapter(c: int) -> void:
|
||||
chapter = c
|
||||
var p: Profile = app.profile
|
||||
var first := (c - 1) * Campaign.STAGES.size()
|
||||
var first := (c - 1) * Campaign.STAGES_PER_CHAPTER
|
||||
selected = p.campaign_stage if Campaign.chapter(mini(p.campaign_stage, Campaign.total() - 1)) == c else first
|
||||
build()
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ var card: Cards.Card
|
||||
var border = null # Color oder null (dann Rahmen der Seltenheitsstufe)
|
||||
var rarity := "Common" # nur Optik
|
||||
var dim := false
|
||||
var bonus := 0 # Vorschau im Kampf: Elementbonus, den die Karte offen gelegt bekäme
|
||||
var lift := false # hebt sich beim Zeigen an (Hand)
|
||||
var drag_enabled := false # Hand im Kampf: Drücken meldet pressed statt clicked
|
||||
var drop := false # Ziel der gerade gezogenen Karte
|
||||
var marked := false # Haken: die Karte ist im Stapel (Deck)
|
||||
|
||||
|
||||
## focusable: per Gamepad oder Tab anwählbar (Sammlung, Booster); A bzw. Enter wirkt wie ein Klick.
|
||||
@@ -92,15 +92,17 @@ func _draw() -> void:
|
||||
var pic_bottom := size.y - r_stat - 4
|
||||
var r_pic := minf(size.x * 0.42, (pic_bottom - pic_top) / 2)
|
||||
Style.picture(self, card, Vector2(size.x / 2, (pic_top + pic_bottom) / 2), r_pic, rarity, seed)
|
||||
Style.rarity_sparkle(self, rect, rarity, seed) # vor den Werten, damit nichts die Zahlen überlagert
|
||||
# Unten: Angriff links, Leben rechts
|
||||
Style.stat_circle(self, Vector2(r_stat - 2, size.y - r_stat + 2), r_stat, str(card.attack), Style.AMBER, f_stat, Color.WHITE, shiny)
|
||||
Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.LIFE, f_stat, Color.WHITE, shiny)
|
||||
if bonus > 0:
|
||||
Style.text_outlined(self, "+%d" % bonus, 13 if big else 11, Style.GREEN, Vector2(r_stat * 2 + 4, size.y - r_stat + 2), "midleft", 3)
|
||||
Style.card_sheen(self, rect, rarity, seed)
|
||||
Style.border(self, rect, frame, 3 if big else 2, 8)
|
||||
Style.cost_crystal(self, Vector2(r_cost + 4, r_cost + 4), r_cost, card.cost, f_stat, shiny)
|
||||
Style.rarity_sparkle(self, rect, rarity, seed)
|
||||
if marked:
|
||||
var mc := Vector2(size.x / 2, size.y - r_stat + 2)
|
||||
draw_circle(mc, r_stat * 0.6, Style.GREEN)
|
||||
Style.text(self, "✓", f_stat - 4, Color.WHITE, mc, "center", true)
|
||||
if dim:
|
||||
draw_rect(rect, Color(0, 0, 0, 0.47))
|
||||
if has_focus():
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
class_name CheatScreen
|
||||
extends Control
|
||||
## Cheats zum Ausprobieren: Münzen, Staub, Level, Skills, Kampagnenkapitel und Sammlung direkt setzen.
|
||||
## Jeder Knopf speichert sofort; oben steht der aktuelle Stand.
|
||||
|
||||
var app
|
||||
var status_label: Label
|
||||
var msg_label: Label
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Style.header(self, "Cheats", func(): app.show_screen(MenuScreen.new()))
|
||||
status_label = Style.label(self, "", 16, Style.GOLD, Rect2(40, 90, 1200, 48), "left", true)
|
||||
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
var groups := [
|
||||
["Währung", [["+1.000 Münzen", func(): app.profile.coins += 1000], ["+10.000 Münzen", func(): app.profile.coins += 10000],
|
||||
["+1.000 Staub", func(): app.profile.dust += 1000], ["+10.000 Staub", func(): app.profile.dust += 10000]]],
|
||||
["Erfahrung", [["+1 Level", func(): app.profile.xp = Skills.xp_for_level(app.profile.player_level() + 1)],
|
||||
["+5 Level", func(): app.profile.xp = Skills.xp_for_level(app.profile.player_level() + 5)],
|
||||
["Skills zurücksetzen", func(): app.profile.skills = {}], ["Erfahrung auf 0", func():
|
||||
app.profile.xp = 0
|
||||
app.profile.skills = {}]]],
|
||||
["Kampagne", [["Kapitel 2 frei", func(): app.profile.campaign_stage = maxi(app.profile.campaign_stage, 10)],
|
||||
["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)],
|
||||
["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():
|
||||
for id in app.profile.collection:
|
||||
app.profile.collection[id] = mini(int(app.profile.collection[id]) + 1, Cards.RARITIES.size() - 1)],
|
||||
["Sammlung zurücksetzen", func(): app.profile.give_start()]]],
|
||||
]
|
||||
var y := 160
|
||||
for g in groups:
|
||||
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
|
||||
y += 70
|
||||
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(40, y + 10, 1200, 24), "left")
|
||||
refresh()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
## Alle Karten der Sammlung geben, in Stufe level; der Stapel bleibt.
|
||||
func _give_all(level: int) -> void:
|
||||
for c in Cards.all():
|
||||
app.profile.collection[c.id] = level
|
||||
|
||||
|
||||
func _apply(action: Callable) -> void:
|
||||
action.call()
|
||||
app.save()
|
||||
app.sfx.play("click")
|
||||
msg_label.text = tr("Gespeichert")
|
||||
refresh()
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
status_label.text = tr("Münzen %d · Staub %d · EP %d (Level %d, %d Skillpunkte frei) · Kampagne %d/%d · Sammlung %d Karten, Stapel %d") % [
|
||||
p.coins, p.dust, p.xp, p.player_level(), p.skill_points(), p.campaign_stage, Campaign.total(), p.collection.size(), p.deck.size()]
|
||||
@@ -0,0 +1 @@
|
||||
uid://dluwr50n0sd17
|
||||
@@ -21,7 +21,7 @@ var b_craft: Button
|
||||
func _ready() -> void:
|
||||
Style.header(self, "Sammlung", func(): app.show_screen(MenuScreen.new()))
|
||||
Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind(""))
|
||||
var filters: Array = Cards.ELEMENTS + [Cards.NEUTRAL]
|
||||
var filters: Array = app.profile.unlocked_elements() # nur Elemente offener Kapitel
|
||||
for i in filters.size():
|
||||
var e: String = filters[i]
|
||||
var fb := Style.button(self, e, Rect2(108 + i * 82, 86, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||
@@ -60,9 +60,10 @@ func set_filter(e: String) -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
## Eigene Karten zuerst (höhere Stufen vorn), dann die fehlenden, sonst in Kartenreihenfolge.
|
||||
## Karten der offenen Elemente: eigene zuerst (höhere Stufen vorn), dann die fehlenden, sonst in
|
||||
## Kartenreihenfolge.
|
||||
func cards() -> Array:
|
||||
var out: Array = Cards.all().duplicate()
|
||||
var out: Array = app.profile.unlocked_cards()
|
||||
if filter != "":
|
||||
out = out.filter(func(c): return filter in c.elements)
|
||||
if search != "":
|
||||
|
||||
@@ -12,7 +12,7 @@ var width := 2
|
||||
var flash := false # kurz rot: wurde gerade angegriffen
|
||||
var slot: Vector2 # Zielposition im Raster
|
||||
var busy := false # wird gerade animiert (kein Neupositionieren)
|
||||
var matchup := 0 # Elementvorteil des gewählten Angreifers gegen dieses fremde Wesen (Game.matchup_bonus); die Werte zeigen dann, was ihn erwartet
|
||||
var matchup := 0 # fremdes Wesen: sein Konter gegen den gewählten Angreifer (Game.matchup_bonus); die Werte zeigen, was ihn erwartet
|
||||
var attack_shift := 0 # eigener Angreifer: Elementvorteil gegen das Ziel unter der Maus
|
||||
var doomed := false # stirbt bei dem Angriff, der gerade unter der Maus liegt
|
||||
var secret := false # verdecktes fremdes Wesen: nur der Rücken ist zu sehen
|
||||
@@ -78,7 +78,7 @@ func _draw() -> void:
|
||||
var pic_bottom := size.y - r_stat - 4
|
||||
var r_pic := minf(size.x * 0.42, (pic_bottom - pic_top) / 2)
|
||||
Style.picture(self, creature.card, Vector2(size.x / 2, (pic_top + pic_bottom) / 2), r_pic, rar, seed)
|
||||
# Unter dem Namen links die Eigenschaften (in der Farbe des Elements, das sie gibt), rechts der Elementbonus
|
||||
# Unter dem Namen links die Eigenschaften (in der Farbe des Elements, das sie gibt)
|
||||
var icon := 6.0 if small else 7.0
|
||||
var y := top_h + 6 + icon
|
||||
for k in creature.keywords():
|
||||
@@ -88,11 +88,7 @@ func _draw() -> void:
|
||||
col = Style.lighter(Cards.ELEMENT_COLORS[e], 0.45)
|
||||
Style.keyword_icon(self, k, Vector2(6 + icon, y), icon, col)
|
||||
y += icon * 2 + 6
|
||||
if creature.aura != 0:
|
||||
var badge := "+%d" % creature.aura if creature.aura > 0 else "−%d" % -creature.aura
|
||||
var bw := Style.FONT_BOLD.get_string_size(badge, HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(10)).x + 8
|
||||
draw_rect(Rect2(size.x - bw - 4, top_h + 5, bw, 16), Color(0.05, 0.05, 0.08, 0.75))
|
||||
Style.text(self, badge, 10, Style.GREEN if creature.aura > 0 else Style.RED, Vector2(size.x - 4 - bw / 2, top_h + 13), "center", true)
|
||||
Style.rarity_sparkle(self, rect, rar, seed) # vor den Werten, damit nichts die Zahlen überlagert
|
||||
_draw_stats(15 if small else 17, r_stat, shiny)
|
||||
Style.card_sheen(self, rect, rar, seed)
|
||||
Style.border(self, rect, bcol, 4 if flash else width, 6)
|
||||
@@ -103,7 +99,6 @@ func _draw() -> void:
|
||||
if doomed:
|
||||
draw_rect(Rect2(0, size.y * 0.5 - 12, size.x, 24), Color(0.05, 0.02, 0.02, 0.8))
|
||||
Style.text_outlined(self, tr("stirbt"), 14, Style.RED, Vector2(size.x / 2, size.y * 0.5), "center", 3)
|
||||
Style.rarity_sparkle(self, rect, rar, seed)
|
||||
_draw_drop()
|
||||
|
||||
|
||||
@@ -128,8 +123,7 @@ func _draw_back(rect: Rect2) -> void:
|
||||
## das Leben bleibt echt. Eigener Angreifer über einem Ziel: sein Angriff mit Elementvorteil.
|
||||
func shown_stats() -> Array:
|
||||
if matchup != 0:
|
||||
var atk := maxi(0, creature.power() - matchup)
|
||||
return [atk, creature.health]
|
||||
return [maxi(0, creature.power() + matchup), creature.health]
|
||||
if attack_shift != 0:
|
||||
return [maxi(0, creature.power() + attack_shift), creature.health]
|
||||
return [creature.power(), creature.health]
|
||||
|
||||
+190
-25
@@ -1,38 +1,139 @@
|
||||
class_name DeckScreen
|
||||
extends Control
|
||||
## Deck zusammenstellen: alle eigenen Karten im Raster, ein Klick nimmt eine Karte in den
|
||||
## Stapel oder wieder heraus. Karten außerhalb des Stapels sind abgedunkelt. Der Stapel hält
|
||||
## 40 bis 60 Karten; „Auffüllen“ nimmt die wertvollsten eigenen Karten des gewählten Filters.
|
||||
## „Stärke“ ist der mittlere Kartenwert (100 Grundkarten … 500 Ultimate Rare).
|
||||
## Deck zusammenstellen: alle eigenen Karten im Raster (Filter nach Element und Mana, Stapelkarten
|
||||
## zuerst, dann höchste Stufen vorn), ein Klick nimmt eine Karte in den Stapel oder wieder heraus. Stapelkarten tragen einen Haken, die anderen sind
|
||||
## abgedunkelt. Rechts Stapelgröße, Stärke (mittlerer Kartenwert, 100 Grundkarten … 500 Ultimate
|
||||
## Rare), Manakurve, Elementanteile und die Vorschau der gezeigten Karte. Der Stapel hält DECK_MIN
|
||||
## bis DECK_MAX Karten; „Auffüllen“ nimmt die wertvollsten eigenen Karten des gewählten Filters.
|
||||
|
||||
const PREVIEW := Rect2(940, 362, 180, 252)
|
||||
const MAX_COST := 10
|
||||
|
||||
var app
|
||||
var filter := ""
|
||||
var filter := "" # Element oder ""
|
||||
var cost_filter := -1 # Kosten 0 bis 10 oder -1
|
||||
var grid: CardGrid
|
||||
var all_button: Button
|
||||
var element_buttons := {} # Element -> Knopf mit Symbol
|
||||
var cost_buttons: Array = [] # je Kosten ein Knopf
|
||||
var count_label: Label
|
||||
var strength_label: Label
|
||||
var curve: CurveView
|
||||
var share: ElementShare
|
||||
var preview_holder: Control
|
||||
var shown_card: Cards.Card = null # Karte in der Vorschau
|
||||
var msg_label: Label
|
||||
|
||||
|
||||
## Filterknopf mit dem Symbol eines Elements.
|
||||
class ElementButton extends Button:
|
||||
var element: String
|
||||
|
||||
func _init(e: String) -> void:
|
||||
element = e
|
||||
tooltip_text = tr(e)
|
||||
|
||||
func _draw() -> void:
|
||||
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.
|
||||
class CurveView extends Control:
|
||||
var counts: Array = []
|
||||
|
||||
func _init(rect: Rect2) -> void:
|
||||
position = rect.position
|
||||
size = rect.size
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
func set_counts(c: Array) -> void:
|
||||
counts = c
|
||||
queue_redraw()
|
||||
|
||||
func _draw() -> void:
|
||||
var n := 11
|
||||
var gap := 4.0
|
||||
var bw := (size.x - gap * (n - 1)) / n
|
||||
var top := 16.0
|
||||
var base := size.y - 16.0
|
||||
var mx := 1
|
||||
for v in counts:
|
||||
mx = maxi(mx, int(v))
|
||||
for i in n:
|
||||
var v: int = int(counts[i]) if i < counts.size() else 0
|
||||
var x := i * (bw + gap)
|
||||
var h := (base - top) * v / mx
|
||||
draw_rect(Rect2(x, base - 2, bw, 2), Style.PANEL_LIGHT)
|
||||
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")
|
||||
|
||||
|
||||
## Elementanteile des Stapels: je Element eine farbige Kachel mit der Kartenzahl.
|
||||
class ElementShare extends Control:
|
||||
var counts := {}
|
||||
|
||||
func _init(rect: Rect2) -> void:
|
||||
position = rect.position
|
||||
size = rect.size
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
func set_counts(c: Dictionary) -> void:
|
||||
counts = c
|
||||
queue_redraw()
|
||||
|
||||
func shown() -> Array:
|
||||
return ElementView.all().filter(func(e): return int(counts.get(e, 0)) > 0)
|
||||
|
||||
func _draw() -> void:
|
||||
var items := shown()
|
||||
if items.is_empty():
|
||||
return
|
||||
var w := size.x / items.size()
|
||||
for i in items.size():
|
||||
var e: String = items[i]
|
||||
var col: Color = Cards.ELEMENT_COLORS[e]
|
||||
var r := Rect2(i * w + 2, 0, w - 4, size.y)
|
||||
draw_style_box(Style.box(Color(col, 0.25), 6, Color(col, 0.7), 1), r)
|
||||
Style.text(self, "%s %d" % [tr(e), int(counts[e])], 12, Style.TEXT, r.get_center(), "center", true)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Style.header(self, "Deck", func(): app.show_screen(MenuScreen.new()))
|
||||
Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind(""))
|
||||
all_button = Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind(""))
|
||||
var filters: Array = Cards.ELEMENTS + [Cards.NEUTRAL]
|
||||
for i in filters.size():
|
||||
var e: String = filters[i]
|
||||
var fb := Style.button(self, e, Rect2(108 + i * 82, 86, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||
fb.add_theme_font_size_override("font_size", 14)
|
||||
var fb := ElementButton.new(e)
|
||||
fb.position = Vector2(108 + i * 40, 86)
|
||||
fb.size = Vector2(34, 34)
|
||||
fb.pressed.connect(set_filter.bind(e))
|
||||
Style.juice(fb)
|
||||
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))
|
||||
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))
|
||||
grid.card_clicked.connect(toggle)
|
||||
add_child(grid)
|
||||
Style.panel(self, Rect2(820, 86, 420, 696))
|
||||
count_label = Style.chip(self, "", Rect2(860, 100, 340, 36), Style.BLUE)
|
||||
strength_label = Style.chip(self, "", Rect2(860, 144, 340, 36), Style.GOLD)
|
||||
var hint := Style.label(self, "", 15, Style.MUTED, Rect2(840, 188, 380, 66), "center")
|
||||
hint.autowrap_mode = TextServer.AUTOWRAP_WORD # vor dem Text, sonst wächst die Breite auf eine Zeile
|
||||
hint.text = "Klick nimmt eine Karte in den Stapel oder heraus. Auffüllen nimmt die wertvollsten Karten des Filters."
|
||||
Style.button(self, "Auffüllen", Rect2(840, 262, 380, 46), fill_up)
|
||||
Style.button(self, "Leeren", Rect2(840, 318, 380, 46), clear_deck, Color("5a3c28"))
|
||||
msg_label = Style.label(self, "", 15, Style.RED, Rect2(840, 382, 380, 48), "center")
|
||||
Style.label(self, "Manakurve", 13, Style.MUTED, Rect2(850, 190, 360, 20), "center")
|
||||
curve = CurveView.new(Rect2(850, 212, 360, 100))
|
||||
add_child(curve)
|
||||
share = ElementShare.new(Rect2(850, 322, 360, 30))
|
||||
add_child(share)
|
||||
preview_holder = Control.new()
|
||||
preview_holder.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(preview_holder)
|
||||
Style.button(self, "Auffüllen", Rect2(840, 626, 380, 44), fill_up, Style.PRIMARY)
|
||||
Style.button(self, "Leeren", Rect2(840, 678, 380, 40), clear_deck)
|
||||
msg_label = Style.label(self, "", 14, Style.MUTED, Rect2(840, 726, 380, 48), "center")
|
||||
msg_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -46,15 +147,34 @@ func set_filter(e: String) -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
## Manafilter: derselbe Knopf noch einmal hebt ihn auf.
|
||||
func set_cost(c: int) -> void:
|
||||
cost_filter = -1 if cost_filter == c else c
|
||||
refresh()
|
||||
|
||||
|
||||
## Hebt den aktiven Filterknopf hervor.
|
||||
func _mark(b: Button, active: bool, base: Color) -> void:
|
||||
b.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))
|
||||
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(base), 8, Style.GOLD if active else Style.MUTED, 2))
|
||||
b.add_theme_stylebox_override("pressed", Style.box(Style.darker(base), 8, Style.GOLD if active else Style.MUTED, 2))
|
||||
|
||||
|
||||
func toggle(card: Cards.Card) -> void:
|
||||
var err: String = app.profile.toggle_deck(card.id)
|
||||
msg_label.text = err
|
||||
say(err)
|
||||
if err == "":
|
||||
app.sfx.play("click")
|
||||
app.save()
|
||||
refresh()
|
||||
|
||||
|
||||
## Fehler rot, sonst der Hinweis.
|
||||
func say(err: String) -> void:
|
||||
msg_label.text = tr(err) if err != "" else tr("Klick auf eine Karte: rein oder raus.")
|
||||
msg_label.add_theme_color_override("font_color", Style.RED if err != "" else Style.MUTED)
|
||||
|
||||
|
||||
## Füllt mit den wertvollsten eigenen Karten des gewählten Filters bis zum Höchstwert auf.
|
||||
func fill_up() -> void:
|
||||
var p: Profile = app.profile
|
||||
@@ -72,30 +192,75 @@ func clear_deck() -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
## Eigene Karten, die im Stapel zuerst, dann höchste Stufen vorn, dann nach Kosten.
|
||||
## Sortierschlüssel einer Karte: im Stapel zuerst, dann höchste Stufen vorn, dann Kosten, dann ID.
|
||||
func sort_key(c: Cards.Card) -> Array:
|
||||
var p: Profile = app.profile
|
||||
return [0 if c.id in p.deck else 1, -p.level(c.id), c.cost, c.id]
|
||||
|
||||
|
||||
## Eigene Karten nach Element- und Manafilter, sortiert (sort_key).
|
||||
func cards() -> Array:
|
||||
var p: Profile = app.profile
|
||||
var out: Array = p.collection.keys().map(func(id): return Cards.by_id(id))
|
||||
if filter != "":
|
||||
out = out.filter(func(c): return filter in c.elements)
|
||||
out.sort_custom(func(a, b):
|
||||
var ia: bool = a.id in p.deck
|
||||
var ib: bool = b.id in p.deck
|
||||
if ia != ib:
|
||||
return ia
|
||||
if p.level(a.id) != p.level(b.id):
|
||||
return p.level(a.id) > p.level(b.id)
|
||||
return a.cost < b.cost if a.cost != b.cost else a.id < b.id)
|
||||
if cost_filter >= 0:
|
||||
out = out.filter(func(c): return c.cost == cost_filter)
|
||||
out.sort_custom(func(a, b): return sort_key(a) < sort_key(b))
|
||||
return out
|
||||
|
||||
|
||||
## Vorschau der gezeigten Karte in ihrer Stufe.
|
||||
func show_card(card: Cards.Card) -> void:
|
||||
var p: Profile = app.profile
|
||||
shown_card = card
|
||||
Style.clear(preview_holder)
|
||||
var v := CardView.new(card.leveled(p.level(card.id)), PREVIEW.size, false, p.rarity(card.id))
|
||||
v.position = PREVIEW.position
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
preview_holder.add_child(v)
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
count_label.text = tr("Stapel: %d Karten (%d bis %d)") % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX]
|
||||
count_label.add_theme_color_override("font_color", Style.RED if p.deck.size() < Game.DECK_MIN else Style.BLUE)
|
||||
strength_label.text = tr("Stärke %d") % DeckGen.strength(p.deck, p.collection)
|
||||
_mark(all_button, filter == "", Style.PANEL_LIGHT)
|
||||
var open: Array = p.unlocked_elements() # nur Elemente offener Kapitel
|
||||
for e in element_buttons:
|
||||
var eb: Button = element_buttons[e]
|
||||
eb.visible = e in open
|
||||
if eb.visible:
|
||||
eb.position = Vector2(108 + open.find(e) * 40, 86)
|
||||
_mark(eb, filter == e, Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||
for c in cost_buttons.size():
|
||||
_mark(cost_buttons[c], cost_filter == c, Style.PANEL_LIGHT)
|
||||
var counts := []
|
||||
counts.resize(11)
|
||||
counts.fill(0)
|
||||
var per_element := {}
|
||||
for id in p.deck:
|
||||
var c := Cards.by_id(id)
|
||||
counts[clampi(c.cost, 0, 10)] += 1
|
||||
per_element[c.elements[0]] = int(per_element.get(c.elements[0], 0)) + 1
|
||||
curve.set_counts(counts)
|
||||
share.set_counts(per_element)
|
||||
if msg_label.text == "":
|
||||
say("")
|
||||
var shown := cards()
|
||||
grid.fill(shown.map(func(c): return c.leveled(p.level(c.id))), func(c): return Cards.RARITIES[c.look])
|
||||
var views := grid.views()
|
||||
for i in views.size():
|
||||
views[i].dim = not (shown[i].id in p.deck)
|
||||
var in_deck: bool = shown[i].id in p.deck
|
||||
views[i].dim = not in_deck
|
||||
views[i].marked = in_deck
|
||||
views[i].hover.connect(func(c, entered):
|
||||
if entered:
|
||||
show_card(c))
|
||||
if shown_card != null and p.owns(shown_card.id):
|
||||
show_card(shown_card)
|
||||
elif shown.is_empty():
|
||||
Style.clear(preview_holder)
|
||||
else:
|
||||
show_card(shown[0])
|
||||
+10
-22
@@ -1,14 +1,12 @@
|
||||
class_name ElementView
|
||||
extends Control
|
||||
## Elementzähler einer Seite als Liste neben ihrem Brett. Es stehen nur Elemente da, bei denen
|
||||
## etwas passiert ist: je Zeile Name, zweimal das Symbol seines Sondereffekts (leuchtend je
|
||||
## offenem Wesen, beide: Effekt aktiv; Neutral hat keinen), die Zahl der ausgespielten Karten
|
||||
## und ab Game.ELEMENT_COUNT der Bonus +1/+1.
|
||||
## Elementpaare einer Seite als Liste neben ihrem Brett. Es stehen nur Elemente mit offenen Wesen
|
||||
## da: je Zeile Name, zweimal das Symbol seines Sondereffekts (leuchtend je offenem Wesen, beide:
|
||||
## Effekt aktiv; Neutral hat keinen) und die Zahl der offenen Wesen.
|
||||
|
||||
const ROW := 32.0
|
||||
|
||||
var own := {} # offene eigene Wesen je Element
|
||||
var played := {} # ausgespielte Karten je Element
|
||||
var own := {} # offene eigene Wesen je Element
|
||||
|
||||
|
||||
func _init(rect: Rect2) -> void:
|
||||
@@ -17,7 +15,6 @@ func _init(rect: Rect2) -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
for e in all():
|
||||
own[e] = 0
|
||||
played[e] = 0
|
||||
|
||||
|
||||
static func all() -> Array:
|
||||
@@ -27,7 +24,6 @@ static func all() -> Array:
|
||||
func update_from(_g: Game, me: Game.Player) -> void:
|
||||
for e in all():
|
||||
own[e] = me.element_count(e) if me != null else 0
|
||||
played[e] = int(me.played.get(e, 0)) if me != null else 0
|
||||
queue_redraw()
|
||||
|
||||
|
||||
@@ -35,24 +31,18 @@ func paired(e: String) -> bool:
|
||||
return Cards.ELEMENT_KEYWORD.has(e) and int(own.get(e, 0)) >= Game.ELEMENT_PAIR
|
||||
|
||||
|
||||
func bonus(e: String) -> int:
|
||||
return int(played.get(e, 0)) / Game.ELEMENT_COUNT
|
||||
|
||||
|
||||
## Elemente, bei denen sich etwas getan hat (offene Wesen oder ausgespielte Karten).
|
||||
## Elemente mit offenen Wesen.
|
||||
func shown() -> Array:
|
||||
return all().filter(func(e): return int(own.get(e, 0)) > 0 or int(played.get(e, 0)) > 0)
|
||||
return all().filter(func(e): return int(own.get(e, 0)) > 0)
|
||||
|
||||
|
||||
## Der Inhalt als Text (Tests, Protokoll): Element offen/gespielt, dazu Effekt und Bonus.
|
||||
## Der Inhalt als Text (Tests, Protokoll): Element und offene Wesen, dazu der Effekt.
|
||||
func text() -> String:
|
||||
var parts: Array[String] = []
|
||||
for e in shown():
|
||||
var s := "%s %d/%d" % [tr(e), int(own.get(e, 0)), int(played.get(e, 0))]
|
||||
var s := "%s %d" % [tr(e), int(own.get(e, 0))]
|
||||
if paired(e):
|
||||
s += " (%s)" % tr(Cards.ELEMENT_KEYWORD[e])
|
||||
if bonus(e) > 0:
|
||||
s += " (+%d/+%d)" % [bonus(e), bonus(e)]
|
||||
parts.append(s)
|
||||
return " · ".join(parts)
|
||||
|
||||
@@ -61,7 +51,7 @@ func _draw() -> void:
|
||||
var y := ROW / 2
|
||||
for e in shown():
|
||||
var col: Color = Cards.ELEMENT_COLORS[e]
|
||||
var active := paired(e) or bonus(e) > 0
|
||||
var active := paired(e)
|
||||
draw_style_box(Style.box(Color(col, 0.18 if active else 0.1), 6, Color(col, 0.6 if active else 0.0), 1 if active else 0), Rect2(0, y - ROW / 2 + 2, size.x, ROW - 4))
|
||||
var px := 10.0
|
||||
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(px, y), "midleft", active)
|
||||
@@ -71,7 +61,5 @@ func _draw() -> void:
|
||||
var filled := k < int(own.get(e, 0))
|
||||
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + k * 24 + 10, y), 8, Style.lighter(col, 0.45) if filled else Color(col, 0.3))
|
||||
px += Game.ELEMENT_PAIR * 24 + 8
|
||||
Style.text(self, str(int(played.get(e, 0))), 15, Style.lighter(col, 0.45), Vector2(px, y), "midleft", true)
|
||||
if bonus(e) > 0:
|
||||
Style.text(self, "+%d/+%d" % [bonus(e), bonus(e)], 12, Style.GREEN, Vector2(size.x - 6, y), "midright", true)
|
||||
Style.text(self, str(int(own.get(e, 0))), 15, Style.lighter(col, 0.45), Vector2(px, y), "midleft", true)
|
||||
y += ROW
|
||||
+3
-3
@@ -18,7 +18,7 @@ func _ready() -> void:
|
||||
add_child(sfx)
|
||||
profile = Profile.load_or_new(save_path)
|
||||
apply_settings()
|
||||
show_screen(MenuScreen.new() if profile.started() else StartDeckScreen.new())
|
||||
show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func apply_settings() -> void:
|
||||
@@ -30,12 +30,12 @@ func apply_settings() -> void:
|
||||
theme = Style.make_theme()
|
||||
|
||||
|
||||
## Neues Profil: Sammlung und Fortschritt werden verworfen, das Startdeck wird neu gewählt.
|
||||
## Neues Profil: Sammlung und Fortschritt werden verworfen, zurück bleiben die neutralen Karten.
|
||||
func reset_profile() -> void:
|
||||
profile = Profile.starter()
|
||||
save()
|
||||
apply_settings()
|
||||
show_screen(StartDeckScreen.new())
|
||||
show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func set_language(code: String) -> void:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class_name MenuScreen
|
||||
extends Control
|
||||
## Hauptmenü: links Titel und Spielarten, rechts drei Karten der eigenen Sammlung als Schaufenster.
|
||||
## 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.
|
||||
|
||||
var app
|
||||
var coins_label: Label
|
||||
@@ -25,10 +26,17 @@ func _ready() -> void:
|
||||
var x := 80
|
||||
for entry in [["Booster öffnen", func(): app.show_screen(BoosterScreen.new())],
|
||||
["Sammlung", func(): app.show_screen(CollectionScreen.new())],
|
||||
["Einstellungen", func(): app.show_screen(SettingsScreen.new())],
|
||||
["Skilltree", func(): app.show_screen(SkillScreen.new())]]:
|
||||
Style.button(self, entry[0], Rect2(x, y, 166, 44), entry[1])
|
||||
x += 176
|
||||
y += 56
|
||||
x = 80
|
||||
for entry in [["Einstellungen", func(): app.show_screen(SettingsScreen.new())],
|
||||
["Beenden", func(): get_tree().quit()]]:
|
||||
Style.button(self, entry[0], Rect2(x, y, 166, 44), entry[1])
|
||||
x += 176
|
||||
var cheats := Style.button(self, "Cheats", Rect2(1120, 744, 120, 36), func(): app.show_screen(CheatScreen.new()))
|
||||
cheats.add_theme_font_size_override("font_size", 14)
|
||||
notice_label = Style.label(self, "", 16, Style.GREEN, Rect2(80, y + 70, 760, 60), "left")
|
||||
notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
_showcase(p)
|
||||
@@ -55,8 +63,8 @@ func _showcase(p: Profile) -> void:
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
coins_label.text = tr("Münzen: %d · Staub: %d") % [p.coins, p.dust]
|
||||
coll_label.text = tr("Sammlung: %d von %d Karten · Kampagne: %d/%d") % [
|
||||
p.collection.size(), Cards.all().size(), p.campaign_stage, Campaign.total()]
|
||||
coll_label.text = tr("Sammlung: %d von %d Karten · Kampagne: %d/%d · Level %d") % [
|
||||
p.collection.size(), p.unlocked_cards().size(), p.campaign_stage, Campaign.total(), p.player_level()]
|
||||
var notes := p.take_notices()
|
||||
notice_label.text = " · ".join(notes)
|
||||
if not notes.is_empty():
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class_name OpponentSelectScreen
|
||||
extends Control
|
||||
## Einzelspieler: Schwierigkeit wählen und ein Duell starten. Die Karten kommen im Kampf
|
||||
## vom gemeinsamen Stapel (siehe Game.start).
|
||||
## Einzelspieler: Schwierigkeit wählen und ein Duell starten. Der Gegner spielt ein Element aus den
|
||||
## offenen Kapiteln in einer Stufe relativ zu deiner Deckstärke (Profile.single_opponent).
|
||||
|
||||
var app
|
||||
var diff_buttons := {}
|
||||
@@ -46,7 +46,11 @@ func refresh() -> void:
|
||||
for key in diff_buttons:
|
||||
_mark(diff_buttons[key], key == p.settings.difficulty)
|
||||
var d := p.difficulty()
|
||||
info_label.text = tr("Gegner mit %d Leben · %d Münzen bei Sieg") % [int(d.life), int(round(Profile.REWARD_WIN * float(d.reward)))]
|
||||
var parts: Array[String] = [tr("Gegnerkarten: %s") % tr(Cards.RARITIES[p.opponent_level()]), tr("Vorausschau-KI") if bool(d.smart) else tr("einfache KI")]
|
||||
if bool(d.perk):
|
||||
parts.append(tr("ein Merkmal"))
|
||||
parts.append(tr("Münzen %s") % Style.factor(float(d.reward)))
|
||||
info_label.text = " · ".join(parts)
|
||||
|
||||
|
||||
func _mark(b: Button, active: bool, col: Color = Color("285a3c")) -> void:
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
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“.
|
||||
|
||||
var app
|
||||
var level_label: Label
|
||||
var xp_label: Label
|
||||
var points_label: Label
|
||||
var msg_label: Label
|
||||
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
|
||||
|
||||
|
||||
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")
|
||||
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 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")
|
||||
refresh()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func learn(id: String) -> void:
|
||||
var err: String = app.profile.learn(id)
|
||||
msg_label.text = tr(err) if err != "" else ""
|
||||
if err == "":
|
||||
app.sfx.play("combo")
|
||||
app.save()
|
||||
refresh()
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
var lvl := p.player_level()
|
||||
var base := Skills.xp_for_level(lvl)
|
||||
level_label.text = tr("Level %d") % lvl
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b0dgryn2kpic5
|
||||
@@ -1,37 +0,0 @@
|
||||
class_name StartDeckScreen
|
||||
extends Control
|
||||
## Erster Start: eines von vier Startdecks wählen, je eines mit einem Element im Mittelpunkt
|
||||
## (25 Karten davon, je fünf der anderen). Danach geht es ins Menü.
|
||||
|
||||
var app
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Style.label(self, tr("Wähle dein Startdeck"), 40, Style.GOLD, Rect2(0, 60, 1280, 60), "center", true)
|
||||
Style.label(self, tr("25 Karten des Elements, dazu je fünf der anderen. Booster und Werkstatt bringen später den Rest."), 17, Style.MUTED, Rect2(140, 124, 1000, 30), "center")
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
var x := 70 + i * 290
|
||||
var col: Color = Cards.ELEMENT_COLORS[e]
|
||||
Style.panel(self, Rect2(x, 190, 260, 470), Style.darker(col, 0.75), 12, col, 2)
|
||||
var ids := DeckGen.start_deck(e)
|
||||
var sample: Array = ids.filter(func(id): return Cards.by_id(id).elements[0] == e)
|
||||
sample.sort_custom(func(a, b): return Cards.by_id(a).cost > Cards.by_id(b).cost)
|
||||
for k in 3:
|
||||
var c: Cards.Card = Cards.by_id(sample[k * 7])
|
||||
var v := CardView.new(c, Vector2(120, 152), false, "Common")
|
||||
v.position = Vector2(x + 20 + k * 40, 210 + k * 14)
|
||||
v.rotation_degrees = -6 + k * 6
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(v)
|
||||
Style.label(self, tr(e), 28, Style.TEXT, Rect2(x, 400, 260, 40), "center", true)
|
||||
Style.label(self, tr(Cards.KEYWORD_TEXT[Cards.ELEMENT_KEYWORD[e]]), 15, Style.MUTED, Rect2(x + 16, 446, 228, 60), "center")
|
||||
var b := Style.button(self, tr("%s wählen") % tr(e), Rect2(x + 30, 590, 200, 50), choose.bind(e), Style.PRIMARY)
|
||||
b.add_theme_font_size_override("font_size", 18)
|
||||
|
||||
|
||||
func choose(element: String) -> void:
|
||||
app.profile.choose_start(element)
|
||||
app.save()
|
||||
app.sfx.play("combo")
|
||||
app.show_screen(MenuScreen.new())
|
||||
@@ -1 +0,0 @@
|
||||
uid://bxl6isdrgqb2b
|
||||
@@ -26,6 +26,11 @@ const DANGER := Color("5a2828")
|
||||
|
||||
|
||||
## Schriftgröße mit der Einstellung „Große Schrift“.
|
||||
## Faktor als Text: ×2, ×1,5 (Dezimaltrenner der Sprache).
|
||||
static func factor(f: float) -> String:
|
||||
return "×" + String.num(f, 1).trim_suffix(".0").replace(".", TranslationServer.translate(","))
|
||||
|
||||
|
||||
static func fs(size: int) -> int:
|
||||
return int(round(size * font_scale))
|
||||
|
||||
@@ -630,6 +635,11 @@ static func _glyph_plain(ci: CanvasItem, card, center: Vector2, size: float, tin
|
||||
ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2)
|
||||
|
||||
|
||||
## Elementsymbol in einer Farbe (Filterknöpfe, Listen), Grundvariante.
|
||||
static func element_icon(ci: CanvasItem, element: String, c: Vector2, size: float, col: Color) -> void:
|
||||
_element_glyph(ci, element, c, size, 0, col)
|
||||
|
||||
|
||||
static func _element_glyph(ci: CanvasItem, element: String, c: Vector2, size: float, variant: int, col: Color) -> void:
|
||||
var h := size / 2
|
||||
match element:
|
||||
|
||||
Reference in new issue
Block a user