Files
cards/scripts/ui/style.gd
T
team3 1d563b9eec
Tests / headless (push) Waiting to run
Tests / fenster-schnell (push) Waiting to run
Tests / fenster-voll (push) Skipped
Tests / balance (push) Skipped
update
2026-10-14 17:35:09 +02:00

700 lines
30 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name Style
## Farben, Schriften, Theme und Zeichenhilfen.
const FONT: FontFile = preload("res://assets/fonts/DejaVuSans.ttf")
const FONT_BOLD: FontFile = preload("res://assets/fonts/DejaVuSans-Bold.ttf")
static var font_scale := 1.0 # Einstellung „Große Schrift“
static var animations := true # Einstellung „Animationen“: der Glanz seltener Karten bewegt sich
const BG := Color("1a1c24")
const PANEL := Color("282b36")
const PANEL_LIGHT := Color("3a3e4c")
const TEXT := Color("ececf0")
const MUTED := Color("9698a5")
const GOLD := Color("f5c850")
const GREEN := Color("5ad282")
const RED := Color("e65555")
const BLUE := Color("6eaaf0")
const YELLOW := Color("ffe65a")
const ORANGE := Color("ff963c")
const PURPLE := Color("8c78dc")
const AMBER := Color("f0b429") # Angriff
const LIFE := Color("4a86d8") # Leben
const MANA := Color("3a78d2") # Kostenkristall
const PRIMARY := Color("285a3c") # Hauptaktion
const DANGER := Color("5a2828")
## Schriftgröße mit der Einstellung „Große Schrift“.
## Faktor als Text: ×2, ×1,5 (Dezimaltrenner der Sprache).
static func factor(f: float) -> String:
return "×" + String.num(f, 1).trim_suffix(".0").replace(".", TranslationServer.translate(","))
static func fs(size: int) -> int:
return int(round(size * font_scale))
static func darker(c: Color, f: float = 0.65) -> Color:
return Color(c.r * f, c.g * f, c.b * f, c.a)
static func lighter(c: Color, d: float = 0.1) -> Color:
return Color(minf(1, c.r + d), minf(1, c.g + d), minf(1, c.b + d), c.a)
static func box(bg: Color, radius: int = 8, border_col = null, border_w: int = 0) -> StyleBoxFlat:
var sb := StyleBoxFlat.new()
sb.bg_color = bg
sb.set_corner_radius_all(radius)
if border_col != null:
sb.border_color = border_col
sb.set_border_width_all(border_w)
return sb
static func make_theme() -> Theme:
var t := Theme.new()
t.default_font = FONT
t.default_font_size = fs(16)
t.set_stylebox("normal", "Button", box(PANEL_LIGHT, 8, MUTED, 2))
t.set_stylebox("hover", "Button", box(lighter(PANEL_LIGHT), 8, MUTED, 2))
t.set_stylebox("pressed", "Button", box(darker(PANEL_LIGHT), 8, MUTED, 2))
t.set_stylebox("disabled", "Button", box(darker(PANEL, 0.9), 8, PANEL, 2))
t.set_stylebox("focus", "Button", box(Color(0, 0, 0, 0), 8, GOLD, 3))
for name in ["font_color", "font_hover_color", "font_pressed_color", "font_focus_color", "font_hover_pressed_color"]:
t.set_color(name, "Button", TEXT)
t.set_color("font_disabled_color", "Button", MUTED)
t.set_font_size("font_size", "Button", fs(18))
t.set_color("font_color", "Label", TEXT)
t.set_stylebox("panel", "Panel", box(PANEL, 8))
t.set_stylebox("normal", "RichTextLabel", StyleBoxEmpty.new())
t.set_stylebox("focus", "RichTextLabel", StyleBoxEmpty.new())
t.set_stylebox("panel", "ScrollContainer", StyleBoxEmpty.new())
t.set_stylebox("normal", "LineEdit", box(PANEL_LIGHT, 6, MUTED, 1))
t.set_stylebox("focus", "LineEdit", box(PANEL_LIGHT, 6, BLUE, 2))
t.set_color("font_color", "LineEdit", TEXT)
t.set_color("font_placeholder_color", "LineEdit", MUTED)
t.set_stylebox("normal", "OptionButton", box(PANEL_LIGHT, 6, MUTED, 1))
t.set_stylebox("hover", "OptionButton", box(lighter(PANEL_LIGHT), 6, MUTED, 1))
t.set_stylebox("pressed", "OptionButton", box(darker(PANEL_LIGHT), 6, MUTED, 1))
t.set_stylebox("focus", "OptionButton", StyleBoxEmpty.new())
t.set_color("font_color", "OptionButton", TEXT)
t.set_color("font_color", "CheckButton", TEXT)
t.set_stylebox("focus", "CheckButton", StyleBoxEmpty.new())
return t
# --- Knoten-Hilfen -------------------------------------------------------------
static func panel(parent: Node, rect: Rect2, bg: Color = PANEL, radius: int = 8, border_col = null, border_w: int = 0) -> Panel:
var p := Panel.new()
p.position = rect.position
p.size = rect.size
var sb := box(bg, radius, border_col, border_w)
sb.shadow_color = Color(0, 0, 0, 0.35)
sb.shadow_size = 6
sb.shadow_offset = Vector2(0, 3)
p.add_theme_stylebox_override("panel", sb)
p.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(p)
return p
static func label(parent: Node, text: String, size: int, color: Color, rect: Rect2, align: String = "left", bold: bool = false) -> Label:
var l := Label.new()
l.text = text
l.position = rect.position
l.size = rect.size
l.add_theme_font_size_override("font_size", fs(size))
l.add_theme_color_override("font_color", color)
if bold:
l.add_theme_font_override("font", FONT_BOLD)
match align:
"center":
l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
"right":
l.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
l.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(l)
return l
static func button(parent: Node, text: String, rect: Rect2, on_pressed: Callable, color = null) -> Button:
var b := Button.new()
b.text = text
b.position = rect.position
b.size = rect.size
b.pressed.connect(on_pressed)
if color != null:
b.add_theme_stylebox_override("normal", box(color, 8, MUTED, 2))
b.add_theme_stylebox_override("hover", box(lighter(color), 8, MUTED, 2))
b.add_theme_stylebox_override("pressed", box(darker(color), 8, MUTED, 2))
juice(b)
parent.add_child(b)
return b
## Knöpfe federn beim Drücken kurz ein.
static func juice(b: Button) -> void:
b.button_down.connect(func():
if animations:
b.pivot_offset = b.size / 2
b.create_tween().tween_property(b, "scale", Vector2(0.96, 0.96), 0.05))
b.button_up.connect(func():
if animations:
b.pivot_offset = b.size / 2
b.create_tween().tween_property(b, "scale", Vector2.ONE, 0.12).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT))
## Kopfzeile eines Unterbildschirms: Titel links, Zurück rechts oben.
static func header(parent: Node, title: String, on_back: Callable) -> Button:
label(parent, title, 34, GOLD, Rect2(40, 22, 700, 46), "left", true)
return button(parent, "Zurück", Rect2(1120, 26, 120, 40), on_back)
## 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)
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).
static func column(parent: Node, width: float, top: float, sep: int = 12) -> VBoxContainer:
var v := VBoxContainer.new()
v.anchor_left = 0.5
v.anchor_right = 0.5
v.anchor_top = 0
v.anchor_bottom = 0
v.offset_left = -width / 2
v.offset_right = width / 2
v.offset_top = top
v.add_theme_constant_override("separation", sep)
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(v)
return v
static func row(parent: Node, sep: int = 12) -> HBoxContainer:
var h := HBoxContainer.new()
h.add_theme_constant_override("separation", sep)
h.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(h)
return h
## Label in einem Container.
static func clabel(parent: Node, text: String, size: int, color: Color, align: String = "center", bold: bool = false, min_w: float = 0) -> Label:
var l := label(parent, text, size, color, Rect2(0, 0, 0, 0), align, bold)
l.size_flags_horizontal = Control.SIZE_EXPAND_FILL if min_w == 0 else Control.SIZE_FILL
if min_w > 0:
l.custom_minimum_size = Vector2(min_w, 0)
return l
## Knopf in einem Container.
static func cbutton(parent: Node, text: String, size: Vector2, on_pressed: Callable, color = null) -> Button:
var b := button(parent, text, Rect2(Vector2.ZERO, size), on_pressed, color)
b.custom_minimum_size = size
return b
static func spacer(parent: Node, h: float) -> Control:
var c := Control.new()
c.custom_minimum_size = Vector2(0, h)
c.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(c)
return c
## Unsichtbare Vollbild-Ebene mit abgedunkeltem Hintergrund, die Klicks schluckt.
static func overlay(parent: Node) -> Control:
var o := Control.new()
o.set_anchors_preset(Control.PRESET_FULL_RECT)
o.visible = false
var dark := ColorRect.new()
dark.color = Color(0, 0, 0, 0.67)
dark.set_anchors_preset(Control.PRESET_FULL_RECT)
dark.mouse_filter = Control.MOUSE_FILTER_STOP
o.add_child(dark)
parent.add_child(o)
return o
## Bestätigungsdialog (unsichtbar): Titel, Hinweis, Ja-Knopf mit Aktion und ein Abbrechen-Knopf.
static func confirm(parent: Node, title: String, text: String, yes: String, on_yes: Callable, no: String = "Abbrechen", color: Color = MUTED) -> Control:
var o := overlay(parent)
panel(o, Rect2(340, 290, 600, 200), PANEL, 10, color, 2)
label(o, title, 26, TEXT, Rect2(340, 312, 600, 40), "center", true)
label(o, text, 16, MUTED, Rect2(340, 360, 600, 24), "center")
button(o, yes, Rect2(380, 420, 240, 46), on_yes, Color("5a2828"))
button(o, no, Rect2(660, 420, 240, 46), func(): o.visible = false)
return o
static func clear(node: Node) -> void:
for c in node.get_children():
node.remove_child(c)
c.queue_free()
# --- Zeichenhilfen für _draw() ---------------------------------------------------
static func text(ci: CanvasItem, s: String, size_in: int, color: Color, pos: Vector2, anchor: String = "topleft", bold: bool = false) -> void:
var size := fs(size_in)
var font: Font = FONT_BOLD if bold else FONT
var dim := font.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
"topright":
x -= dim.x
"midleft":
y -= dim.y / 2
"midright":
x -= dim.x
y -= dim.y / 2
"bottomleft":
y -= dim.y
"bottomright":
x -= dim.x
y -= dim.y
"midbottom":
x -= dim.x / 2
y -= dim.y
ci.draw_string(font, Vector2(x, y + font.get_ascent(size)), s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color)
## Text mit dunklem Rand: bleibt auf jeder Farbe lesbar (Werte, Kosten).
static func text_outlined(ci: CanvasItem, s: String, size_in: int, color: Color, pos: Vector2, anchor: String = "center", outline: int = 4) -> 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 at := Vector2(x, y + FONT_BOLD.get_ascent(size))
ci.draw_string_outline(FONT_BOLD, at, s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, outline, Color(0.05, 0.05, 0.08, 0.95))
ci.draw_string(FONT_BOLD, at, s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color)
## Schriftgröße, mit der ein Text in eine Breite passt (bis zur Mindestgröße).
static func fit_size(s: String, bold: bool, size: int, max_w: float, min_size: int = 9) -> int:
var font: Font = FONT_BOLD if bold else FONT
while size > min_size and font.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, fs(size)).x > max_w:
size -= 1
return size
static func fit(s: String, bold: bool, size: int, max_w: float) -> String:
var font: Font = FONT_BOLD if bold else FONT
while font.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size).x > max_w and s.length() > 3:
s = s.substr(0, s.length() - 2) + "…"
return s
static func border(ci: CanvasItem, rect: Rect2, color: Color, width: int, radius: int) -> void:
var sb := StyleBoxFlat.new()
sb.draw_center = false
sb.border_color = color
sb.set_border_width_all(width)
sb.set_corner_radius_all(radius)
ci.draw_style_box(sb, rect)
# --- Seltenheit als Aussehen ------------------------------------------------------
# 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 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) >= 1
## Farbe einer Stufe; Ultimate Rare schillert über die Zeit durch alle Farben.
static func rarity_color(rar: String, seed: int = 0) -> Color:
if rar == "Ultimate Rare" and animations:
return Color.from_hsv(fmod(Time.get_ticks_msec() / 1000.0 * 0.12 + (seed % 97) / 97.0, 1.0), 0.55, 1.0)
return Cards.RARITY_COLORS[rar]
## Rahmenfarbe einer Stufe: Common bleibt schlicht, alles darüber trägt seine Farbe.
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 und Ultimate Rare golden, sonst weiß.
static func rarity_name_color(rar: String) -> Color:
match rar:
"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 rar == "Ultimate Rare"
## 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 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)
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, 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])
static func _area(poly: PackedVector2Array) -> float:
var a := 0.0
for i in poly.size():
var p := poly[i]
var q := poly[(i + 1) % poly.size()]
a += p.x * q.y - q.x * p.y
return absf(a) / 2.0
## Funkeln bei Ultimate Rare: kleine Sterne, die aufblitzen und vergehen; ganz zum Schluss zeichnen.
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:
continue
var s := 2.0 + 4.0 * tw
var col := Color(1, 1, 1, 0.3 + 0.65 * tw)
ci.draw_line(p - Vector2(s, 0), p + Vector2(s, 0), col, 1.4)
ci.draw_line(p - Vector2(0, s), p + Vector2(0, s), col, 1.4)
ci.draw_circle(p, 1.3, col)
## Weicher Schein um eine Karte höherer Stufe; vor dem Hintergrund zeichnen.
static func glow(ci: CanvasItem, rect: Rect2, color: Color, radius: int = 8, spread: int = 8) -> void:
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0, 0, 0, 0)
sb.set_corner_radius_all(radius)
sb.shadow_color = Color(color, 0.6)
sb.shadow_size = spread
ci.draw_style_box(sb, rect)
## 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)
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 ------------------------------------------------------
## 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(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))
_stat_text(ci, str(cost), font, Color.WHITE, c, shiny)
## 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))
_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,
## Regeneration Kreuz, Doppelschlag gekreuzte Klingen.
static func keyword_icon(ci: CanvasItem, kw: String, c: Vector2, r: float, col: Color = Color("f2f2f6")) -> void:
ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.7))
var w := maxf(1.5, r * 0.22)
match kw:
"Spott":
ci.draw_line(c + Vector2(0, -r * 0.7), c + Vector2(0, r * 0.2), col, w * 1.6)
ci.draw_circle(c + Vector2(0, r * 0.6), w * 0.95, col)
"Regeneration":
ci.draw_line(c + Vector2(0, -r * 0.7), c + Vector2(0, r * 0.7), col, w * 1.5)
ci.draw_line(c + Vector2(-r * 0.7, 0), c + Vector2(r * 0.7, 0), col, w * 1.5)
"Flink":
for i in 2:
var x := c.x - r * 0.55 + i * r * 0.55
ci.draw_polyline(PackedVector2Array([Vector2(x, c.y - r * 0.6), Vector2(x + r * 0.5, c.y), Vector2(x, c.y + r * 0.6)]), col, w)
"Doppelschlag":
ci.draw_line(c + Vector2(-r * 0.65, r * 0.65), c + Vector2(r * 0.6, -r * 0.6), col, w)
ci.draw_line(c + Vector2(r * 0.65, r * 0.65), c + Vector2(-r * 0.6, -r * 0.6), col, w)
ci.draw_line(c + Vector2(-r * 0.7, r * 0.35), c + Vector2(-r * 0.35, r * 0.7), col, w)
ci.draw_line(c + Vector2(r * 0.7, r * 0.35), c + Vector2(r * 0.35, r * 0.7), col, w)
# --- Kartensymbole (prozedural, je Element) --------------------------
## relief: Schatten- und Lichtkante unter dem Symbol (Ultimate Rare).
static func glyph(ci: CanvasItem, card, center: Vector2, size: float, alpha: float = 0.95, relief: bool = false) -> void:
if relief:
ci.draw_set_transform(Vector2(2, 2))
_glyph_plain(ci, card, center, size, Color(0, 0, 0, alpha * 0.6))
ci.draw_set_transform(Vector2(-1, -1))
_glyph_plain(ci, card, center, size, Color(1, 1, 1, alpha * 0.5))
ci.draw_set_transform(Vector2.ZERO)
_glyph_plain(ci, card, center, size, Color(0, 0, 0, 0), alpha)
## Symbol in einer festen Farbe (Relief) oder, bei durchsichtigem tint, in Elementfarben.
static func _glyph_plain(ci: CanvasItem, card, center: Vector2, size: float, tint: Color, alpha: float = 0.95) -> void:
var variant := absi(hash(card.id)) % 3
var col := lighter(Cards.ELEMENT_COLORS[card.elements[0]], 0.38)
col.a = alpha
if tint.a > 0:
col = tint
var elems: Array = card.elements
if elems.size() == 1:
_element_glyph(ci, elems[0], center, size, variant, col)
return
# Verschmolzen: die ersten beiden Elemente nebeneinander, ab dem dritten ein Ring
_element_glyph(ci, elems[0], center + Vector2(-size * 0.28, 0), size * 0.62, variant, col)
var col2 := lighter(Cards.ELEMENT_COLORS[elems[1]], 0.38)
col2.a = alpha
if tint.a > 0:
col2 = tint
_element_glyph(ci, elems[1], center + Vector2(size * 0.28, 0), size * 0.62, variant, col2)
if elems.size() >= 3:
ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2)
## Elementsymbol in einer Farbe (Filterknöpfe, Listen), Grundvariante.
static func element_icon(ci: CanvasItem, element: String, c: Vector2, size: float, col: Color) -> void:
_element_glyph(ci, element, c, size, 0, col)
static func _element_glyph(ci: CanvasItem, element: String, c: Vector2, size: float, variant: int, col: Color) -> void:
var h := size / 2
match element:
"Feuer":
var n := 1 + variant
for i in n:
var off := Vector2((i - (n - 1) / 2.0) * size * 0.32, 0)
var s := 1.0 if i == n / 2 else 0.7
_flame(ci, c + off, h * s, col)
"Wasser":
for i in 2 + variant % 2:
var y := c.y + (i - 0.5) * size * 0.3
for k in 3:
var x := c.x - size * 0.4 + k * size * 0.4
ci.draw_arc(Vector2(x, y), size * 0.2, PI, TAU, 12, col, maxf(2, size * 0.09))
"Erde":
var n := 1 + variant
for i in n:
var off := (i - (n - 1) / 2.0) * size * 0.4
var s := 1.0 if i == n / 2 else 0.65
var pts := PackedVector2Array([c + Vector2(off - h * s, h * 0.7), c + Vector2(off, -h * s), c + Vector2(off + h * s, h * 0.7)])
ci.draw_colored_polygon(pts, col)
ci.draw_line(c + Vector2(-h, h * 0.7), c + Vector2(h, h * 0.7), col, maxf(2, size * 0.08))
"Luft":
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:
var w := h * 0.75
var pts := PackedVector2Array([
c + Vector2(0, -h), c + Vector2(w * 0.55, -h * 0.25), c + Vector2(w * 0.4, h * 0.45),
c + Vector2(0, h), c + Vector2(-w * 0.4, h * 0.45), c + Vector2(-w * 0.55, -h * 0.25)])
ci.draw_colored_polygon(pts, col)
var inner := PackedVector2Array([
c + Vector2(0, -h * 0.35), c + Vector2(w * 0.25, h * 0.35), c + Vector2(0, h * 0.75), c + Vector2(-w * 0.25, h * 0.35)])
ci.draw_colored_polygon(inner, Color(1.0, 0.9, 0.5, 0.75 * col.a))
## Dezenter Hintergrund: Farbverlauf und schräge Linien.
class Backdrop extends Control:
func _draw() -> void:
var sz := size
draw_rect(Rect2(Vector2.ZERO, sz), Style.BG)
var c := Vector2(sz.x * 0.5, sz.y * 0.35)
for i in range(8, 0, -1):
var r := sz.x * 0.09 * i
draw_circle(c, r, Color(0.16, 0.18, 0.26, 0.05))
for k in range(-20, 40):
var x := k * 80.0
draw_line(Vector2(x, 0), Vector2(x + sz.y * 0.6, sz.y), Color(1, 1, 1, 0.012), 1.0)