690 lines
30 KiB
GDScript
690 lines
30 KiB
GDScript
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“.
|
|
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)
|
|
|
|
|
|
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)
|