Files
cards/scripts/ui/style.gd

368 lines
16 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")
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")
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 = 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", StyleBoxEmpty.new())
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", 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
p.add_theme_stylebox_override("panel", box(bg, radius, border_col, border_w))
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", 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))
parent.add_child(b)
return b
## 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
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: int, color: Color, pos: Vector2, anchor: String = "topleft", bold: bool = false) -> void:
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)
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)
static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius: int = 8) -> void:
var a: Color = Cards.ELEMENT_COLORS[elements[0]]
if elements.size() == 1:
ci.draw_style_box(box(a, radius), rect)
elif elements[0] == elements[1]:
# Doppeltes Element: gleiche Farbe mit zweitem, hellem Innenrahmen
ci.draw_style_box(box(a, radius), rect)
border(ci, rect.grow(-5), lighter(a, 0.27), 2, maxi(2, radius - 3))
else:
var b: Color = Cards.ELEMENT_COLORS[elements[1]]
var half := floorf(rect.size.x / 2)
var left := StyleBoxFlat.new()
left.bg_color = a
left.corner_radius_top_left = radius
left.corner_radius_bottom_left = radius
var right := StyleBoxFlat.new()
right.bg_color = b
right.corner_radius_top_right = radius
right.corner_radius_bottom_right = radius
ci.draw_style_box(left, Rect2(rect.position, Vector2(half, rect.size.y)))
ci.draw_style_box(right, Rect2(rect.position + Vector2(half, 0), Vector2(rect.size.x - half, rect.size.y)))
static func rules_text() -> String:
var lines := [
"Ziel: Besiege alle anderen. Du verlierst, wenn dein Leben auf 0 fällt.",
"Du spielst gegen 1 bis 5 Magier, jeder gegen jeden, alle nach denselben Regeln:",
"%d Leben, pro Zug 1 Energie mehr (höchstens 10) und 1 Karte. Die Zugreihenfolge wird ausgelost." % Game.START_LIFE,
"Wer nicht beginnt, startet mit einer Karte mehr und hat im ersten Zug 1 Energie extra.",
"Fallen liegen verdeckt (höchstens 2) und lösen aus, wenn dich jemand angreift oder ein Wesen ausspielt.",
"",
"Karten kosten Energie. Wesen kommen aufs Feld (max. 5) und greifen ab dem nächsten Zug an.",
"Zauber wirken sofort. Ein Wesen greift ein gegnerisches Wesen oder direkt den Gegner an;",
"beide Seiten verursachen ihren Angriffswert als Schaden.",
"",
"Elemente: Feuer (Schaden), Wasser (Kontrolle), Erde (Zähigkeit), Luft (Tempo).",
"Kombos verbinden zwei Elemente, auch das gleiche doppelt (Feuer+Feuer = Inferno).",
"Kombokarten sind seltener und stärker, Dreifach-Kombos sind mythisch. Ein Deck darf höchstens 2 Elemente enthalten.",
Cards.RESONANCE_TEXT,
"Feldkombo: Spielst du ein Wesen neben ein eigenes Wesen mit anderem Element, löst eine Kombo aus (eine je Ausspielen):",
" Dampf friert das stärkste gegnerische Wesen, Lava macht 1 Schaden an alle gegnerischen Wesen, Blitz 2 Schaden",
" an den schwächsten Gegner, Schlamm heilt dich um 3, Sturm zieht 1 Karte, Sand gibt dem neuen Wesen Schild.",
"",
"Schlüsselwörter:",
]
for k in Cards.KEYWORDS:
lines.append(" %s: %s" % [k, Cards.KEYWORD_HELP[k]])
lines.append("")
lines.append("Deck: genau %d Karten, höchstens %dx dieselbe Karte." % [Game.DECK_SIZE, Game.MAX_COPIES])
lines.append("Leeres Deck: jede weitere gezogene Karte verursacht steigenden Schaden.")
lines.append("Booster: 5 Karten für 100 Münzen, mindestens eine seltene. Siege bringen Münzen.")
lines.append("")
lines.append("Werkstatt: Karten zerlegen bringt Staub, aus Staub entstehen neue Karten. Fünf Deck-Slots.")
lines.append("Lokal zu mehreren: bis zu vier Menschen am selben Rechner, dazu KI, ohne Münzen.")
lines.append("Mulligan: Im ersten eigenen Zug darfst du einmal Handkarten zurücklegen und nachziehen.")
lines.append("Deckstärke: Seltenheit, Schlüsselwörter und Effekte ergeben je Karte einen Wert. Gegnerdecks werden auf deine")
lines.append(" Stärke gebaut (Leicht 0,9-fach, Normal 1-fach, Schwer 1,2-fach mit mehr Münzen). Die Kampagne liegt nie darunter.")
lines.append("Kampagnen-Bosse haben ein Merkmal (★), zum Beispiel stärkere Feuerwesen. Netzwerkspiel im lokalen Netz.")
lines.append("Steuerung: Linksklick spielt Karten und wählt Ziele, Rechtsklick oder Esc bricht ab, Leertaste beendet den Zug, 1 bis 9 spielen Handkarten.")
return "\n".join(lines)
# --- Kartensymbole (prozedural, je Element und Kartenart) --------------------------
static func glyph(ci: CanvasItem, card, center: Vector2, size: float) -> void:
var variant := absi(hash(card.id)) % 3
var col := lighter(Cards.ELEMENT_COLORS[card.elements[0]], 0.38)
col.a = 0.95
if card.is_trap():
# Falle: Kreuz im Kreis
ci.draw_arc(center, size * 0.5, 0, TAU, 32, col, maxf(2, size * 0.08))
var d := size * 0.28
ci.draw_line(center + Vector2(-d, -d), center + Vector2(d, d), col, maxf(2, size * 0.1))
ci.draw_line(center + Vector2(-d, d), center + Vector2(d, -d), col, maxf(2, size * 0.1))
return
if card.kind == "Zauber":
_star(ci, center, size * 0.55, Color(col, 0.35))
var elems: Array = card.elements
if elems.size() >= 2 and elems[0] != elems[1]:
_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 = 0.95
_element_glyph(ci, elems[1], center + Vector2(size * 0.28, 0), size * 0.62, variant, col2)
else:
_element_glyph(ci, elems[0], center, size, variant, col)
if elems.size() >= 2:
ci.draw_arc(center, size * 0.6, 0, TAU, 32, Color(col, 0.7), 2)
if elems.size() >= 3:
ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2)
static func _star(ci: CanvasItem, c: Vector2, r: float, col: Color) -> void:
var pts := PackedVector2Array()
for i in 8:
var a := TAU * i / 8 - PI / 2
var rr := r if i % 2 == 0 else r * 0.45
pts.append(c + Vector2(cos(a), sin(a)) * rr)
ci.draw_colored_polygon(pts, 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))
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))
## 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)