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))) 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 das stärkste gegnerische Wesen, Blitz 2 Schaden", " an den schwächsten Gegner, Schlamm heilt dich um 3, Sturm zieht 1 Karte, Sand gibt dem neuen Wesen +1/+1.", "", "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("Elementbooster: 5 Karten eines Elements (und seiner Kombos) für %d Münzen." % Profile.ELEMENT_BOOSTER_COST) lines.append("Deck-Code: im Deckbau unter „Code“ kopieren und mit anderen teilen.") lines.append("Ein unterbrochener Einzelspieler-Kampf lässt sich im Menü fortsetzen.") 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("Netzwerkspiel: Fällt der Host aus, übernimmt ein Mitspieler; Zuschauer sehen die Partie ohne Hände.") 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)