Elemente, Kombos, Fallen, KI mit Vorausschau, Kampagne, Hotseat und Netzwerk, Werkstatt, Erfolge, Tagesaufgaben, Tests und Exporte. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
366 lines
16 KiB
GDScript
366 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("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)
|