update
This commit is contained in:
@@ -101,14 +101,7 @@ static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -
|
||||
for cr in p.board:
|
||||
if cr.hidden and game.can_reveal(p, cr)[0]:
|
||||
out.append({"type": "reveal", "creature": cr})
|
||||
var pairs := []
|
||||
for i in p.board.size():
|
||||
for j in range(i + 1, p.board.size()):
|
||||
if game.can_fuse(p, p.board[i], p.board[j])[0]:
|
||||
pairs.append([p.board[i], p.board[j]])
|
||||
if prune and pairs.size() > MAX_TARGETS:
|
||||
pairs = _top(pairs, func(pr): return _value(pr[0]) + _value(pr[1]), MAX_TARGETS)
|
||||
for pr in pairs:
|
||||
for pr in fusion_pairs(game, p, prune):
|
||||
out.append({"type": "fuse", "creature": pr[0], "other": pr[1]})
|
||||
for cr in p.board:
|
||||
if not cr.can_attack():
|
||||
@@ -126,6 +119,39 @@ static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -
|
||||
return out
|
||||
|
||||
|
||||
## Verschmelzungen, die sich lohnen könnten: Paare eigener Wesen (mit prune die schwächsten, die
|
||||
## Plätze frei machen), jedes angeschlagene Wesen mit der billigsten Handkarte, die zwei
|
||||
## billigsten Handkarten. Das Ergebnis kommt auf die Hand, die Vorausschau legt es im Folgeschritt.
|
||||
static func fusion_pairs(game: Game, p: Game.Player, prune: bool) -> Array:
|
||||
var pairs := []
|
||||
for i in p.board.size():
|
||||
for j in range(i + 1, p.board.size()):
|
||||
pairs.append([p.board[i], p.board[j]])
|
||||
if prune and pairs.size() > MAX_TARGETS:
|
||||
pairs = _top(pairs, func(pr): return -(_value(pr[0]) + _value(pr[1])), MAX_TARGETS)
|
||||
var cheap := _cheapest(p)
|
||||
if not cheap.is_empty():
|
||||
for cr in p.board:
|
||||
if cr.health < cr.max_health:
|
||||
pairs.append([cr, cheap[0]])
|
||||
if cheap.size() >= 2:
|
||||
pairs.append([cheap[0], cheap[1]])
|
||||
return pairs.filter(func(pr): return game.can_fuse(p, pr[0], pr[1])[0])
|
||||
|
||||
|
||||
## Handkarten nach Kosten aufsteigend.
|
||||
static func _cheapest(p: Game.Player) -> Array:
|
||||
var cheap: Array = p.hand.duplicate()
|
||||
cheap.sort_custom(func(a, b): return a.cost < b.cost)
|
||||
return cheap
|
||||
|
||||
|
||||
## Teil einer Verschmelzung in einem anderen Spiel: Handkarten sind in Kopien dieselben Objekte,
|
||||
## Wesen werden über ihre uid gefunden.
|
||||
static func _part(g: Game, x):
|
||||
return x if x is Cards.Card else g.find_creature(x.uid)
|
||||
|
||||
|
||||
## Führt eine Aktion des Originals auf der Kopie aus.
|
||||
static func apply_on_clone(g2: Game, game: Game, action: Dictionary) -> void:
|
||||
var p2: Game.Player = g2.players[game.current_idx]
|
||||
@@ -135,7 +161,7 @@ static func apply_on_clone(g2: Game, game: Game, action: Dictionary) -> void:
|
||||
"reveal":
|
||||
g2.reveal(p2, g2.find_creature(action.creature.uid))
|
||||
"fuse":
|
||||
g2.fuse(p2, g2.find_creature(action.creature.uid), g2.find_creature(action.other.uid))
|
||||
g2.fuse(p2, _part(g2, action.creature), _part(g2, action.other))
|
||||
_:
|
||||
g2.attack(g2.find_creature(action.creature.uid), map_target(g2, game, action.target))
|
||||
|
||||
@@ -150,9 +176,9 @@ static func map_target(g2: Game, game: Game, t):
|
||||
static func map_action(game: Game, copy: Game, action: Dictionary) -> Dictionary:
|
||||
var out := action.duplicate()
|
||||
if action.has("creature"):
|
||||
out["creature"] = game.find_creature(action.creature.uid)
|
||||
out["creature"] = _part(game, action.creature)
|
||||
if action.has("other"):
|
||||
out["other"] = game.find_creature(action.other.uid)
|
||||
out["other"] = _part(game, action.other)
|
||||
if action.get("target") != null:
|
||||
out["target"] = map_target(game, copy, action.target)
|
||||
return out
|
||||
@@ -211,7 +237,7 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
||||
static func _kills(attacker, defender) -> bool:
|
||||
if defender is Game.Player:
|
||||
return attacker.power() >= defender.life
|
||||
return attacker.power() + Cards.matchup(attacker.card.elements, defender.card.elements) >= defender.health
|
||||
return attacker.power() + Game.matchup_bonus(attacker, defender) >= defender.health
|
||||
|
||||
|
||||
static func _value(c) -> int:
|
||||
@@ -234,8 +260,14 @@ static func _best(list: Array, key: Callable):
|
||||
static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
|
||||
if game.winner != "" or game.enemies_of(p).is_empty():
|
||||
return {"type": "end"}
|
||||
# Gierig: erst das teuerste Spielbare legen (bei vollem Feld die zwei schwächsten Wesen
|
||||
# verschmelzen), dann aufdecken und angreifen, dann Doppelschlag kaufen, dann ziehen.
|
||||
# Gierig: ein Wesen mit höchstens halbem Leben heilen (mit der billigsten Handkarte
|
||||
# verschmelzen, wenn das Ergebnis bezahlbar bleibt), dann das teuerste Spielbare legen (bei
|
||||
# vollem Feld die zwei schwächsten Wesen verschmelzen), dann aufdecken und angreifen, dann ziehen.
|
||||
var cheap := _cheapest(p)
|
||||
if not cheap.is_empty():
|
||||
for cr in p.board:
|
||||
if cr.health * 2 <= cr.max_health and game.can_fuse(p, cr, cheap[0])[0] and Game.fusion_result(p, cr, cheap[0]).cost <= p.energy:
|
||||
return {"type": "fuse", "creature": cr, "other": cheap[0]}
|
||||
var playable := p.hand.filter(func(c): return game.can_play(p, c)[0])
|
||||
if not playable.is_empty():
|
||||
Game.shuffle(playable, game.rng)
|
||||
@@ -244,7 +276,7 @@ static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
|
||||
if c.cost > card.cost:
|
||||
card = c
|
||||
return {"type": "play", "card": card, "face_down": false}
|
||||
if p.board.size() >= Game.MAX_BOARD and p.hand.any(func(c): return c.cost <= p.energy - 1):
|
||||
if p.board.size() >= Game.MAX_BOARD and p.hand.any(func(c): return c.cost <= p.energy):
|
||||
var weakest: Array = p.board.duplicate()
|
||||
weakest.sort_custom(func(a, b): return _value(a) < _value(b))
|
||||
if game.can_fuse(p, weakest[0], weakest[1])[0]:
|
||||
|
||||
@@ -2,14 +2,24 @@ class_name Cards
|
||||
## Elemente, Kombonamen, Schlüsselwörter und der Kartenpool.
|
||||
|
||||
const ELEMENTS: Array[String] = ["Feuer", "Wasser", "Erde", "Luft"]
|
||||
# Neutral ist kein Element: nie im Vorteil oder Nachteil, zählt nicht für Paare und Zähler.
|
||||
# Verschmolzen mit einem Element nimmt das Wesen im schlechten Matchup die neutrale Seite.
|
||||
const NEUTRAL := "Neutral"
|
||||
const NEUTRAL_TEXT := "Nie im Vorteil oder Nachteil"
|
||||
const ELEMENT_COLORS := {
|
||||
"Feuer": Color("be3c23"),
|
||||
"Wasser": Color("2d64be"),
|
||||
"Erde": Color("73552d"),
|
||||
"Luft": Color("78aac8"),
|
||||
"Neutral": Color("6e7382"),
|
||||
}
|
||||
# Jede Karte hat genau ein Element. Kombos sind die Namen verschmolzener Wesen.
|
||||
# Jede Karte hat genau ein Element (oder ist neutral). Kombos sind die Namen verschmolzener Wesen.
|
||||
const COMBOS := {
|
||||
"Neutral+Neutral": "Veteran",
|
||||
"Feuer+Neutral": "Feuerritter",
|
||||
"Wasser+Neutral": "Wasserritter",
|
||||
"Erde+Neutral": "Erdritter",
|
||||
"Luft+Neutral": "Luftritter",
|
||||
"Feuer+Feuer": "Inferno",
|
||||
"Wasser+Wasser": "Tiefsee",
|
||||
"Erde+Erde": "Berg",
|
||||
@@ -47,9 +57,15 @@ const ELEMENT_KEYWORD := {"Erde": "Spott", "Luft": "Flink", "Wasser": "Regenerat
|
||||
const DATA_PATH := "res://data/karten.json"
|
||||
|
||||
|
||||
## Reihenfolge der Elemente in Namen und Darstellung: wie ELEMENTS, Neutral zuletzt.
|
||||
static func element_order(e: String) -> int:
|
||||
var i := ELEMENTS.find(e)
|
||||
return i if i >= 0 else ELEMENTS.size()
|
||||
|
||||
|
||||
static func combo_key(elements: Array) -> String:
|
||||
var sorted := elements.duplicate()
|
||||
sorted.sort_custom(func(a, b): return ELEMENTS.find(a) < ELEMENTS.find(b))
|
||||
sorted.sort_custom(func(a, b): return element_order(a) < element_order(b))
|
||||
return "+".join(sorted)
|
||||
|
||||
|
||||
@@ -98,11 +114,10 @@ static func cost_of(attack: int, health: int) -> int:
|
||||
return mini(10, (2 * attack + health) / 3)
|
||||
|
||||
|
||||
## Wert eines verschmolzenen Wesens: der bessere Wert plus ein Drittel des schwächeren (mindestens 1),
|
||||
## aufgerundet. Bei gleichen Werten sind das zwei Drittel der Summe, sonst mehr; nie schlechter
|
||||
## als das stärkere Wesen, und jeder Partner bringt mindestens +1, auch mit 0.
|
||||
## Wert eines verschmolzenen Wesens aus den Grundwerten der Eltern: die aufgerundete Hälfte der
|
||||
## Summe plus eins. Ein starker und ein schwacher Partner ergeben also weniger als der starke.
|
||||
static func fusion_value(a: int, b: int) -> int:
|
||||
return maxi(a, b) + maxi(1, ceili(mini(a, b) / 3.0))
|
||||
return ceili((a + b) / 2.0) + 1
|
||||
|
||||
|
||||
## Name des verschmolzenen Wesens: die Kombo seiner Elemente (ein Element zählt doppelt).
|
||||
@@ -110,18 +125,18 @@ static func fusion_name(elements: Array) -> String:
|
||||
return combo_name(elements if elements.size() >= 2 else [elements[0], elements[0]])
|
||||
|
||||
|
||||
## Karte des verschmolzenen Wesens: Elemente und Eigenschaften beider Karten, Werte nach
|
||||
## fusion_value (oder die übergebenen), eine Verschmelzung mehr als der erfahrenere Elternteil.
|
||||
static func fused(a: Card, b: Card, attack: int = -1, health: int = -1) -> Card:
|
||||
if attack < 0:
|
||||
attack = fusion_value(a.attack, b.attack)
|
||||
if health < 0:
|
||||
health = fusion_value(a.health, b.health)
|
||||
## Verschmelzung zweier Karten: Elemente (sortiert) und Eigenschaften beider, Angriff und Leben
|
||||
## nach fusion_value aus den Grundwerten, Kosten daraus wie bei jeder Karte; dann die Stufe level
|
||||
## mit ihrem +1/+1 (ohne Angabe die höhere der Eltern). Das Bild trägt der stärkere Elternteil
|
||||
## (Angriff plus Leben).
|
||||
static func fused(a: Card, b: Card, level: int = -1) -> Card:
|
||||
var attack := fusion_value(a.base_attack(), b.base_attack())
|
||||
var health := fusion_value(a.base_health(), b.base_health())
|
||||
var els := []
|
||||
for e in a.elements + b.elements:
|
||||
if not (e in els):
|
||||
els.append(e)
|
||||
els.sort_custom(func(x, y): return ELEMENTS.find(x) < ELEMENTS.find(y))
|
||||
els.sort_custom(func(x, y): return element_order(x) < element_order(y))
|
||||
var c := Card.new()
|
||||
c.id = "fus_%s_%s" % [a.id, b.id]
|
||||
c.name = fusion_name(els)
|
||||
@@ -132,8 +147,9 @@ static func fused(a: Card, b: Card, attack: int = -1, health: int = -1) -> Card:
|
||||
for k in KEYWORDS:
|
||||
if k in a.keywords or k in b.keywords:
|
||||
c.keywords.append(k)
|
||||
c.look = maxi(a.look, b.look) # die höhere Stufe gewinnt
|
||||
return c
|
||||
var strong := a if a.attack + a.health >= b.attack + b.health else b
|
||||
c.art = strong.art_card().id
|
||||
return c.leveled(maxi(level, maxi(maxi(a.look, b.look), 0)))
|
||||
|
||||
|
||||
## Platzhalter für ein verdecktes gegnerisches Wesen: unbekanntes Element, geschätzte Werte.
|
||||
@@ -159,6 +175,7 @@ class Card extends RefCounted:
|
||||
var cost: int = 0 # abgeleitet: (2 · Angriff + Leben) / 3, abgerundet, höchstens 10 (aus den Grundwerten)
|
||||
var keywords: Array = [] # feste Eigenschaften der Karte (Elementpaare geben ihre zur Laufzeit)
|
||||
var look := -1 # Seltenheitsstufe des Exemplars (gibt +1/+1 je Stufe); -1: Grundkarte aus dem Pool
|
||||
var art := "" # verschmolzen: Karte, deren Bild das Wesen trägt (der stärkere Elternteil)
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
var d := {"id": id, "name": name, "name_en": name_en, "elements": elements, "attack": attack, "health": health}
|
||||
@@ -166,10 +183,24 @@ class Card extends RefCounted:
|
||||
d["keywords"] = keywords
|
||||
if look >= 0:
|
||||
d["look"] = look
|
||||
if art != "":
|
||||
d["art"] = art
|
||||
if cost != Cards.cost_of(attack, health):
|
||||
d["cost"] = cost
|
||||
return d
|
||||
|
||||
## Wert der Karte: was sie über ihre Manakosten hinaus bringt, mal 100. Grundkarten liegen
|
||||
## bei 100, jede Seltenheitsstufe gibt 100 dazu (Ultimate Rare 500); Fusionen aus ihren Werten.
|
||||
func value() -> int:
|
||||
return (Cards.cost_of(attack, health) - cost + 1) * 100
|
||||
|
||||
## Grundwerte ohne den Stufenbonus.
|
||||
func base_attack() -> int:
|
||||
return attack - maxi(look, 0)
|
||||
|
||||
func base_health() -> int:
|
||||
return health - maxi(look, 0)
|
||||
|
||||
## Stufe dieser Karte für einen Besitzer mit dieser Sammlung (Game.Player.looks).
|
||||
func rarity_in(looks: Dictionary) -> String:
|
||||
return Cards.RARITIES[look if look >= 0 else int(looks.get(id, 0))]
|
||||
@@ -185,8 +216,13 @@ class Card extends RefCounted:
|
||||
c.cost = int(d.get("cost", Cards.cost_of(c.attack, c.health)))
|
||||
c.keywords = Array(d.get("keywords", []))
|
||||
c.look = int(d.get("look", -1))
|
||||
c.art = str(d.get("art", ""))
|
||||
return c
|
||||
|
||||
## Karte, deren Bild gezeichnet wird: bei Verschmelzungen der stärkere Elternteil, sonst sie selbst.
|
||||
func art_card() -> Card:
|
||||
return Cards.by_id(art) if art != "" and Cards._by_id.has(art) else self
|
||||
|
||||
## Eigenes Exemplar (Hand, Brett): Schlüsselwörter gehören dem Exemplar, nicht dem Pool.
|
||||
func copy() -> Card:
|
||||
return Card.from_dict(to_dict())
|
||||
@@ -212,14 +248,19 @@ class Card extends RefCounted:
|
||||
func combo() -> String:
|
||||
return Cards.combo_name(elements)
|
||||
|
||||
## Kartentext: die festen Eigenschaften.
|
||||
func is_neutral() -> bool:
|
||||
return Cards.NEUTRAL in elements
|
||||
|
||||
## Kartentext: Neutral und die festen Eigenschaften.
|
||||
func text() -> String:
|
||||
if keywords.is_empty():
|
||||
return ""
|
||||
var kws: Array[String] = []
|
||||
if is_neutral():
|
||||
kws.append(tr(Cards.NEUTRAL))
|
||||
for k in Cards.KEYWORDS:
|
||||
if k in keywords:
|
||||
kws.append(tr(k))
|
||||
if kws.is_empty():
|
||||
return ""
|
||||
return ", ".join(kws) + "."
|
||||
|
||||
|
||||
|
||||
@@ -40,11 +40,15 @@ static func start_deck(element: String) -> Array:
|
||||
return out
|
||||
|
||||
|
||||
## Füllt deck aus pool (Karten-IDs) bis limit auf, die teuersten zuerst; nichts doppelt.
|
||||
static func fill(deck: Array, pool: Array, limit: int) -> Array:
|
||||
## 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.
|
||||
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))
|
||||
cards.sort_custom(func(a, b): return a.cost > b.cost if a.cost != b.cost else a.id < b.id)
|
||||
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
|
||||
@@ -52,5 +56,16 @@ static func fill(deck: Array, pool: Array, limit: int) -> Array:
|
||||
return out
|
||||
|
||||
|
||||
## Stärke eines Stapels: der mittlere Kartenwert (Card.value) in den Stufen aus levels,
|
||||
## 100 für lauter Grundkarten bis 500 für lauter Ultimate Rare; leer: 0.
|
||||
static func strength(deck: Array, levels: Dictionary) -> int:
|
||||
if deck.is_empty():
|
||||
return 0
|
||||
var sum := 0
|
||||
for id in deck:
|
||||
sum += Cards.by_id(id).leveled(int(levels.get(id, 0))).value()
|
||||
return roundi(float(sum) / deck.size())
|
||||
|
||||
|
||||
static func random_opponent(life: int = Game.START_LIFE) -> Game.Player:
|
||||
return Game.Player.new("Gegner", false, life)
|
||||
|
||||
123
scripts/game.gd
123
scripts/game.gd
@@ -4,14 +4,13 @@ extends RefCounted
|
||||
## Elementboni und Verschmelzen.
|
||||
|
||||
const MAX_BOARD := 5
|
||||
const MAX_HAND := 5
|
||||
const START_HAND := 5 # Startkarten, alle im ersten Zug bezahlbar
|
||||
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_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 := 5 # je so viele ausgespielte Karten eines Elements: +1/+1 für die eigenen Wesen 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).
|
||||
@@ -154,23 +153,11 @@ class Player extends RefCounted:
|
||||
func alive() -> bool:
|
||||
return life > 0
|
||||
|
||||
## Eine Karte vom eigenen Stapel als Exemplar in der eigenen Stufe; null, wenn er leer ist.
|
||||
## Mit max_cost kommt die oberste bezahlbare, zu teure wandern nach unten (Starthand).
|
||||
func draw_from_deck(max_cost: int = 99) -> Cards.Card:
|
||||
var skipped := []
|
||||
var id := ""
|
||||
while not deck.is_empty():
|
||||
var top: String = deck.pop_back()
|
||||
if Cards.by_id(top).cost <= max_cost:
|
||||
id = top
|
||||
break
|
||||
skipped.append(top)
|
||||
if id == "" and not skipped.is_empty(): # nichts Bezahlbares: dann eben die oberste
|
||||
id = skipped.pop_front()
|
||||
for sk in skipped:
|
||||
deck.insert(0, sk)
|
||||
if id == "":
|
||||
## Die oberste Karte des eigenen Stapels als Exemplar in der eigenen Stufe; null, wenn er leer ist.
|
||||
func draw_from_deck() -> Cards.Card:
|
||||
if deck.is_empty():
|
||||
return null
|
||||
var id: String = deck.pop_back()
|
||||
return Cards.by_id(id).leveled(int(looks.get(id, 0)))
|
||||
|
||||
## Offene eigene Wesen mit diesem Element.
|
||||
@@ -295,8 +282,8 @@ func masked_clone(viewer: Player) -> Game:
|
||||
|
||||
# --- Zugablauf ---------------------------------------------------------------
|
||||
|
||||
## Jeder spielt mit seinem eigenen Stapel (leer: ein zufälliger); die Starthand sind fünf im
|
||||
## ersten Zug bezahlbare Karten daraus.
|
||||
## Jeder spielt mit seinem eigenen Stapel (leer: ein zufälliger); die Starthand sind die
|
||||
## obersten fünf Karten daraus, wie gemischt.
|
||||
func start(fixed_order: bool = false) -> void:
|
||||
if not fixed_order:
|
||||
shuffle(players, rng) # zufällige Zugreihenfolge
|
||||
@@ -307,7 +294,7 @@ func start(fixed_order: bool = false) -> void:
|
||||
p.deck = DeckGen.random_deck(rng)
|
||||
shuffle(p.deck, rng)
|
||||
for k in START_HAND:
|
||||
var c := p.draw_from_deck(START_MANA)
|
||||
var c := p.draw_from_deck()
|
||||
if c != null:
|
||||
p.hand.append(c)
|
||||
turn = 1
|
||||
@@ -324,8 +311,6 @@ func start(fixed_order: bool = false) -> void:
|
||||
func can_draw(p: Player) -> Array:
|
||||
if winner != "" or p != current():
|
||||
return [false, "Nicht dein Zug"]
|
||||
if p.hand.size() >= MAX_HAND:
|
||||
return [false, "Deine Hand ist voll"]
|
||||
if p.deck.is_empty():
|
||||
return [false, "Dein Stapel ist leer"]
|
||||
if p.energy < draw_cost(p):
|
||||
@@ -497,75 +482,51 @@ static func look_of(p: Player, c: Cards.Card) -> int:
|
||||
return c.look if c.look >= 0 else int(p.looks.get(c.id, 0))
|
||||
|
||||
|
||||
## Kosten: die Hälfte der Manasumme beider Karten, aufgerundet.
|
||||
static func fusion_cost(a, b) -> int:
|
||||
return ceili((card_of(a).cost + card_of(b).cost) / 2.0)
|
||||
|
||||
|
||||
## a und b: zwei eigene Wesen auf dem Brett oder zwei eigene Handkarten.
|
||||
## a und b: zwei verschiedene eigene Karten, auf der Hand oder als Wesen auf dem Brett, auch
|
||||
## gemischt. Verschmelzen kostet kein Mana.
|
||||
func can_fuse(p: Player, a, b) -> Array:
|
||||
if winner != "" or p != current():
|
||||
return [false, "Nicht dein Zug"]
|
||||
if a == null or b == null or a == b:
|
||||
return [false, "Zwei verschiedene Karten wählen"]
|
||||
if a is Creature and b is Creature:
|
||||
if not (a in p.board) or not (b in p.board):
|
||||
return [false, "Nur eigene Wesen"]
|
||||
elif a is Creature or b is Creature:
|
||||
return [false, "Nur Hand mit Hand oder Brett mit Brett"]
|
||||
elif not (a in p.hand) or not (b in p.hand):
|
||||
return [false, "Nicht auf der Hand"]
|
||||
if p.energy < fusion_cost(a, b):
|
||||
return [false, "Nicht genug Mana"]
|
||||
for x in [a, b]:
|
||||
if not ((x in p.board) if x is Creature else (x in p.hand)):
|
||||
return [false, "Nur eigene Karten"]
|
||||
return [true, ""]
|
||||
|
||||
|
||||
## Verschmilzt zwei Handkarten zu einer oder zwei eigene Wesen zu einem offenen: Elemente und
|
||||
## Eigenschaften beider, Werte nach Cards.fusion_value, Kosten folgen den Werten.
|
||||
## Die Karte, die aus a und b entsteht (siehe Cards.fused), in der höheren Stufe der Eltern.
|
||||
static func fusion_result(p: Player, a, b) -> Cards.Card:
|
||||
return Cards.fused(card_of(a), card_of(b), maxi(look_of(p, card_of(a)), look_of(p, card_of(b))))
|
||||
|
||||
|
||||
## Verschmilzt zwei eigene Karten zu einer neuen Handkarte: Wesen verlassen das Brett, die neue
|
||||
## Karte ist unverletzt und wird wie jede andere ausgespielt. Sie landet am Platz der vorderen
|
||||
## Handkarte, sonst hinten.
|
||||
func fuse(p: Player, a, b) -> String:
|
||||
var ok := can_fuse(p, a, b)
|
||||
if not ok[0]:
|
||||
return ok[1]
|
||||
p.energy -= fusion_cost(a, b)
|
||||
if a is Creature:
|
||||
return _fuse_board(p, a, b)
|
||||
var card := Cards.fused(a, b)
|
||||
card.look = maxi(look_of(p, a), look_of(p, b))
|
||||
var at := p.hand.find(a)
|
||||
p.hand.erase(a)
|
||||
p.hand.erase(b)
|
||||
var card := fusion_result(p, a, b)
|
||||
var at := p.hand.size()
|
||||
var gone := []
|
||||
for x in [a, b]:
|
||||
if x is Creature:
|
||||
p.board.erase(x)
|
||||
gone.append(x.uid)
|
||||
else:
|
||||
at = mini(at, p.hand.find(x))
|
||||
p.hand.erase(x)
|
||||
p.hand.insert(mini(at, p.hand.size()), card)
|
||||
say("%s und %s verschmelzen auf der Hand zu %s (%d/%d)." % [a.name, b.name, card.name, card.attack, card.health])
|
||||
emit({"type": "fuse", "player": players.find(p), "uid": -1, "gone": []})
|
||||
return ""
|
||||
|
||||
|
||||
## Auf dem Brett zählen die Werte der Wesen (ohne Elementbonus, den bekommt das neue selbst);
|
||||
## das Ergebnis ist nur angriffsbereit, wenn beide es waren.
|
||||
func _fuse_board(p: Player, a: Creature, b: Creature) -> String:
|
||||
var card := Cards.fused(a.card, b.card, Cards.fusion_value(a.attack, b.attack),
|
||||
Cards.fusion_value(a.max_health - a.aura, b.max_health - b.aura))
|
||||
card.look = maxi(look_of(p, a.card), look_of(p, b.card))
|
||||
var cr := Creature.new(card, p, next_uid)
|
||||
next_uid += 1
|
||||
cr.health = mini(card.health, Cards.fusion_value(mini(a.health, a.max_health - a.aura), mini(b.health, b.max_health - b.aura)))
|
||||
cr.ready = a.ready and b.ready
|
||||
cr.attacks_done = maxi(a.attacks_done, b.attacks_done)
|
||||
var at := p.board.find(a)
|
||||
p.board.erase(a)
|
||||
p.board.erase(b)
|
||||
p.board.insert(mini(at, p.board.size()), cr)
|
||||
say("%s und %s verschmelzen zu %s (%d/%d)." % [a.name, b.name, cr.name, cr.attack, cr.health])
|
||||
emit({"type": "fuse", "player": players.find(p), "uid": cr.uid, "gone": [a.uid, b.uid]})
|
||||
apply_auras()
|
||||
cleanup()
|
||||
check_end()
|
||||
say("%s und %s verschmelzen zu %s (%d/%d) auf der Hand." % [a.name, b.name, card.name, card.attack, card.health])
|
||||
emit({"type": "fuse", "player": players.find(p), "uid": -1, "gone": gone})
|
||||
return ""
|
||||
|
||||
|
||||
# --- Elementboni ---------------------------------------------------------------
|
||||
|
||||
## +1/+1 je volle ELEMENT_COUNT ausgespielte Karten eines Elements des Spielers; mehrere Elemente zählen alle.
|
||||
## +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:
|
||||
@@ -648,12 +609,18 @@ func attack(cr: Creature, target) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
## Elementvorteil beim Zuschlagen und beim Konter: +1 im Vorteil, -1 im Nachteil,
|
||||
## gegen Helden gibt es keinen.
|
||||
## 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.
|
||||
static func matchup_bonus(source: Creature, target) -> int:
|
||||
if not (target is Creature):
|
||||
return 0
|
||||
return Cards.matchup(source.card.elements, target.card.elements)
|
||||
var m := Cards.matchup(source.card.elements, target.card.elements)
|
||||
if m > 0:
|
||||
return target.aura
|
||||
if m < 0:
|
||||
return -source.aura
|
||||
return 0
|
||||
|
||||
|
||||
func damage(target, amount: int) -> void:
|
||||
|
||||
@@ -16,9 +16,9 @@ const REWARD_WIN := 100
|
||||
const REWARD_LOSS := 50
|
||||
# Schwierigkeit: Leben der Gegner, KI und Münzen.
|
||||
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", "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},
|
||||
}
|
||||
# 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}
|
||||
@@ -36,215 +36,215 @@ var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1)
|
||||
|
||||
## Neues Profil: noch ohne Karten, das Startdeck wählt der Spieler (choose_start).
|
||||
static func starter() -> Profile:
|
||||
var p := Profile.new()
|
||||
p.coins = START_COINS
|
||||
return p
|
||||
var p := Profile.new()
|
||||
p.coins = START_COINS
|
||||
return p
|
||||
|
||||
|
||||
## Hat der Spieler schon ein Startdeck gewählt?
|
||||
func started() -> bool:
|
||||
return not collection.is_empty()
|
||||
return not collection.is_empty()
|
||||
|
||||
|
||||
## Das Startdeck eines Elements wird Sammlung und Stapel.
|
||||
func choose_start(element: String) -> void:
|
||||
collection = {}
|
||||
deck = DeckGen.start_deck(element)
|
||||
for id in deck:
|
||||
collection[id] = 0
|
||||
collection = {}
|
||||
deck = DeckGen.start_deck(element)
|
||||
for id in deck:
|
||||
collection[id] = 0
|
||||
|
||||
|
||||
## Lädt den Spielstand; bei Schaden die Sicherung; sonst ein neues Profil.
|
||||
static func load_or_new(path: String = SAVE_PATH) -> Profile:
|
||||
for candidate in [path, path + ".bak"]:
|
||||
if FileAccess.file_exists(candidate):
|
||||
var json := JSON.new()
|
||||
if json.parse(FileAccess.get_file_as_string(candidate)) == OK and json.data is Dictionary:
|
||||
return from_dict(json.data)
|
||||
if FileAccess.file_exists(path):
|
||||
DirAccess.rename_absolute(ProjectSettings.globalize_path(path), ProjectSettings.globalize_path(path) + ".corrupt")
|
||||
var p := starter()
|
||||
p.save(path)
|
||||
return p
|
||||
for candidate in [path, path + ".bak"]:
|
||||
if FileAccess.file_exists(candidate):
|
||||
var json := JSON.new()
|
||||
if json.parse(FileAccess.get_file_as_string(candidate)) == OK and json.data is Dictionary:
|
||||
return from_dict(json.data)
|
||||
if FileAccess.file_exists(path):
|
||||
DirAccess.rename_absolute(ProjectSettings.globalize_path(path), ProjectSettings.globalize_path(path) + ".corrupt")
|
||||
var p := starter()
|
||||
p.save(path)
|
||||
return p
|
||||
|
||||
|
||||
## Liest jede Spielstand-Version; ältere Stände behalten Münzen, Staub, Kampagne und
|
||||
## Einstellungen und bekommen die Startsammlung.
|
||||
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.campaign_stage = int(d.get("campaign_stage", 0))
|
||||
if int(d.get("version", 1)) >= SAVE_VERSION:
|
||||
var coll = d.get("collection", {})
|
||||
for cid in coll:
|
||||
if Cards.has_id(cid):
|
||||
p.collection[cid] = clampi(int(coll[cid]), 0, Cards.RARITIES.size() - 1)
|
||||
var ids := []
|
||||
for cid in d.get("deck", []):
|
||||
if p.collection.has(cid) and not (cid in ids):
|
||||
ids.append(cid)
|
||||
p.deck = ids.slice(0, Game.DECK_MAX)
|
||||
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)),
|
||||
"language": str(se.get("language", "de")), "difficulty": str(se.get("difficulty", "normal")),
|
||||
"big_font": bool(se.get("big_font", false))}
|
||||
if not DIFFICULTIES.has(p.settings.difficulty):
|
||||
p.settings.difficulty = "normal"
|
||||
p.perk = str(d.get("perk", ""))
|
||||
if not Game.PERKS.has(p.perk):
|
||||
p.perk = ""
|
||||
return p
|
||||
var p := Profile.new()
|
||||
p.coins = int(d.get("coins", 0))
|
||||
p.dust = int(d.get("dust", 0))
|
||||
p.campaign_stage = int(d.get("campaign_stage", 0))
|
||||
if int(d.get("version", 1)) >= SAVE_VERSION:
|
||||
var coll = d.get("collection", {})
|
||||
for cid in coll:
|
||||
if Cards.has_id(cid):
|
||||
p.collection[cid] = clampi(int(coll[cid]), 0, Cards.RARITIES.size() - 1)
|
||||
var ids := []
|
||||
for cid in d.get("deck", []):
|
||||
if p.collection.has(cid) and not (cid in ids):
|
||||
ids.append(cid)
|
||||
p.deck = ids.slice(0, Game.DECK_MAX)
|
||||
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)),
|
||||
"language": str(se.get("language", "de")), "difficulty": str(se.get("difficulty", "normal")),
|
||||
"big_font": bool(se.get("big_font", false))}
|
||||
if not DIFFICULTIES.has(p.settings.difficulty):
|
||||
p.settings.difficulty = "normal"
|
||||
p.perk = str(d.get("perk", ""))
|
||||
if not Game.PERKS.has(p.perk):
|
||||
p.perk = ""
|
||||
return p
|
||||
|
||||
|
||||
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, "collection": collection, "deck": deck,
|
||||
"campaign_stage": campaign_stage, "settings": settings, "perk": perk}
|
||||
|
||||
|
||||
## Speichert und sichert den vorherigen Stand als .bak.
|
||||
func save(path: String = SAVE_PATH) -> void:
|
||||
var abs := ProjectSettings.globalize_path(path)
|
||||
if FileAccess.file_exists(path):
|
||||
DirAccess.copy_absolute(abs, abs + ".bak")
|
||||
var f := FileAccess.open(path, FileAccess.WRITE)
|
||||
f.store_string(JSON.stringify(to_dict(), "\t"))
|
||||
f.close()
|
||||
var abs := ProjectSettings.globalize_path(path)
|
||||
if FileAccess.file_exists(path):
|
||||
DirAccess.copy_absolute(abs, abs + ".bak")
|
||||
var f := FileAccess.open(path, FileAccess.WRITE)
|
||||
f.store_string(JSON.stringify(to_dict(), "\t"))
|
||||
f.close()
|
||||
|
||||
|
||||
func take_notices() -> Array[String]:
|
||||
var out := notices.duplicate()
|
||||
notices.clear()
|
||||
return out
|
||||
var out := notices.duplicate()
|
||||
notices.clear()
|
||||
return out
|
||||
|
||||
|
||||
# --- Sammlung und Stapel -----------------------------------------------------------
|
||||
|
||||
func owns(cid: String) -> bool:
|
||||
return collection.has(cid)
|
||||
return collection.has(cid)
|
||||
|
||||
|
||||
func level(cid: String) -> int:
|
||||
return int(collection.get(cid, 0))
|
||||
return int(collection.get(cid, 0))
|
||||
|
||||
|
||||
func rarity(cid: String) -> String:
|
||||
return Cards.RARITIES[level(cid)]
|
||||
return Cards.RARITIES[level(cid)]
|
||||
|
||||
|
||||
## Karten-ID -> Stufe für den Kampf (nur aufgewertete Karten, siehe Game.Player.looks).
|
||||
func looks() -> Dictionary:
|
||||
var out := {}
|
||||
for cid in collection:
|
||||
if int(collection[cid]) > 0:
|
||||
out[cid] = int(collection[cid])
|
||||
return out
|
||||
var out := {}
|
||||
for cid in collection:
|
||||
if int(collection[cid]) > 0:
|
||||
out[cid] = int(collection[cid])
|
||||
return out
|
||||
|
||||
|
||||
## Der Stapel für den Kampf: der gewählte, mit weiteren eigenen Karten bis DECK_MIN aufgefüllt,
|
||||
## falls er zu klein ist (Karten können durch Zerlegen wegfallen).
|
||||
func deck_cards() -> Array:
|
||||
return DeckGen.fill(deck.filter(func(id): return collection.has(id)), collection.keys(), Game.DECK_MIN)
|
||||
return DeckGen.fill(deck.filter(func(id): return collection.has(id)), collection.keys(), Game.DECK_MIN, collection)
|
||||
|
||||
|
||||
## Karte in den Stapel oder heraus. Gibt "" zurück oder eine Fehlermeldung.
|
||||
func toggle_deck(cid: String) -> String:
|
||||
if cid in deck:
|
||||
deck.erase(cid)
|
||||
return ""
|
||||
if not owns(cid):
|
||||
return "Karte nicht in der Sammlung"
|
||||
if deck.size() >= Game.DECK_MAX:
|
||||
return "Der Stapel ist voll (%d Karten)" % Game.DECK_MAX
|
||||
deck.append(cid)
|
||||
return ""
|
||||
if cid in deck:
|
||||
deck.erase(cid)
|
||||
return ""
|
||||
if not owns(cid):
|
||||
return "Karte nicht in der Sammlung"
|
||||
if deck.size() >= Game.DECK_MAX:
|
||||
return "Der Stapel ist voll (%d Karten)" % Game.DECK_MAX
|
||||
deck.append(cid)
|
||||
return ""
|
||||
|
||||
|
||||
## Wertet eine Karte um eine Stufe auf. Gibt "" zurück oder eine Fehlermeldung.
|
||||
func upgrade(cid: String) -> String:
|
||||
if not owns(cid):
|
||||
return "Karte nicht in der Sammlung"
|
||||
var lvl := level(cid)
|
||||
if lvl >= Cards.RARITIES.size() - 1:
|
||||
return "Schon auf der höchsten Stufe"
|
||||
var cost: int = DUST_UPGRADE[Cards.RARITIES[lvl + 1]]
|
||||
if dust < cost:
|
||||
return "Nicht genug Staub (%d nötig)" % cost
|
||||
dust -= cost
|
||||
collection[cid] = lvl + 1
|
||||
return ""
|
||||
if not owns(cid):
|
||||
return "Karte nicht in der Sammlung"
|
||||
var lvl := level(cid)
|
||||
if lvl >= Cards.RARITIES.size() - 1:
|
||||
return "Schon auf der höchsten Stufe"
|
||||
var cost: int = DUST_UPGRADE[Cards.RARITIES[lvl + 1]]
|
||||
if dust < cost:
|
||||
return "Nicht genug Staub (%d nötig)" % cost
|
||||
dust -= cost
|
||||
collection[cid] = lvl + 1
|
||||
return ""
|
||||
|
||||
|
||||
## Entzaubern: eine Stufe abgeben bringt Staub; eine Common-Karte verlässt die Sammlung.
|
||||
## Gibt "" zurück oder eine Fehlermeldung.
|
||||
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]]
|
||||
if lvl == 0:
|
||||
collection.erase(cid)
|
||||
deck.erase(cid)
|
||||
else:
|
||||
collection[cid] = lvl - 1
|
||||
return ""
|
||||
if not owns(cid):
|
||||
return "Karte nicht in der Sammlung"
|
||||
var lvl := level(cid)
|
||||
dust += DUST_DISMANTLE[Cards.RARITIES[lvl]]
|
||||
if lvl == 0:
|
||||
collection.erase(cid)
|
||||
deck.erase(cid)
|
||||
else:
|
||||
collection[cid] = lvl - 1
|
||||
return ""
|
||||
|
||||
|
||||
## Herstellen: eine fehlende Karte für Staub als Common. Gibt "" zurück oder eine Fehlermeldung.
|
||||
func craft(cid: String) -> String:
|
||||
if owns(cid):
|
||||
return "Karte schon in der Sammlung"
|
||||
if dust < CRAFT_COST:
|
||||
return "Nicht genug Staub (%d nötig)" % CRAFT_COST
|
||||
dust -= CRAFT_COST
|
||||
collection[cid] = 0
|
||||
return ""
|
||||
if owns(cid):
|
||||
return "Karte schon in der Sammlung"
|
||||
if dust < CRAFT_COST:
|
||||
return "Nicht genug Staub (%d nötig)" % CRAFT_COST
|
||||
dust -= CRAFT_COST
|
||||
collection[cid] = 0
|
||||
return ""
|
||||
|
||||
|
||||
# --- Booster ----------------------------------------------------------------------
|
||||
|
||||
## Fünf Karten mit gewürfelter Seltenheit.
|
||||
static func roll_booster(r: RandomNumberGenerator) -> Array:
|
||||
var rarities := RARITY_WEIGHTS.keys()
|
||||
var weights := PackedFloat32Array(RARITY_WEIGHTS.values())
|
||||
var pool := Cards.all()
|
||||
var out := []
|
||||
for i in BOOSTER_SIZE:
|
||||
var rar: String = rarities[r.rand_weighted(weights)]
|
||||
if i == BOOSTER_SIZE - 1 and out.all(func(x): return x.rarity == "Common"):
|
||||
rar = "Rare" # mindestens eine Rare pro Booster
|
||||
out.append({"card": pool[r.randi_range(0, pool.size() - 1)], "rarity": rar})
|
||||
return out
|
||||
var rarities := RARITY_WEIGHTS.keys()
|
||||
var weights := PackedFloat32Array(RARITY_WEIGHTS.values())
|
||||
var pool := Cards.all()
|
||||
var out := []
|
||||
for i in BOOSTER_SIZE:
|
||||
var rar: String = rarities[r.rand_weighted(weights)]
|
||||
if i == BOOSTER_SIZE - 1 and out.all(func(x): return x.rarity == "Common"):
|
||||
rar = "Rare" # mindestens eine Rare pro Booster
|
||||
out.append({"card": pool[r.randi_range(0, pool.size() - 1)], "rarity": rar})
|
||||
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:
|
||||
return []
|
||||
if r == null:
|
||||
r = Game.make_rng()
|
||||
coins -= BOOSTER_COST
|
||||
var out := roll_booster(r)
|
||||
for res in out:
|
||||
var lvl: int = Cards.RARITIES.find(res.rarity)
|
||||
res["new"] = not owns(res.card.id)
|
||||
res["upgraded"] = false
|
||||
res["dust"] = 0
|
||||
if res.new:
|
||||
collection[res.card.id] = lvl
|
||||
if deck.size() < Game.DECK_MAX:
|
||||
deck.append(res.card.id)
|
||||
elif lvl > level(res.card.id):
|
||||
res["upgraded"] = true
|
||||
res["dust"] = DUST_DISMANTLE[rarity(res.card.id)]
|
||||
collection[res.card.id] = lvl
|
||||
else:
|
||||
res["dust"] = DUST_DISMANTLE[res.rarity]
|
||||
dust += res.dust
|
||||
return out
|
||||
if coins < BOOSTER_COST:
|
||||
return []
|
||||
if r == null:
|
||||
r = Game.make_rng()
|
||||
coins -= BOOSTER_COST
|
||||
var out := roll_booster(r)
|
||||
for res in out:
|
||||
var lvl: int = Cards.RARITIES.find(res.rarity)
|
||||
res["new"] = not owns(res.card.id)
|
||||
res["upgraded"] = false
|
||||
res["dust"] = 0
|
||||
if res.new:
|
||||
collection[res.card.id] = lvl
|
||||
if deck.size() < Game.DECK_MAX:
|
||||
deck.append(res.card.id)
|
||||
elif lvl > level(res.card.id):
|
||||
res["upgraded"] = true
|
||||
res["dust"] = DUST_DISMANTLE[rarity(res.card.id)]
|
||||
collection[res.card.id] = lvl
|
||||
else:
|
||||
res["dust"] = DUST_DISMANTLE[res.rarity]
|
||||
dust += res.dust
|
||||
return out
|
||||
|
||||
|
||||
# --- Kampf und Kampagne -----------------------------------------------------------
|
||||
@@ -252,20 +252,20 @@ func open_booster(r: RandomNumberGenerator = null) -> Array:
|
||||
## 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
|
||||
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)
|
||||
coins += reward
|
||||
return reward
|
||||
var reward := REWARD_LOSS
|
||||
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)
|
||||
coins += reward
|
||||
return reward
|
||||
|
||||
|
||||
func difficulty() -> Dictionary:
|
||||
return DIFFICULTIES.get(settings.get("difficulty", "normal"), DIFFICULTIES.normal)
|
||||
return DIFFICULTIES.get(settings.get("difficulty", "normal"), DIFFICULTIES.normal)
|
||||
|
||||
|
||||
func perk_unlocked() -> bool:
|
||||
return campaign_stage >= Campaign.STAGES.size()
|
||||
return campaign_stage >= Campaign.STAGES.size()
|
||||
|
||||
@@ -139,14 +139,31 @@ func pop_in(tile: CreatureView) -> void:
|
||||
|
||||
|
||||
func fade_out(tile: CreatureView) -> void:
|
||||
tile.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
fade_view(tile)
|
||||
|
||||
|
||||
## Blendet eine Ansicht aus und entfernt sie (Wesen, die sterben; Handkarten, die verschmelzen).
|
||||
func fade_view(view: Control) -> void:
|
||||
view.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
if not enabled:
|
||||
tile.queue_free()
|
||||
view.queue_free()
|
||||
return
|
||||
var tw := tile.create_tween().set_parallel(true)
|
||||
tw.tween_property(tile, "modulate:a", 0.0, 0.3)
|
||||
tw.tween_property(tile, "scale", Vector2(0.6, 0.6), 0.3)
|
||||
tw.chain().tween_callback(tile.queue_free)
|
||||
var tw := view.create_tween().set_parallel(true)
|
||||
tw.tween_property(view, "modulate:a", 0.0, 0.3)
|
||||
tw.tween_property(view, "scale", Vector2(0.6, 0.6), 0.3)
|
||||
tw.chain().tween_callback(view.queue_free)
|
||||
|
||||
|
||||
## Eine Ansicht gleitet von from an ihren Platz (die neue Handkarte nach dem Verschmelzen).
|
||||
func glide(view: Control, from: Rect2) -> void:
|
||||
if not enabled:
|
||||
return
|
||||
var to := view.position
|
||||
view.position = from.position
|
||||
view.scale = from.size / view.size
|
||||
var tw := view.create_tween().set_parallel(true)
|
||||
tw.tween_property(view, "position", to, 0.25).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
tw.tween_property(view, "scale", Vector2.ONE, 0.25).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
func move_to(tile: CreatureView, pos: Vector2) -> void:
|
||||
|
||||
@@ -8,7 +8,7 @@ const B_UNDO := Rect2(20, 10, 210, 32) # links oben: Rückgängig, d
|
||||
const B_SKIP := Rect2(20, 48, 210, 32)
|
||||
const OPP_DECK := Rect2(1090, 10, 130, 182) # Stapel des Gegners rechts oben
|
||||
const OPP_BOX := Rect2(1050, 200, 210, 70) # Kasten des Gegners darunter
|
||||
const OPP_HAND_Y := 11
|
||||
const OPP_HAND_Y := 22 # Bogen hebt die äußeren Rückseiten um bis zu 12 Pixel
|
||||
const OPP_ELEMENTS := Rect2(20, 220, 210, 128) # Elementzähler des Gegners, links von seinem Brett
|
||||
const OWN_ELEMENTS := Rect2(20, 440, 210, 128) # eigene Elementzähler, links vom eigenen Brett
|
||||
const INFO := Rect2(20, 580, 210, 60) # Zusatzzeile zur Vorschau
|
||||
@@ -16,11 +16,11 @@ const MSG := Rect2(20, 650, 210, 80) # Meldungen
|
||||
const STATUS := Rect2(1050, 526, 210, 70) # eigener Kasten rechts über dem Stapel
|
||||
const DECK := Rect2(1090, 606, 130, 182) # eigener Stapel rechts unten
|
||||
const OPTIONS := Rect2(440, 180, 400, 420) # Optionsmenü (Esc)
|
||||
const OWN_X := 345
|
||||
const HAND_W := 110 # Handkarten so groß wie die Wesen auf dem Feld
|
||||
const HAND_H := 140
|
||||
const HAND_GAP := 10
|
||||
const HAND_Y := 610
|
||||
const OWN_X := 295
|
||||
const HAND_W := 130 # Handkarten so groß wie die Wesen auf dem Feld
|
||||
const HAND_H := 165
|
||||
const HAND_GAP := 6
|
||||
const HAND_Y := 605
|
||||
const HAND_ARC := 3.0 # Grad Neigung und Pixel Absenkung je Platz von der Mitte (leichter Bogen)
|
||||
const LOG_PANEL := Rect2(240, 60, 800, 560)
|
||||
const PREVIEW_SIZE := Vector2(170, 238)
|
||||
@@ -29,19 +29,23 @@ const CENTER_W := 800
|
||||
|
||||
|
||||
static func own_slot(j: int) -> Rect2:
|
||||
return Rect2(OWN_X + j * 120, 440, 110, 140)
|
||||
return Rect2(OWN_X + j * 140, 410, HAND_W, HAND_H)
|
||||
|
||||
|
||||
## Brett des Gegners über seiner Elementleiste.
|
||||
static func opp_slot(j: int) -> Rect2:
|
||||
return Rect2(OWN_X + j * 120, 220, 110, 140)
|
||||
return Rect2(OWN_X + j * 140, 205, HAND_W, HAND_H)
|
||||
|
||||
|
||||
## Hand: bis zu fünf Karten unten, mittig; den Bogen legt hand_arc dazu.
|
||||
## Hand: alle Karten unten, mittig; passen sie nicht nebeneinander, überlappen sie sich
|
||||
## gleichmäßig. Den Bogen legt hand_arc dazu.
|
||||
static func hand_rect(count: int, i: int) -> Rect2:
|
||||
var total := count * HAND_W + maxi(0, count - 1) * HAND_GAP
|
||||
var step: float = HAND_W + HAND_GAP
|
||||
if count > 1:
|
||||
step = minf(step, float(CENTER_W - HAND_W) / (count - 1))
|
||||
var total: float = HAND_W + (count - 1) * step
|
||||
var x := CENTER_X + (CENTER_W - total) / 2
|
||||
return Rect2(x + i * (HAND_W + HAND_GAP), HAND_Y, HAND_W, HAND_H)
|
||||
return Rect2(x + i * step, HAND_Y, HAND_W, HAND_H)
|
||||
|
||||
|
||||
## Hand des Gegners oben, gleich groß, nur die Rückseiten.
|
||||
|
||||
@@ -5,8 +5,9 @@ extends Control
|
||||
## die Leertaste oder der Rechtsklick ins Leere.
|
||||
## Mittlere Maustaste auf eine Handkarte legt sie verdeckt. Rechtsklick auf eine Handkarte oder
|
||||
## ein eigenes Wesen öffnet das Kartenmenü: verdeckt legen, aufdecken, Eigenschaften kaufen.
|
||||
## Verschmelzen: eine Karte auf eine andere ziehen und dort eine Sekunde stillhalten. Mit
|
||||
## Gamepad öffnet Y das Menü, dort zusätzlich mit „Verschmelzen mit …“.
|
||||
## Verschmelzen: eine Handkarte oder ein eigenes Wesen auf ein anderes ziehen und loslassen; das
|
||||
## Ergebnis (eine neue Handkarte) erscheint über dem Ziel, darunter Bestätigen oder Abbrechen.
|
||||
## Mit Gamepad öffnet Y das Menü, dort zusätzlich mit „Verschmelzen mit …“.
|
||||
## Modi: Einzelspieler und Kampagne.
|
||||
|
||||
var app
|
||||
@@ -17,15 +18,13 @@ var fx: BattleFx
|
||||
var selected = null # Game.Creature
|
||||
var hover_target = null # fremdes Wesen unter der Maus: Vorschau des Angriffs mit dem gewählten Wesen
|
||||
var fuse_first = null # Karte oder Wesen, für das im Menü „Verschmelzen mit …“ gewählt wurde
|
||||
var hold_time := 1.0 # so lange still über dem Ziel halten, dann verschmelzen die beiden
|
||||
var fuse_pending = null # [a, b] nach dem Loslassen über einem Ziel, wartet auf Bestätigung
|
||||
var press_target = null # Karte oder Wesen unter der gedrückten linken Maustaste
|
||||
var press_view: Control = null
|
||||
var press_pos := Vector2.ZERO
|
||||
var drag_ghost: Control = null # schwebende Kopie der gezogenen Karte
|
||||
var drag_target = null # mögliches Verschmelzungsziel unter dem Zeiger
|
||||
var drag_target_view: Control = null
|
||||
var hold_t := 0.0
|
||||
var hold_pos := Vector2.ZERO
|
||||
var msg_t := 0.0
|
||||
var ai_delay := 0.75
|
||||
var ai_timer := 0.0
|
||||
@@ -77,6 +76,7 @@ var info_label: Label # Vorschau: Elementbonus, Merkmale
|
||||
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
|
||||
var hand_arrivals := {} # Karte -> Rect2: neue Handkarte, die aus der Verschmelzung in die Hand gleitet
|
||||
var banner_turn := -1 # Zug, für den die Zugansage schon lief
|
||||
var overlay: Control
|
||||
var result_label: Label
|
||||
@@ -408,15 +408,6 @@ func _process(delta: float) -> void:
|
||||
undo_t -= delta
|
||||
if undo_t <= 0.0:
|
||||
clear_undo()
|
||||
if drag_ghost != null and drag_target != null:
|
||||
_set_hold(hold_t + delta)
|
||||
if hold_t >= hold_time:
|
||||
var a = press_target
|
||||
var b = drag_target
|
||||
press_target = null
|
||||
_end_drag()
|
||||
do_fuse(a, b)
|
||||
after_action()
|
||||
if game.winner != "":
|
||||
if not ended:
|
||||
ended = true
|
||||
@@ -487,7 +478,7 @@ func cancel() -> void:
|
||||
_end_drag()
|
||||
|
||||
|
||||
# --- Ziehen mit der Maus: Klick beim Loslassen, Verschmelzen nach einer Sekunde über dem Ziel ---
|
||||
# --- Ziehen mit der Maus: Klick beim Loslassen; losgelassen über einem Ziel fragt nach dem Verschmelzen ---
|
||||
|
||||
func _press(obj, view: Control, pos: Vector2) -> void:
|
||||
_end_drag()
|
||||
@@ -505,23 +496,28 @@ func _drag_motion(pos: Vector2) -> void:
|
||||
_start_drag(pos)
|
||||
drag_ghost.position = pos - drag_ghost.size / 2
|
||||
var t = _fusion_target_at(pos)
|
||||
if t != drag_target or pos.distance_to(hold_pos) > 6.0:
|
||||
_set_hold(0.0)
|
||||
if t != drag_target:
|
||||
_mark_target(false)
|
||||
drag_target = t
|
||||
drag_target_view = _view_of(t)
|
||||
hold_pos = pos
|
||||
_mark_target(true)
|
||||
|
||||
|
||||
## Loslassen ohne Ziehen ist der Klick.
|
||||
## Loslassen ohne Ziehen ist der Klick; über einem Ziel öffnet es die Verschmelzungsfrage.
|
||||
func _release() -> void:
|
||||
var obj = press_target
|
||||
var target = drag_target
|
||||
var target_rect := _rect_of(drag_target_view) if drag_target_view != null else Rect2()
|
||||
var dragged := drag_ghost != null
|
||||
press_target = null
|
||||
_end_drag()
|
||||
if obj == null:
|
||||
return
|
||||
if dragged:
|
||||
refresh()
|
||||
if target != null:
|
||||
open_fuse_confirm(obj, target, target_rect)
|
||||
else:
|
||||
refresh()
|
||||
elif obj is Cards.Card:
|
||||
on_click("hand", obj)
|
||||
else:
|
||||
@@ -549,24 +545,27 @@ func _end_drag() -> void:
|
||||
drag_ghost = null
|
||||
if is_instance_valid(press_view):
|
||||
press_view.modulate.a = 1.0
|
||||
_set_hold(0.0)
|
||||
_mark_target(false)
|
||||
drag_target = null
|
||||
drag_target_view = null
|
||||
|
||||
|
||||
func _set_hold(v: float) -> void:
|
||||
hold_t = v
|
||||
func _mark_target(on: bool) -> void:
|
||||
if is_instance_valid(drag_target_view):
|
||||
drag_target_view.hold = clampf(v / hold_time, 0.0, 1.0) if v > 0.0 else 0.0
|
||||
drag_target_view.drop = on
|
||||
drag_target_view.queue_redraw()
|
||||
|
||||
|
||||
## Karte oder Wesen unter pos, mit dem sich die gezogene Karte verschmelzen lässt.
|
||||
## Platz einer Ansicht: Kacheln zählen an ihrem Platz, auch während sie dorthin gleiten.
|
||||
func _rect_of(v: Control) -> Rect2:
|
||||
return Rect2(v.slot, v.size) if v is CreatureView else v.get_global_rect()
|
||||
|
||||
|
||||
## Handkarte oder eigenes Wesen unter pos, mit dem sich die gezogene Karte verschmelzen lässt.
|
||||
func _fusion_target_at(pos: Vector2):
|
||||
for v in (hand_views if press_target is Cards.Card else own_views):
|
||||
for v in hand_views + own_views:
|
||||
var obj = v.card if v is CardView else v.creature
|
||||
var r: Rect2 = Rect2(v.slot, v.size) if v is CreatureView else v.get_global_rect() # Kacheln zählen an ihrem Platz, auch während sie dorthin gleiten
|
||||
if obj != press_target and r.has_point(pos) and game.can_fuse(perspective, press_target, obj)[0]:
|
||||
if obj != press_target and _rect_of(v).has_point(pos) and game.can_fuse(perspective, press_target, obj)[0]:
|
||||
return obj
|
||||
return null
|
||||
|
||||
@@ -581,9 +580,9 @@ func _view_of(obj) -> Control:
|
||||
return null
|
||||
|
||||
|
||||
## Mögliche Partner für „Verschmelzen mit …“: andere Handkarten bzw. andere eigene Wesen.
|
||||
## Mögliche Partner für „Verschmelzen mit …“: alle anderen Handkarten und eigenen Wesen.
|
||||
func fusion_partners(target) -> Array:
|
||||
var pool: Array = perspective.hand if target is Cards.Card else perspective.board
|
||||
var pool: Array = perspective.hand + perspective.board
|
||||
return pool.filter(func(o): return o != target and game.can_fuse(perspective, target, o)[0])
|
||||
|
||||
|
||||
@@ -675,6 +674,37 @@ func close_menu() -> void:
|
||||
menu_layer.visible = false
|
||||
Style.clear(menu_layer)
|
||||
menu_target = null
|
||||
fuse_pending = null
|
||||
|
||||
|
||||
## Nach dem Loslassen über einem Ziel: die neue Handkarte schwebt über dem Ziel, darunter
|
||||
## Bestätigen und Abbrechen; beide Ausgangskarten bleiben sichtbar. Klick daneben oder Esc bricht ab.
|
||||
func open_fuse_confirm(a, b, target: Rect2) -> void:
|
||||
cancel()
|
||||
Style.clear(menu_layer)
|
||||
fuse_pending = [a, b]
|
||||
var card := Game.fusion_result(perspective, a, b)
|
||||
var size := Vector2(BattleLayout.HAND_W, BattleLayout.HAND_H)
|
||||
var bw := 150.0
|
||||
var x := clampf(target.get_center().x - size.x / 2, 10 + bw - size.x / 2, 1270 - bw - size.x / 2)
|
||||
var y := maxf(10.0, target.position.y - size.y - 48)
|
||||
var v := CardView.new(card, size, false, Cards.RARITIES[card.look])
|
||||
v.position = Vector2(x, y)
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
menu_layer.add_child(v)
|
||||
var by := y + size.y + 6
|
||||
var bx := x + size.x / 2 - bw - 3
|
||||
var yes := Style.button(menu_layer, tr("Verschmelzen"), Rect2(bx, by, bw, 36), func():
|
||||
var pair: Array = fuse_pending
|
||||
close_menu()
|
||||
do_fuse(pair[0], pair[1], Rect2(v.position, v.size))
|
||||
after_action(), Color("285a3c"))
|
||||
yes.add_theme_font_size_override("font_size", 15)
|
||||
var no := Style.button(menu_layer, tr("Abbrechen"), Rect2(bx + bw + 6, by, bw, 36), func():
|
||||
close_menu()
|
||||
refresh())
|
||||
no.add_theme_font_size_override("font_size", 15)
|
||||
menu_layer.visible = true
|
||||
|
||||
|
||||
## Abwerfen: eine Handkarte gegen Mana loswerden.
|
||||
@@ -696,13 +726,26 @@ func do_draw() -> void:
|
||||
fx.sfx("buy")
|
||||
|
||||
|
||||
## Verschmilzt zwei Handkarten oder zwei eigene Wesen (siehe Game.fuse).
|
||||
func do_fuse(a, b) -> void:
|
||||
## Verschmilzt zwei eigene Karten zur neuen Handkarte (siehe Game.fuse). from: von dort gleitet
|
||||
## die neue Karte in die Hand, Eltern auf der Hand blenden aus (Wesen tut das der Abgleich).
|
||||
func do_fuse(a, b, from: Rect2 = Rect2()) -> void:
|
||||
cancel()
|
||||
clear_undo()
|
||||
var before: Array = perspective.hand.duplicate()
|
||||
var err := game.fuse(perspective, a, b)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
return
|
||||
if from.size.x <= 0:
|
||||
return
|
||||
for x in [a, b]:
|
||||
var pv := _view_of(x)
|
||||
if pv is CardView:
|
||||
pv.reparent(fx_layer)
|
||||
fx.fade_view(pv)
|
||||
for c in perspective.hand:
|
||||
if not (c in before):
|
||||
hand_arrivals[c] = from
|
||||
|
||||
|
||||
func clear_undo() -> void:
|
||||
@@ -747,7 +790,8 @@ func on_click(kind: String, obj) -> void:
|
||||
var first = fuse_first
|
||||
fuse_first = null
|
||||
if obj != null and obj != first and g.can_fuse(perspective, first, obj)[0]:
|
||||
do_fuse(first, obj)
|
||||
var tv := _view_of(obj)
|
||||
do_fuse(first, obj, _rect_of(tv) if tv != null else Rect2())
|
||||
else:
|
||||
say(tr("Abgebrochen"))
|
||||
after_action()
|
||||
@@ -846,6 +890,8 @@ func creature_info(cr: Game.Creature) -> String:
|
||||
var perk := cr.power() - cr.attack - cr.aura
|
||||
if perk > 0:
|
||||
parts.append(tr("Merkmal +%d") % perk)
|
||||
if cr.card.is_neutral():
|
||||
parts.append(tr(Cards.NEUTRAL_TEXT))
|
||||
for k in cr.keywords():
|
||||
if not (k in cr.card.keywords):
|
||||
parts.append(tr("%s durch das Element") % tr(k))
|
||||
@@ -1089,6 +1135,9 @@ func refresh() -> void:
|
||||
var arc: Array = BattleLayout.hand_arc(h.hand.size(), i)
|
||||
v.rotation_degrees = arc[0]
|
||||
v.position.y += arc[1]
|
||||
if hand_arrivals.has(card):
|
||||
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]:
|
||||
|
||||
@@ -5,9 +5,9 @@ extends ScrollContainer
|
||||
signal card_clicked(card: Cards.Card)
|
||||
|
||||
const COLS := 6
|
||||
const CW := 112
|
||||
const CH := 157
|
||||
const GAP := 14
|
||||
const CW := 118
|
||||
const CH := 165
|
||||
const GAP := 10
|
||||
|
||||
var grid: GridContainer
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
class_name CardView
|
||||
extends Control
|
||||
## Zeichnet eine Karte (Hand, Sammlung, Booster, Vorschau).
|
||||
## Aufbau wie bei Hearthstone: Kostenkristall oben links, Bild, Namensband, Eigenschaften als
|
||||
## Symbole, Angriff und Leben als Kreise unten. Das erste Element färbt den Hintergrund, weitere
|
||||
## Elemente zeigt ein Medaillon als Tortenstücke hinter dem Bild; die Seltenheit steckt in Name,
|
||||
## Rahmen, Glanz und Edelstein (siehe Style). Sehr flache Karten zeigen nur das Nötigste.
|
||||
## Oben Kosten (links) und Name, in der Mitte das Bild im Kreis, unten Angriff (links) und
|
||||
## Leben (rechts). Die Elemente färben den Hintergrund (verschmolzen: ein Streifen je Element),
|
||||
## das Bild trägt bei Verschmelzungen der stärkere Elternteil; die Seltenheit steckt in Name,
|
||||
## Bild, Werten und Rahmen (siehe Style).
|
||||
|
||||
signal clicked(view: CardView)
|
||||
signal pressed(view: CardView, pos: Vector2) # nur mit drag_enabled: der Kampf macht daraus Klick oder Ziehen
|
||||
@@ -17,7 +17,7 @@ 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 hold := 0.0 # Fortschritt des Verschmelzens, wenn eine Karte darüber gehalten wird (0..1)
|
||||
var drop := false # Ziel der gerade gezogenen Karte
|
||||
|
||||
|
||||
## focusable: per Gamepad oder Tab anwählbar (Sammlung, Booster); A bzw. Enter wirkt wie ein Klick.
|
||||
@@ -75,120 +75,35 @@ func _draw() -> void:
|
||||
if rarity != "Common":
|
||||
Style.glow(self, rect, Style.rarity_color(rarity, seed), 8, 8 if size.x >= 180 else 6)
|
||||
Style.fill_elements(self, rect, card.elements, 8)
|
||||
if size.y < 90:
|
||||
_draw_compact(rect, frame, seed)
|
||||
else:
|
||||
_draw_full(rect, frame, seed)
|
||||
var big := size.x >= 180
|
||||
var mid := size.x >= 140
|
||||
var f_name := 17 if big else (14 if mid else 11)
|
||||
var f_stat := 20 if big else (17 if mid else 14)
|
||||
var r_cost := 17.0 if big else (14.0 if mid else 12.0)
|
||||
var r_stat := 18.0 if big else (15.0 if mid else 12.0)
|
||||
var pad := 8.0 if mid else 6.0
|
||||
var shiny := Style.rarity_stats_shiny(rarity)
|
||||
# Oben: Kosten links, daneben der Name; das Namensfeld ist genau so hoch wie der Kristall
|
||||
var top_h := r_cost * 2 + 4
|
||||
draw_rect(Rect2(2, 2, size.x - 4, top_h), Color(0.05, 0.05, 0.08, 0.6))
|
||||
Style.card_name(self, card.title(), f_name, rarity, Rect2(r_cost * 2 + 10, 2, size.x - r_cost * 2 - pad - 10, top_h), seed)
|
||||
# Mitte: das Bild im Kreis, so groß wie der Platz bis zu den Werten erlaubt
|
||||
var pic_top := top_h + 4
|
||||
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)
|
||||
# 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 dim:
|
||||
draw_rect(rect, Color(0, 0, 0, 0.47))
|
||||
if has_focus():
|
||||
Style.border(self, rect, Style.GOLD, 4, 8)
|
||||
if hold > 0.0:
|
||||
var c := size / 2
|
||||
var r := minf(size.x, size.y) * 0.3
|
||||
draw_circle(c, r + 6, Color(0, 0, 0, 0.55))
|
||||
draw_arc(c, r, -PI / 2, -PI / 2 + TAU * hold, 40, Style.GOLD, 5.0, true)
|
||||
|
||||
|
||||
## Volle Karte: Sammlung, Booster, Vorschau.
|
||||
func _draw_full(rect: Rect2, frame: Color, seed: int) -> void:
|
||||
var big := size.x >= 180
|
||||
var mid := size.x >= 140
|
||||
var f_name := 17 if big else (14 if mid else 11)
|
||||
var f_text := 13 if big else (11 if mid else 10)
|
||||
var f_stat := 20 if big else (17 if mid else 14)
|
||||
var r_cost := 17.0 if big else (14.0 if mid else 12.0)
|
||||
var r_stat := 18.0 if big else (15.0 if mid else 12.0)
|
||||
var icon := 11.0 if big else (9.0 if mid else 7.0)
|
||||
# Bild: das Elementsymbol füllt den oberen Teil
|
||||
var band_top: float = size.y * 0.36
|
||||
var gc := Vector2(size.x / 2, band_top * 0.56)
|
||||
var gs := minf(size.x * 0.62, band_top * 0.95)
|
||||
if card.is_combo():
|
||||
Style.element_medallion(self, gc, gs * 0.6, card.elements)
|
||||
Style.glyph(self, card, gc, gs, 0.4, Style.rarity_relief(rarity))
|
||||
Style.rarity_sheen(self, rect, rarity, seed, Rect2(0, 0, size.x, band_top))
|
||||
# Namensband über die volle Breite
|
||||
var band_h: float = Style.fs(f_name) + 10
|
||||
var band := Rect2(0, band_top, size.x, band_h)
|
||||
draw_rect(band, Color(0.05, 0.05, 0.08, 0.82))
|
||||
var nf := Style.fit_size(card.title(), true, f_name, size.x - 12)
|
||||
Style.text(self, Style.fit(card.title(), true, Style.fs(nf), size.x - 12), nf, Style.rarity_name_color(rarity, seed), Vector2(size.x / 2, band.get_center().y), "center", true)
|
||||
var y := band.end.y + 4
|
||||
# Kombo eines verschmolzenen Wesens
|
||||
if card.is_combo():
|
||||
Style.text(self, tr(card.combo()), f_text, Color("e6e6e6"), Vector2(size.x / 2, y), "midtop")
|
||||
y += Style.fs(f_text) + 4
|
||||
# Schlüsselwörter als Symbole, auf großen Karten mit Wort
|
||||
var kws: Array[String] = []
|
||||
for k in Cards.KEYWORDS:
|
||||
if k in card.keywords:
|
||||
kws.append(k)
|
||||
if not kws.is_empty():
|
||||
var with_words := big
|
||||
var widths: Array[float] = []
|
||||
var total := 0.0
|
||||
for k in kws:
|
||||
var w: float = icon * 2 + 6 + (Style.FONT.get_string_size(tr(k), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(f_text)).x + 8 if with_words else 0)
|
||||
widths.append(w)
|
||||
total += w + 6
|
||||
if with_words and total > size.x - 12:
|
||||
with_words = false
|
||||
total = kws.size() * (icon * 2 + 12)
|
||||
var x := (size.x - total + 6) / 2
|
||||
for i in kws.size():
|
||||
var k := kws[i]
|
||||
Style.keyword_icon(self, k, Vector2(x + icon, y + icon + 2), icon)
|
||||
if with_words:
|
||||
Style.text(self, tr(k), f_text, Style.TEXT, Vector2(x + icon * 2 + 6, y + icon + 2), "midleft")
|
||||
x += widths[i] + 6
|
||||
else:
|
||||
x += icon * 2 + 12
|
||||
y += icon * 2 + 8
|
||||
# Effekttext: so hoch wie nötig, endet über den Wertkreisen
|
||||
var box := Rect2()
|
||||
var fx := card.text()
|
||||
var box_bottom: float = size.y - r_stat * 2 - 4
|
||||
if fx != "" and box_bottom - y > Style.fs(f_text) + 6:
|
||||
var tsize := Style.fs(f_text)
|
||||
var needed: float = Style.FONT.get_multiline_string_size(fx, HORIZONTAL_ALIGNMENT_LEFT, size.x - 20, tsize).y + 8
|
||||
var box_top: float = maxf(y, y + (box_bottom - y - needed) / 2)
|
||||
box = Rect2(6, box_top, size.x - 12, box_bottom - box_top)
|
||||
draw_rect(box, Color(0.06, 0.06, 0.09, 0.72))
|
||||
var ty := box.position.y + 4
|
||||
var max_lines := maxi(1, int((box.end.y - ty - 2) / Style.FONT.get_height(tsize)))
|
||||
draw_multiline_string(Style.FONT, Vector2(box.position.x + 4, ty + Style.FONT.get_ascent(tsize)),
|
||||
fx, HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, tsize, max_lines, Style.TEXT)
|
||||
# Werte springen aus den unteren Ecken, dazwischen der Edelstein
|
||||
Style.stat_circle(self, Vector2(r_stat - 2, size.y - r_stat + 2), r_stat, str(card.attack), Style.AMBER, f_stat)
|
||||
Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.LIFE, f_stat)
|
||||
if bonus > 0:
|
||||
Style.text_outlined(self, "+%d" % bonus, f_text, Style.GREEN, Vector2(r_stat * 2 + 4, size.y - r_stat + 2), "midleft", 3)
|
||||
Style.rarity_gem(self, rarity, Vector2(size.x / 2, size.y - r_stat + 1), 8 if big else 6, seed)
|
||||
Style.border(self, rect, frame, 3 if big else 2, 8)
|
||||
Style.cost_crystal(self, Vector2(r_cost + 2, r_cost + 2), r_cost, card.cost, f_stat)
|
||||
Style.rarity_sparkle(self, rect, rarity, seed, box)
|
||||
|
||||
|
||||
## Flache Handkachel: Kosten, Name, Symbole, Werte.
|
||||
func _draw_compact(rect: Rect2, frame: Color, seed: int) -> void:
|
||||
Style.rarity_sheen(self, rect, rarity, seed)
|
||||
Style.border(self, rect, frame, 2, 8)
|
||||
Style.cost_crystal(self, Vector2(13, 13), 11, card.cost, 13)
|
||||
var name_w: float = size.x - 30 - (12 if rarity != "Common" else 0)
|
||||
var nf := Style.fit_size(card.title(), true, 12, name_w)
|
||||
Style.text(self, Style.fit(card.title(), true, Style.fs(nf), name_w), nf, Style.TEXT, Vector2(28, 13), "midleft", true)
|
||||
Style.rarity_gem(self, rarity, Vector2(size.x - 9, 13), 5, seed)
|
||||
var kws: Array[String] = []
|
||||
for k in Cards.KEYWORDS:
|
||||
if k in card.keywords:
|
||||
kws.append(k)
|
||||
var x := size.x / 2 - (kws.size() * 18 - 4) / 2.0
|
||||
for k in kws:
|
||||
Style.keyword_icon(self, k, Vector2(x + 6, size.y - 13), 6)
|
||||
x += 18
|
||||
Style.stat_circle(self, Vector2(11, size.y - 11), 10, str(card.attack), Style.AMBER, 12)
|
||||
Style.stat_circle(self, Vector2(size.x - 11, size.y - 11), 10, str(card.health), Style.LIFE, 12)
|
||||
if bonus > 0:
|
||||
Style.text_outlined(self, "+%d" % bonus, 10, Style.GREEN, Vector2(4, size.y - 29), "midleft", 3)
|
||||
Style.rarity_sparkle(self, rect, rarity, seed)
|
||||
if drop:
|
||||
Style.border(self, rect, Style.GOLD, 5, 8)
|
||||
|
||||
@@ -21,13 +21,14 @@ 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(""))
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
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)
|
||||
search_box = LineEdit.new()
|
||||
search_box.position = Vector2(444, 86)
|
||||
search_box.size = Vector2(220, 34)
|
||||
search_box.position = Vector2(526, 86)
|
||||
search_box.size = Vector2(190, 34)
|
||||
search_box.placeholder_text = "Suche"
|
||||
search_box.text_changed.connect(func(t: String):
|
||||
search = t
|
||||
|
||||
@@ -12,11 +12,11 @@ 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 (+1/-1); die Werte zeigen dann, was ihn erwartet
|
||||
var matchup := 0 # Elementvorteil des gewählten Angreifers gegen dieses fremde Wesen (Game.matchup_bonus); die Werte zeigen dann, 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
|
||||
var hold := 0.0 # Fortschritt des Verschmelzens, wenn eine Karte darüber gehalten wird (0..1)
|
||||
var drop := false # Ziel der gerade gezogenen Karte
|
||||
|
||||
|
||||
func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void:
|
||||
@@ -54,7 +54,7 @@ func _draw() -> void:
|
||||
var rect := Rect2(Vector2.ZERO, size)
|
||||
if secret:
|
||||
_draw_back(rect)
|
||||
_draw_hold()
|
||||
_draw_drop()
|
||||
return
|
||||
var small := size.x < 100
|
||||
var rar := rarity()
|
||||
@@ -62,44 +62,41 @@ func _draw() -> void:
|
||||
if rar != "Common":
|
||||
Style.glow(self, rect, Style.rarity_color(rar, seed), 6, 6)
|
||||
Style.fill_elements(self, rect, creature.card.elements, 6)
|
||||
var gc := Vector2(size.x / 2, size.y * 0.5)
|
||||
if creature.card.is_combo():
|
||||
Style.element_medallion(self, gc, size.x * 0.42, creature.card.elements)
|
||||
Style.glyph(self, creature.card, gc, size.x * 0.75, 0.3, Style.rarity_relief(rar))
|
||||
Style.rarity_sheen(self, rect, rar, seed, rect)
|
||||
var bcol: Color = border if border != null else Style.rarity_frame(rar, seed)
|
||||
if flash:
|
||||
bcol = Style.RED
|
||||
Style.border(self, rect, bcol, 4 if flash else width, 6)
|
||||
var fs := 11 if small else 13
|
||||
var name_w: float = size.x - 6
|
||||
var nf := Style.fit_size(creature.card.title(), true, fs, name_w)
|
||||
draw_rect(Rect2(2, 2, size.x - 4, Style.fs(fs) + 8), Color(0.05, 0.05, 0.08, 0.7))
|
||||
Style.text(self, Style.fit(creature.card.title(), true, Style.fs(nf), name_w), nf, Style.rarity_name_color(rar, seed), Vector2(3 + name_w / 2, 6), "midtop", true)
|
||||
# Elementbonus als ein Abzeichen rechts unter dem Namen; die Symbole rücken dafür nach links
|
||||
var badge := ""
|
||||
var bw := 0.0
|
||||
if creature.aura != 0:
|
||||
badge = "+%d" % creature.aura if creature.aura > 0 else "−%d" % -creature.aura
|
||||
bw = Style.FONT_BOLD.get_string_size(badge, HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(10)).x + 8
|
||||
var by := Style.fs(fs) + 13
|
||||
draw_rect(Rect2(size.x - bw - 4, by, 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, by + 8), "center", true)
|
||||
# Eigenschaften als Symbole in der Farbe des Elements, das sie gibt
|
||||
var icon := 7.0 if small else 9.0
|
||||
var kws := creature.keywords()
|
||||
var avail := size.x - (bw + 8 if badge != "" else 0.0)
|
||||
var x := avail / 2 - (kws.size() * (icon * 2 + 6) - 6) / 2.0
|
||||
var y := Style.fs(fs) + 14 + icon
|
||||
for k in kws:
|
||||
var shiny := Style.rarity_stats_shiny(rar)
|
||||
# Oben: Kosten links, daneben der Name; das Namensfeld ist genau so hoch wie der Kristall
|
||||
var fs := 12 if small else 13
|
||||
var r_cost := 10.0 if small else 11.0
|
||||
var top_h := r_cost * 2 + 4
|
||||
draw_rect(Rect2(2, 2, size.x - 4, top_h), Color(0.05, 0.05, 0.08, 0.6))
|
||||
Style.card_name(self, creature.card.title(), fs, rar, Rect2(r_cost * 2 + 10, 2, size.x - r_cost * 2 - 16, top_h), seed)
|
||||
# Mitte: das Bild im Kreis; unten Angriff links, Leben rechts
|
||||
var r_stat := 12.0 if small else 14.0
|
||||
var pic_top := top_h + 4
|
||||
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
|
||||
var icon := 6.0 if small else 7.0
|
||||
var y := top_h + 6 + icon
|
||||
for k in creature.keywords():
|
||||
var col := Color("f2f2f6")
|
||||
for e in creature.card.elements:
|
||||
if Cards.ELEMENT_KEYWORD.get(e, "") == k:
|
||||
col = Style.lighter(Cards.ELEMENT_COLORS[e], 0.45)
|
||||
Style.keyword_icon(self, k, Vector2(x + icon, y), icon, col)
|
||||
x += icon * 2 + 6
|
||||
_draw_stats(15 if small else 17, 12.0 if small else 14.0)
|
||||
Style.rarity_gem(self, rar, Vector2(size.x / 2, size.y - 9), 5, seed)
|
||||
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)
|
||||
_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)
|
||||
Style.cost_crystal(self, Vector2(r_cost + 4, r_cost + 4), r_cost, creature.card.cost, 13, shiny)
|
||||
if creature.hidden:
|
||||
draw_rect(rect, Color(0.05, 0.05, 0.1, 0.55))
|
||||
Style.text_outlined(self, tr("verdeckt"), 12, Style.TEXT, Vector2(size.x / 2, size.y / 2), "center", 3)
|
||||
@@ -107,17 +104,13 @@ func _draw() -> void:
|
||||
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_hold()
|
||||
_draw_drop()
|
||||
|
||||
|
||||
## Ring, der sich füllt, solange eine Karte zum Verschmelzen darüber gehalten wird.
|
||||
func _draw_hold() -> void:
|
||||
if hold <= 0.0:
|
||||
return
|
||||
var c := size / 2
|
||||
var r := minf(size.x, size.y) * 0.3
|
||||
draw_circle(c, r + 6, Color(0, 0, 0, 0.55))
|
||||
draw_arc(c, r, -PI / 2, -PI / 2 + TAU * hold, 40, Style.GOLD, 5.0, true)
|
||||
## Goldener Rahmen, solange eine Karte zum Verschmelzen darüber schwebt.
|
||||
func _draw_drop() -> void:
|
||||
if drop:
|
||||
Style.border(self, Rect2(Vector2.ZERO, size), Style.GOLD, 5, 6)
|
||||
|
||||
|
||||
## Verdecktes fremdes Wesen: ein Kartenrücken.
|
||||
@@ -143,11 +136,11 @@ func shown_stats() -> Array:
|
||||
|
||||
|
||||
## Angriff gelb, Leben blau; die Zahl wird grün, wenn sie für den aktuellen Angriff steigt, rot, wenn sie sinkt.
|
||||
func _draw_stats(f: int, r: float) -> void:
|
||||
func _draw_stats(f: int, r: float, shiny: bool) -> void:
|
||||
var shown := shown_stats()
|
||||
var atk: int = shown[0]
|
||||
var hp: int = shown[1]
|
||||
var atk_text := Style.GREEN if atk > creature.power() else (Style.RED if atk < creature.power() else Color.WHITE)
|
||||
var hp_text := Style.GREEN if hp > creature.health else (Style.RED if hp < creature.health else Color.WHITE)
|
||||
Style.stat_circle(self, Vector2(r - 1, size.y - r + 1), r, str(atk), Style.AMBER, f, atk_text)
|
||||
Style.stat_circle(self, Vector2(size.x - r + 1, size.y - r + 1), r, str(hp), Style.LIFE, f, hp_text)
|
||||
Style.stat_circle(self, Vector2(r - 1, size.y - r + 1), r, str(atk), Style.AMBER, f, atk_text, shiny)
|
||||
Style.stat_circle(self, Vector2(size.x - r + 1, size.y - r + 1), r, str(hp), Style.LIFE, f, hp_text, shiny)
|
||||
|
||||
@@ -2,20 +2,23 @@ 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 teuersten eigenen Karten des gewählten Filters.
|
||||
## 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).
|
||||
|
||||
var app
|
||||
var filter := ""
|
||||
var grid: CardGrid
|
||||
var count_label: Label
|
||||
var strength_label: Label
|
||||
var msg_label: Label
|
||||
|
||||
|
||||
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(""))
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
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)
|
||||
grid = CardGrid.new(Rect2(40, 132, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 650))
|
||||
@@ -23,11 +26,13 @@ func _ready() -> void:
|
||||
add_child(grid)
|
||||
Style.panel(self, Rect2(820, 86, 420, 696))
|
||||
count_label = Style.chip(self, "", Rect2(860, 100, 340, 36), Style.BLUE)
|
||||
var hint := Style.label(self, "Klick nimmt eine Karte in den Stapel oder heraus. Auffüllen nimmt die teuersten Karten des Filters.", 15, Style.MUTED, Rect2(840, 160, 380, 60), "center")
|
||||
hint.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
Style.button(self, "Auffüllen", Rect2(840, 240, 380, 46), fill_up)
|
||||
Style.button(self, "Leeren", Rect2(840, 296, 380, 46), clear_deck, Color("5a3c28"))
|
||||
msg_label = Style.label(self, "", 15, Style.RED, Rect2(840, 360, 380, 48), "center")
|
||||
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")
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -50,13 +55,13 @@ func toggle(card: Cards.Card) -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
## Füllt mit den teuersten eigenen Karten des gewählten Filters bis zum Höchstwert auf.
|
||||
## 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
|
||||
var pool: Array = p.collection.keys()
|
||||
if filter != "":
|
||||
pool = pool.filter(func(id): return filter in Cards.by_id(id).elements)
|
||||
p.deck = DeckGen.fill(p.deck, pool, Game.DECK_MAX)
|
||||
p.deck = DeckGen.fill(p.deck, pool, Game.DECK_MAX, p.collection)
|
||||
app.save()
|
||||
refresh()
|
||||
|
||||
@@ -67,7 +72,7 @@ func clear_deck() -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
## Eigene Karten, die im Stapel zuerst, sonst nach Kosten.
|
||||
## Eigene Karten, die im Stapel zuerst, dann höchste Stufen vorn, dann nach Kosten.
|
||||
func cards() -> Array:
|
||||
var p: Profile = app.profile
|
||||
var out: Array = p.collection.keys().map(func(id): return Cards.by_id(id))
|
||||
@@ -78,6 +83,8 @@ func cards() -> Array:
|
||||
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)
|
||||
return out
|
||||
|
||||
@@ -86,6 +93,7 @@ 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)
|
||||
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()
|
||||
|
||||
@@ -2,8 +2,8 @@ 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), die Zahl der ausgespielten Karten und ab
|
||||
## Game.ELEMENT_COUNT der Bonus +1/+1.
|
||||
## offenem Wesen, beide: Effekt aktiv; Neutral hat keinen), die Zahl der ausgespielten Karten
|
||||
## und ab Game.ELEMENT_COUNT der Bonus +1/+1.
|
||||
|
||||
const ROW := 32.0
|
||||
|
||||
@@ -15,20 +15,24 @@ func _init(rect: Rect2) -> void:
|
||||
position = rect.position
|
||||
size = rect.size
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
for e in Cards.ELEMENTS:
|
||||
for e in all():
|
||||
own[e] = 0
|
||||
played[e] = 0
|
||||
|
||||
|
||||
static func all() -> Array:
|
||||
return Cards.ELEMENTS + [Cards.NEUTRAL]
|
||||
|
||||
|
||||
func update_from(_g: Game, me: Game.Player) -> void:
|
||||
for e in Cards.ELEMENTS:
|
||||
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()
|
||||
|
||||
|
||||
func paired(e: String) -> bool:
|
||||
return int(own.get(e, 0)) >= Game.ELEMENT_PAIR
|
||||
return Cards.ELEMENT_KEYWORD.has(e) and int(own.get(e, 0)) >= Game.ELEMENT_PAIR
|
||||
|
||||
|
||||
func bonus(e: String) -> int:
|
||||
@@ -37,7 +41,7 @@ func bonus(e: String) -> int:
|
||||
|
||||
## Elemente, bei denen sich etwas getan hat (offene Wesen oder ausgespielte Karten).
|
||||
func shown() -> Array:
|
||||
return Cards.ELEMENTS.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 or int(played.get(e, 0)) > 0)
|
||||
|
||||
|
||||
## Der Inhalt als Text (Tests, Protokoll): Element offen/gespielt, dazu Effekt und Bonus.
|
||||
@@ -62,9 +66,10 @@ func _draw() -> void:
|
||||
var px := 10.0
|
||||
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(px, y), "midleft", active)
|
||||
px = 76
|
||||
for k in Game.ELEMENT_PAIR:
|
||||
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))
|
||||
if Cards.ELEMENT_KEYWORD.has(e):
|
||||
for k in Game.ELEMENT_PAIR:
|
||||
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:
|
||||
|
||||
@@ -91,6 +91,7 @@ func _map_gamepad() -> void:
|
||||
if not InputMap.action_get_events(action).any(func(e): return e is InputEventJoypadButton):
|
||||
var ev := InputEventJoypadButton.new()
|
||||
ev.button_index = pair[1]
|
||||
ev.device = -1 # jedes Gamepad
|
||||
InputMap.action_add_event(action, ev)
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ func _ready() -> void:
|
||||
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(110, 140), false, "Common")
|
||||
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
|
||||
|
||||
@@ -151,7 +151,9 @@ static func header(parent: Node, title: String, on_back: Callable) -> Button:
|
||||
## Kleine Kachel mit Text, etwa für Münzen und Staub.
|
||||
static func chip(parent: Node, text: String, rect: Rect2, color: Color = GOLD) -> Label:
|
||||
panel(parent, rect, darker(PANEL, 0.9), rect.size.y / 2, PANEL_LIGHT, 1)
|
||||
return label(parent, text, 17, color, rect, "center", true)
|
||||
var l := label(parent, text, 17, color, rect, "center", true)
|
||||
l.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
return l
|
||||
|
||||
|
||||
## Vertikal zentrierte Spalte (Container-Layout für Menübildschirme).
|
||||
@@ -309,13 +311,14 @@ static func border(ci: CanvasItem, rect: Rect2, color: Color, width: int, radius
|
||||
|
||||
|
||||
# --- Seltenheit als Aussehen ------------------------------------------------------
|
||||
# Common matt; Rare silberner Name und Rahmen mit stehendem Glanzstreifen; Super Rare Holo-Bild
|
||||
# (wandernder Regenbogenschimmer); Ultra Rare goldener Name und Rahmen plus Holo-Bild;
|
||||
# Ultimate Rare Relief, Holo auf Bild und Rahmen, Funkeln, schillernde Farben.
|
||||
# Common matt; Rare silbern glänzender Name; Super Rare Holo-Bild (wandernder Regenbogen im
|
||||
# Bildkreis); Ultra Rare goldener Name und Holo-Bild; Ultimate Rare goldener Name, goldenes
|
||||
# Bild, glänzende Werte, Schimmer über die ganze Karte, Funkeln und schillernder Rahmen.
|
||||
# Rahmen und Schein tragen ab Rare die Stufenfarbe.
|
||||
|
||||
## Ab Super Rare bewegt sich der Glanz; dann zeichnet die Karte jeden Frame neu.
|
||||
## Ab Rare bewegt sich der Glanz; dann zeichnet die Karte jeden Frame neu.
|
||||
static func rarity_animated(rar: String) -> bool:
|
||||
return animations and Cards.RARITIES.find(rar) >= 2
|
||||
return animations and Cards.RARITIES.find(rar) >= 1
|
||||
|
||||
|
||||
## Farbe einer Stufe; Ultimate Rare schillert über die Zeit durch alle Farben.
|
||||
@@ -330,56 +333,138 @@ static func rarity_frame(rar: String, seed: int = 0) -> Color:
|
||||
return Color("3a3e4c") if rar == "Common" else rarity_color(rar, seed)
|
||||
|
||||
|
||||
## Farbe des Kartennamens: Rare silbern, Ultra Rare golden, Ultimate Rare schillernd, sonst weiß.
|
||||
static func rarity_name_color(rar: String, seed: int = 0) -> Color:
|
||||
## Farbe des Kartennamens: Rare silbern, Ultra und Ultimate Rare golden, sonst weiß.
|
||||
static func rarity_name_color(rar: String) -> Color:
|
||||
match rar:
|
||||
"Rare", "Ultra Rare", "Ultimate Rare":
|
||||
return lighter(rarity_color(rar, seed), 0.25)
|
||||
"Rare":
|
||||
return Cards.RARITY_COLORS["Rare"]
|
||||
"Ultra Rare", "Ultimate Rare":
|
||||
return Cards.RARITY_COLORS["Ultra Rare"]
|
||||
return TEXT
|
||||
|
||||
|
||||
## Der Name glänzt bei Rare (silbern), Ultra und Ultimate Rare (golden).
|
||||
static func rarity_name_shiny(rar: String) -> bool:
|
||||
return rar in ["Rare", "Ultra Rare", "Ultimate Rare"]
|
||||
|
||||
|
||||
## Ultimate Rare: auch Kosten, Angriff und Leben glänzen.
|
||||
static func rarity_stats_shiny(rar: String) -> bool:
|
||||
return rar == "Ultimate Rare"
|
||||
|
||||
|
||||
## Ultimate Rare zeigt das Bild als Relief (Licht- und Schattenkante).
|
||||
static func rarity_relief(rar: String) -> bool:
|
||||
return Cards.RARITIES.find(rar) >= 4
|
||||
return rar == "Ultimate Rare"
|
||||
|
||||
|
||||
## Glanz wie auf einer Folienkarte. Rare: ein stehender weißer Streifen über die Karte.
|
||||
## Ab Super Rare: Holo, ein wandernder Regenbogenschimmer über das Bild (picture); Ultimate Rare
|
||||
## zusätzlich schwach über die ganze Karte.
|
||||
static func rarity_sheen(ci: CanvasItem, rect: Rect2, rar: String, seed: int, picture: Rect2 = Rect2()) -> void:
|
||||
var lvl := Cards.RARITIES.find(rar)
|
||||
if lvl < 1:
|
||||
return
|
||||
if picture.size.x <= 0:
|
||||
picture = rect
|
||||
if lvl == 1:
|
||||
_sheen_bands(ci, rect, (seed % 100) / 100.0, [[1.0, Color(1, 1, 1, 0.07)], [0.55, Color(1, 1, 1, 0.09)], [0.22, Color(1, 1, 1, 0.13)]])
|
||||
## Kartenname links in rect, immer eine Zeile, nie abgeschnitten: die Schrift schrumpft bis
|
||||
## drei Punkte unter die Vorgabe, danach wird der Text schmaler gezeichnet (wie bei langen
|
||||
## Yu-Gi-Oh-Namen).
|
||||
static func card_name(ci: CanvasItem, s: String, size_in: int, rar: String, rect: Rect2, seed: int) -> void:
|
||||
var fsize := fit_size(s, true, size_in, rect.size.x, maxi(8, size_in - 3))
|
||||
var w := FONT_BOLD.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, fs(fsize)).x
|
||||
var squeeze := minf(1.0, rect.size.x / maxf(w, 1.0))
|
||||
var y := rect.position.y + (rect.size.y - FONT_BOLD.get_height(fs(fsize))) / 2
|
||||
ci.draw_set_transform(Vector2(rect.position.x, y), 0.0, Vector2(squeeze, 1.0))
|
||||
if rarity_name_shiny(rar):
|
||||
shiny_text(ci, s, fsize, rarity_name_color(rar), Vector2.ZERO, "topleft", seed)
|
||||
else:
|
||||
text(ci, s, fsize, rarity_name_color(rar), Vector2.ZERO, "topleft", true)
|
||||
ci.draw_set_transform(Vector2.ZERO)
|
||||
|
||||
|
||||
## Metallisch glänzender fetter Text: dunkler Schatten, Grundfarbe, darüber Buchstabe für
|
||||
## Buchstabe ein heller Streifen, der mit Animation über den Text läuft.
|
||||
static func shiny_text(ci: CanvasItem, s: String, size_in: int, color: Color, pos: Vector2, anchor: String, seed: int) -> void:
|
||||
var size := fs(size_in)
|
||||
var dim := FONT_BOLD.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size)
|
||||
var x := pos.x
|
||||
var y := pos.y
|
||||
match anchor:
|
||||
"center":
|
||||
x -= dim.x / 2
|
||||
y -= dim.y / 2
|
||||
"midtop":
|
||||
x -= dim.x / 2
|
||||
"midleft":
|
||||
y -= dim.y / 2
|
||||
var phase := (seed % 100) / 100.0
|
||||
if animations:
|
||||
phase = fmod(Time.get_ticks_msec() / 1800.0 + phase, 1.0)
|
||||
var at := Vector2(x, y + FONT_BOLD.get_ascent(size))
|
||||
ci.draw_string(FONT_BOLD, at + Vector2(1, 1), s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, Color(0, 0, 0, 0.55))
|
||||
var cx := x
|
||||
for i in s.length():
|
||||
var ch := s[i]
|
||||
var w := FONT_BOLD.get_string_size(ch, HORIZONTAL_ALIGNMENT_LEFT, -1, size).x
|
||||
var d := absf(fmod((cx + w / 2 - x) / maxf(dim.x, 1.0) - phase * 1.6 + 0.3 + 2.0, 1.0) - 0.5) * 2.0 # 0 am Streifen
|
||||
var k := clampf(1.0 - d * 2.4, 0.0, 1.0)
|
||||
ci.draw_string(FONT_BOLD, Vector2(cx, at.y), ch, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color.lerp(Color.WHITE, 0.1 + 0.75 * k))
|
||||
cx += w
|
||||
|
||||
|
||||
## Glanz wie auf einer Folienkarte, auf clip beschnitten. lvl 2 und 3: Holo, ein wandernder
|
||||
## Regenbogenschimmer; lvl 4: goldene Bänder.
|
||||
static func sheen(ci: CanvasItem, clip: PackedVector2Array, bounds: Rect2, seed: int, gold: bool = false) -> void:
|
||||
var phase := (seed % 100) / 100.0
|
||||
if animations:
|
||||
phase = fmod(Time.get_ticks_msec() / 2500.0 + phase, 1.0)
|
||||
var layers := []
|
||||
for i in 4:
|
||||
var col := Color(1.0, 0.86, 0.45, 0.2 - i * 0.03) if gold else Color.from_hsv(fmod(phase + i * 0.17, 1.0), 0.6, 1.0, 0.16)
|
||||
layers.append([1.0 - i * 0.22, col])
|
||||
_sheen_bands(ci, clip, bounds, phase, layers)
|
||||
|
||||
|
||||
## Ultimate Rare: schwacher schillernder Schimmer über die ganze Karte.
|
||||
static func card_sheen(ci: CanvasItem, rect: Rect2, rar: String, seed: int) -> void:
|
||||
if rar != "Ultimate Rare":
|
||||
return
|
||||
var phase := (seed % 100) / 100.0
|
||||
if animations:
|
||||
phase = fmod(Time.get_ticks_msec() / 2500.0 + phase, 1.0)
|
||||
var holo := []
|
||||
for i in 4:
|
||||
holo.append([1.0 - i * 0.22, Color.from_hsv(fmod(phase + i * 0.17, 1.0), 0.6, 1.0, 0.16)])
|
||||
_sheen_bands(ci, picture, phase, holo)
|
||||
if lvl >= 4:
|
||||
var soft := []
|
||||
for i in 3:
|
||||
soft.append([1.0 - i * 0.3, Color.from_hsv(fmod(phase + 0.5 + i * 0.2, 1.0), 0.5, 1.0, 0.07)])
|
||||
_sheen_bands(ci, rect, fmod(phase + 0.5, 1.0), soft)
|
||||
var soft := []
|
||||
for i in 3:
|
||||
soft.append([1.0 - i * 0.3, Color.from_hsv(fmod(phase + 0.5 + i * 0.2, 1.0), 0.5, 1.0, 0.07)])
|
||||
_sheen_bands(ci, rect_poly(rect), rect, fmod(phase + 0.5, 1.0), soft)
|
||||
|
||||
|
||||
## Schräge Streifen über rect, auf rect beschnitten. layers: [Breitenanteil, Farbe].
|
||||
static func _sheen_bands(ci: CanvasItem, rect: Rect2, phase: float, layers: Array) -> void:
|
||||
var frame := PackedVector2Array([rect.position, Vector2(rect.end.x, rect.position.y), rect.end, Vector2(rect.position.x, rect.end.y)])
|
||||
var w := rect.size.x * 0.34
|
||||
var k := rect.size.y * 0.75 # Schräge
|
||||
var x0 := rect.position.x - w + (rect.size.x + w + k) * phase
|
||||
static func rect_poly(rect: Rect2) -> PackedVector2Array:
|
||||
return PackedVector2Array([rect.position, Vector2(rect.end.x, rect.position.y), rect.end, Vector2(rect.position.x, rect.end.y)])
|
||||
|
||||
|
||||
static func circle_poly(c: Vector2, r: float, steps: int = 40) -> PackedVector2Array:
|
||||
var pts := PackedVector2Array()
|
||||
for i in steps:
|
||||
var a := TAU * i / steps
|
||||
pts.append(c + Vector2(cos(a), sin(a)) * r)
|
||||
return pts
|
||||
|
||||
|
||||
## Rechteck mit runden Ecken als Polygon (für Beschnitt).
|
||||
static func rounded_poly(rect: Rect2, radius: float) -> PackedVector2Array:
|
||||
var pts := PackedVector2Array()
|
||||
var r := minf(radius, minf(rect.size.x, rect.size.y) / 2)
|
||||
var corners := [[rect.end - Vector2(r, r), 0.0], [Vector2(rect.position.x + r, rect.end.y - r), PI / 2],
|
||||
[rect.position + Vector2(r, r), PI], [Vector2(rect.end.x - r, rect.position.y + r), PI * 1.5]]
|
||||
for corner in corners:
|
||||
for k in 7:
|
||||
var a: float = corner[1] + PI / 2 * k / 6
|
||||
pts.append(corner[0] + Vector2(cos(a), sin(a)) * r)
|
||||
return pts
|
||||
|
||||
|
||||
## Schräge Streifen über bounds, auf clip beschnitten. layers: [Breitenanteil, Farbe].
|
||||
static func _sheen_bands(ci: CanvasItem, clip: PackedVector2Array, bounds: Rect2, phase: float, layers: Array) -> void:
|
||||
var w := bounds.size.x * 0.34
|
||||
var k := bounds.size.y * 0.75 # Schräge
|
||||
var x0 := bounds.position.x - w + (bounds.size.x + w + k) * phase
|
||||
for layer in layers:
|
||||
var lw: float = w * layer[0]
|
||||
var lx := x0 + (w - lw) / 2
|
||||
var band := PackedVector2Array([Vector2(lx, rect.position.y), Vector2(lx + lw, rect.position.y), Vector2(lx + lw - k, rect.end.y), Vector2(lx - k, rect.end.y)])
|
||||
for poly in Geometry2D.intersect_polygons(band, frame):
|
||||
var band := PackedVector2Array([Vector2(lx, bounds.position.y), Vector2(lx + lw, bounds.position.y), Vector2(lx + lw - k, bounds.end.y), Vector2(lx - k, bounds.end.y)])
|
||||
for poly in Geometry2D.intersect_polygons(band, clip):
|
||||
if poly.size() >= 3 and _area(poly) > 2.0: # Splitter am Rand lassen sich nicht zeichnen
|
||||
ci.draw_colored_polygon(poly, layer[1])
|
||||
|
||||
@@ -394,16 +479,15 @@ static func _area(poly: PackedVector2Array) -> float:
|
||||
|
||||
|
||||
## Funkeln bei Ultimate Rare: kleine Sterne, die aufblitzen und vergehen; ganz zum Schluss zeichnen.
|
||||
## avoid: Bereich, der frei bleibt (der Kartentext).
|
||||
static func rarity_sparkle(ci: CanvasItem, rect: Rect2, rar: String, seed: int, avoid: Rect2 = Rect2()) -> void:
|
||||
if Cards.RARITIES.find(rar) < 4:
|
||||
static func rarity_sparkle(ci: CanvasItem, rect: Rect2, rar: String, seed: int) -> void:
|
||||
if rar != "Ultimate Rare":
|
||||
return
|
||||
var t := Time.get_ticks_msec() / 1000.0
|
||||
for i in 9:
|
||||
var h := absi(hash(Vector2i(seed, i)))
|
||||
var p := rect.position + Vector2(8 + (h % 1000) / 1000.0 * (rect.size.x - 16), 8 + ((h / 1000) % 1000) / 1000.0 * (rect.size.y - 16))
|
||||
var tw := 0.5 + 0.5 * sin(t * 2.5 + i * 1.9 + seed % 7) if animations else 0.35 + 0.65 * ((i * 7 + seed) % 5) / 4.0
|
||||
if tw < 0.15 or avoid.has_point(p):
|
||||
if tw < 0.15:
|
||||
continue
|
||||
var s := 2.0 + 4.0 * tw
|
||||
var col := Color(1, 1, 1, 0.3 + 0.65 * tw)
|
||||
@@ -422,65 +506,70 @@ static func glow(ci: CanvasItem, rect: Rect2, color: Color, radius: int = 8, spr
|
||||
ci.draw_style_box(sb, rect)
|
||||
|
||||
|
||||
## Edelstein in der Farbe der Stufe (ab Rare).
|
||||
static func rarity_gem(ci: CanvasItem, rar: String, c: Vector2, s: float, seed: int = 0) -> void:
|
||||
if rar == "Common":
|
||||
return
|
||||
var col := rarity_color(rar, seed)
|
||||
var diamond := func(h: float, k: Color) -> void:
|
||||
ci.draw_colored_polygon(PackedVector2Array([c + Vector2(0, -h), c + Vector2(h * 0.72, 0), c + Vector2(0, h), c + Vector2(-h * 0.72, 0)]), k)
|
||||
diamond.call(s + 2, Color("14141c"))
|
||||
diamond.call(s, col)
|
||||
ci.draw_colored_polygon(PackedVector2Array([c + Vector2(0, -s * 0.7), c + Vector2(s * 0.3, -s * 0.15), c + Vector2(-s * 0.35, -s * 0.1)]), Color(1, 1, 1, 0.55))
|
||||
|
||||
|
||||
## Hintergrund in der Farbe des ersten Elements; weitere Elemente zeigt element_medallion.
|
||||
## Hintergrund in den Farben der Elemente: ein Element füllt die Karte, verschmolzene Wesen
|
||||
## tragen je Element einen senkrechten Streifen.
|
||||
static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius: int = 8) -> void:
|
||||
ci.draw_style_box(box(Cards.ELEMENT_COLORS[elements[0]], radius), rect)
|
||||
|
||||
|
||||
## Medaillon eines verschmolzenen Wesens: ein Kreis hinter dem Bild, als Tortenstücke in den
|
||||
## Farben seiner Elemente, mit dunklem Rand.
|
||||
static func element_medallion(ci: CanvasItem, c: Vector2, r: float, elements: Array) -> void:
|
||||
ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.75))
|
||||
var n := elements.size()
|
||||
for i in n:
|
||||
var from := -PI / 2 + TAU * i / n
|
||||
var to := -PI / 2 + TAU * (i + 1) / n
|
||||
var pts := PackedVector2Array([c])
|
||||
var steps := 24
|
||||
for k in steps + 1:
|
||||
var ang := lerpf(from, to, float(k) / steps)
|
||||
pts.append(c + Vector2(cos(ang), sin(ang)) * r)
|
||||
ci.draw_colored_polygon(pts, Cards.ELEMENT_COLORS[elements[i]])
|
||||
for i in n:
|
||||
var ang := -PI / 2 + TAU * i / n
|
||||
ci.draw_line(c, c + Vector2(cos(ang), sin(ang)) * r, Color(0.05, 0.05, 0.08, 0.75), 2.0)
|
||||
if elements.size() < 2:
|
||||
return
|
||||
var clip := rounded_poly(rect, radius)
|
||||
var w := rect.size.x / elements.size()
|
||||
for i in range(1, elements.size()):
|
||||
var strip := Rect2(rect.position.x + w * i, rect.position.y, w, rect.size.y)
|
||||
for poly in Geometry2D.intersect_polygons(rect_poly(strip), clip):
|
||||
if poly.size() >= 3:
|
||||
ci.draw_colored_polygon(poly, Cards.ELEMENT_COLORS[elements[i]])
|
||||
ci.draw_line(strip.position + Vector2(0, 2), Vector2(strip.position.x, strip.end.y - 2), Color(0.05, 0.05, 0.08, 0.5), 1.5)
|
||||
|
||||
|
||||
# --- Kartenteile ------------------------------------------------------
|
||||
|
||||
## Kostenkristall: sechseckig, blau, Zahl in der Mitte.
|
||||
static func cost_crystal(ci: CanvasItem, c: Vector2, r: float, cost: int, font: int) -> void:
|
||||
## Bild der Karte: dunkler Kreis mit Ring in der Elementfarbe, darin das Symbol der Bildkarte
|
||||
## (bei Verschmelzungen die des stärkeren Elternteils). Ab Super Rare glänzt der Kreis (Holo),
|
||||
## bei Ultimate Rare golden und als Relief.
|
||||
static func picture(ci: CanvasItem, card, c: Vector2, r: float, rar: String, seed: int) -> void:
|
||||
var src = card.art_card()
|
||||
var ring := lighter(Cards.ELEMENT_COLORS[src.elements[0]], 0.3)
|
||||
ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.8))
|
||||
ci.draw_circle(c, r, Color(0.09, 0.09, 0.13, 0.9))
|
||||
glyph(ci, src, c, r * 1.3, 0.95, rarity_relief(rar))
|
||||
var lvl := Cards.RARITIES.find(rar)
|
||||
if lvl >= 2:
|
||||
sheen(ci, circle_poly(c, r), Rect2(c - Vector2(r, r), Vector2(r * 2, r * 2)), seed, lvl >= 4)
|
||||
ci.draw_arc(c, r + 1.5, 0, TAU, 48, Color(1.0, 0.86, 0.45) if lvl >= 4 else ring, 3.0, true)
|
||||
|
||||
|
||||
## Kostenkristall: sechseckig, blau, Zahl in der Mitte; shiny: goldene, glänzende Zahl.
|
||||
static func cost_crystal(ci: CanvasItem, c: Vector2, r: float, cost: int, font: int, shiny: bool = false) -> void:
|
||||
var pts := PackedVector2Array()
|
||||
var outer := PackedVector2Array()
|
||||
for i in 6:
|
||||
var a := -PI / 2 + i * PI / 3
|
||||
pts.append(c + Vector2(cos(a), sin(a)) * r)
|
||||
outer.append(c + Vector2(cos(a), sin(a)) * (r + 2))
|
||||
ci.draw_colored_polygon(outer, Color("14141c"))
|
||||
ci.draw_colored_polygon(outer, Color(1.0, 0.86, 0.45) if shiny else Color("14141c"))
|
||||
ci.draw_colored_polygon(pts, MANA)
|
||||
var shine := PackedVector2Array([pts[5], pts[0], c + Vector2(0, -r * 0.1), c + Vector2(-r * 0.55, r * 0.15)])
|
||||
ci.draw_colored_polygon(shine, Color(1, 1, 1, 0.22))
|
||||
text_outlined(ci, str(cost), font, Color.WHITE, c, "center", 3)
|
||||
_stat_text(ci, str(cost), font, Color.WHITE, c, shiny)
|
||||
|
||||
|
||||
## Wertkreis (Angriff gelb, Leben rot) mit Zahl.
|
||||
static func stat_circle(ci: CanvasItem, c: Vector2, r: float, value: String, color: Color, font: int, text_col: Color = Color.WHITE) -> void:
|
||||
ci.draw_circle(c, r + 2, Color("14141c"))
|
||||
## Wertkreis (Angriff gelb, Leben blau) mit Zahl; shiny: goldener Ring, glänzende Zahl.
|
||||
static func stat_circle(ci: CanvasItem, c: Vector2, r: float, value: String, color: Color, font: int, text_col: Color = Color.WHITE, shiny: bool = false) -> void:
|
||||
ci.draw_circle(c, r + 2, Color(1.0, 0.86, 0.45) if shiny else Color("14141c"))
|
||||
ci.draw_circle(c, r, color)
|
||||
ci.draw_circle(c + Vector2(-r * 0.25, -r * 0.3), r * 0.45, Color(1, 1, 1, 0.16))
|
||||
text_outlined(ci, value, font, text_col, c, "center", 3)
|
||||
_stat_text(ci, value, font, text_col, c, shiny)
|
||||
|
||||
|
||||
static func _stat_text(ci: CanvasItem, s: String, font: int, col: Color, c: Vector2, shiny: bool) -> void:
|
||||
if shiny and col == Color.WHITE:
|
||||
var size := fs(font)
|
||||
var dim := FONT_BOLD.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size)
|
||||
ci.draw_string_outline(FONT_BOLD, c - dim / 2 + Vector2(0, FONT_BOLD.get_ascent(size)), s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, 3, Color(0.05, 0.05, 0.08, 0.95))
|
||||
shiny_text(ci, s, font, Color(1.0, 0.86, 0.45), c, "center", int(c.x + c.y))
|
||||
else:
|
||||
text_outlined(ci, s, font, col, c, "center", 3)
|
||||
|
||||
|
||||
## Eigenschaft als Symbol auf dunklem Kreis: Spott Ausrufezeichen, Flink Doppelpfeil,
|
||||
@@ -568,6 +657,11 @@ static func _element_glyph(ci: CanvasItem, element: String, c: Vector2, size: fl
|
||||
for i in 2 + variant:
|
||||
var r := h * (1.0 - i * 0.28)
|
||||
ci.draw_arc(c + Vector2(i * size * 0.06, 0), r, PI * 0.2 + i * 0.4, PI * 1.5 + i * 0.4, 20, col, maxf(2, size * 0.09))
|
||||
_: # Neutral: ein schlichter Schild
|
||||
var w := h * 0.8
|
||||
var pts := PackedVector2Array([c + Vector2(-w, -h * 0.8), c + Vector2(w, -h * 0.8), c + Vector2(w, h * 0.1), c + Vector2(0, h), c + Vector2(-w, h * 0.1)])
|
||||
ci.draw_polyline(pts + PackedVector2Array([pts[0]]), col, maxf(2, size * 0.09))
|
||||
ci.draw_line(c + Vector2(0, -h * 0.5), c + Vector2(0, h * 0.6), col, maxf(2, size * 0.07))
|
||||
|
||||
|
||||
static func _flame(ci: CanvasItem, c: Vector2, h: float, col: Color) -> void:
|
||||
|
||||
Reference in New Issue
Block a user