class_name Style ## Farben, Schriften, Theme und Zeichenhilfen. const FONT: FontFile = preload("res://assets/fonts/DejaVuSans.ttf") static var font_scale := 1.0 # Einstellung „Große Schrift“ static func fs(size: int) -> int: return int(round(size * font_scale)) 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 = 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 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", 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)) 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 ## 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) 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))) # --- 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)