Files
assets-generator/preview/mockup_base.gd
2026-10-10 16:35:28 +02:00

430 lines
20 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends Control
## Mockup des Kampfbildschirms (1280×800) von Dinger. Die Optik kommt aus dem Dictionary P;
## Richtungen überschreiben P in _init(). Layout wie BattleLayout im Spiel.
const FONT_DIR := "/home/arbeit/projects/cards/assets/fonts/"
const ELEM := {"Feuer": Color("be3c23"), "Wasser": Color("2d64be"), "Erde": Color("73552d"), "Luft": Color("78aac8")}
var font: FontFile
var bold: FontFile
var P := {
"bg_top": Color("1a1c24"), "bg_bottom": Color("12131a"), "vignette": 0.35,
"panel_bg": Color("282b36"), "panel_bg2": Color("282b36"), "panel_border": Color("3a3e4c"), "panel_border_w": 1,
"panel_radius": 8, "panel_shadow": 6, "panel_highlight": Color(1, 1, 1, 0.0), "chamfer": 0,
"slot_bg": Color(1, 1, 1, 0.03), "slot_border": Color(1, 1, 1, 0.08),
"card_radius": 8, "card_border": Color("14141c"), "card_border_w": 2, "card_inner": Color(1, 1, 1, 0.0),
"card_grad": 0.0, "card_vignette": 0.0, "glyph_shadow": true, "glyph_outline": false,
"band_bg": Color(0.05, 0.05, 0.08, 0.82), "band_text": Color("ececf0"), "band_inset": 0,
"back_bg": Color("3c285a"), "back_bg2": Color("3c285a"), "back_border": Color("f5c850"), "back_inner": Color("5a3c82"),
"stat": "circle", "stat_outline": Color("14141c"), "stat_w": 2,
"btn_bg": Color("3a3e4c"), "btn_bg2": Color("3a3e4c"), "btn_border": Color("9698a5"), "btn_w": 2, "btn_radius": 8, "btn_edge": Color(0, 0, 0, 0),
"text": Color("ececf0"), "muted": Color("9698a5"), "gold": Color("f5c850"), "life": Color("e65555"), "mana": Color("6eaaf0"),
"amber": Color("f0b429"), "blue": Color("4a86d8"), "crystal": Color("3a78d2"),
"rare": Color("c8d0dc"), "ultra": Color("f0aa28"), "elem_light": 0.38,
}
func _init() -> void:
font = FontFile.new()
font.load_dynamic_font(FONT_DIR + "DejaVuSans.ttf")
bold = FontFile.new()
bold.load_dynamic_font(FONT_DIR + "DejaVuSans-Bold.ttf")
# --- Grundformen ---------------------------------------------------------------
static func lighter(c: Color, d: float) -> Color:
return Color(minf(1, c.r + d), minf(1, c.g + d), minf(1, c.b + d), c.a)
static func darker(c: Color, f: float) -> Color:
return Color(c.r * f, c.g * f, c.b * f, c.a)
## Eckpunkte eines abgerundeten (oder abgeschrägten) Rechtecks.
func shape(rect: Rect2, radius: float) -> PackedVector2Array:
var pts := PackedVector2Array()
var cut: float = P.chamfer
if cut > 0:
var p := rect.position
var e := rect.end
pts.append_array([Vector2(p.x + cut, p.y), Vector2(e.x - cut, p.y), Vector2(e.x, p.y + cut), Vector2(e.x, e.y - cut),
Vector2(e.x - cut, e.y), Vector2(p.x + cut, e.y), Vector2(p.x, e.y - cut), Vector2(p.x, p.y + cut)])
return pts
radius = minf(radius, minf(rect.size.x, rect.size.y) / 2)
var centers := [rect.position + Vector2(radius, radius), Vector2(rect.end.x - radius, rect.position.y + radius),
rect.end - Vector2(radius, radius), Vector2(rect.position.x + radius, rect.end.y - radius)]
for i in 4:
for k in 13:
var a := -PI + PI / 2 * i + PI / 2 * k / 12
pts.append(centers[i] + Vector2(cos(a), sin(a)) * radius)
return pts
## Fläche: senkrechter Verlauf von top nach bottom.
func fill(rect: Rect2, radius: float, top: Color, bottom: Color) -> void:
var pts := shape(rect, radius)
var cols := PackedColorArray()
for p in pts:
cols.append(top.lerp(bottom, clampf((p.y - rect.position.y) / rect.size.y, 0, 1)))
draw_polygon(pts, cols)
func outline(rect: Rect2, radius: float, col: Color, w: float) -> void:
if P.chamfer > 0:
var pts := shape(rect.grow(-w / 2), radius - w / 2)
pts.append(pts[0])
draw_polyline(pts, col, w, true)
return
var sb := StyleBoxFlat.new()
sb.draw_center = false
sb.border_color = col
sb.set_border_width_all(int(round(w)))
sb.set_corner_radius_all(int(radius))
sb.anti_aliasing_size = 1.0
draw_style_box(sb, rect)
func shadow(rect: Rect2, radius: float, size: int, alpha := 0.45, offset := Vector2(0, 3)) -> void:
if size <= 0:
return
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0, 0, 0, 0)
sb.set_corner_radius_all(int(radius))
sb.shadow_color = Color(0, 0, 0, alpha)
sb.shadow_size = size
sb.shadow_offset = offset
draw_style_box(sb, rect)
func glow(rect: Rect2, radius: float, col: Color, size: int) -> void:
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0, 0, 0, 0)
sb.set_corner_radius_all(int(radius))
sb.shadow_color = Color(col, 0.55)
sb.shadow_size = size
draw_style_box(sb, rect)
## Dunkler Rand nach innen (Vignette) als Ringe.
func vignette(rect: Rect2, radius: float, strength: float) -> void:
if strength <= 0:
return
for i in 4:
var r := rect.grow(-i * 3.0)
outline(r, radius - i * 3, Color(0, 0, 0, strength * (0.3 - i * 0.06)), 3)
func txt(s: String, size: int, col: Color, pos: Vector2, anchor := "center", b := false) -> void:
var f := bold if b else font
var dim := f.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size)
var p := pos
match anchor:
"center": p -= dim / 2
"midtop": p.x -= dim.x / 2
"midleft": p.y -= dim.y / 2
"midright": p -= Vector2(dim.x, dim.y / 2)
"topright": p.x -= dim.x
draw_string(f, Vector2(p.x, p.y + f.get_ascent(size)), s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, col)
func txt_outlined(s: String, size: int, col: Color, pos: Vector2, ow := 4, ocol := Color(0.05, 0.05, 0.08, 0.95)) -> void:
var dim := bold.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size)
var at := pos - dim / 2 + Vector2(0, bold.get_ascent(size))
draw_string_outline(bold, at, s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, ow, ocol)
draw_string(bold, at, s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, col)
func fit(s: String, size: int, max_w: float) -> String:
while bold.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
# --- Bausteine -----------------------------------------------------------------
func panel(rect: Rect2, border = null) -> void:
shadow(rect, P.panel_radius, P.panel_shadow)
fill(rect, P.panel_radius, P.panel_bg, P.panel_bg2)
if P.panel_highlight.a > 0:
draw_line(rect.position + Vector2(P.panel_radius, 1.5), Vector2(rect.end.x - P.panel_radius, rect.position.y + 1.5), P.panel_highlight, 1.0)
var bc: Color = border if border != null else P.panel_border
if P.panel_border_w > 0:
outline(rect, P.panel_radius, bc, P.panel_border_w)
func button(rect: Rect2, label: String, size := 14) -> void:
if P.btn_edge.a > 0:
fill(Rect2(rect.position + Vector2(0, 4), rect.size), P.btn_radius, P.btn_edge, P.btn_edge)
else:
shadow(rect, P.btn_radius, 4, 0.35)
fill(rect, P.btn_radius, P.btn_bg, P.btn_bg2)
if P.panel_highlight.a > 0:
draw_line(rect.position + Vector2(P.btn_radius, 1.5), Vector2(rect.end.x - P.btn_radius, rect.position.y + 1.5), P.panel_highlight, 1.0)
if P.btn_w > 0:
outline(rect, P.btn_radius, P.btn_border, P.btn_w)
txt(label, size, P.text, rect.get_center())
func slot(rect: Rect2) -> void:
fill(rect, P.card_radius, P.slot_bg, P.slot_bg)
outline(rect, P.card_radius, P.slot_border, 1)
## Zahl auf Form: Kreis, Sechseck oder Schild.
func stat(c: Vector2, r: float, value: String, col: Color, fsize: int, shape_name := "") -> void:
var kind: String = shape_name if shape_name != "" else P.stat
if kind == "hex":
var pts := PackedVector2Array()
for i in 6:
var a := -PI / 2 + i * PI / 3
pts.append(c + Vector2(cos(a), sin(a)) * r)
var closed := pts.duplicate()
closed.append(pts[0])
draw_colored_polygon(pts, col)
draw_colored_polygon(PackedVector2Array([pts[4], pts[5], pts[0], c + Vector2(0, -r * 0.1), c + Vector2(-r * 0.55, r * 0.15)]), Color(1, 1, 1, 0.16))
draw_polyline(closed, P.stat_outline, P.stat_w + 1, true)
else:
draw_circle(c, r + P.stat_w, P.stat_outline, true, -1, true)
draw_circle(c, r, col, true, -1, true)
draw_circle(c + Vector2(-r * 0.25, -r * 0.3), r * 0.45, Color(1, 1, 1, 0.16), true, -1, true)
draw_arc(c, r - 0.5, 0, TAU, 40, darker(col, 0.75), 1.5, true)
txt_outlined(value, fsize, Color.WHITE, c, 3)
func crystal(c: Vector2, r: float, cost: String, fsize: int) -> void:
stat(c, r, cost, P.crystal, fsize, "hex")
func keyword_icon(c: Vector2, r: float, kind: String, col: Color) -> void:
draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.75))
var w := maxf(1.5, r * 0.22)
match kind:
"Doppelschlag":
draw_line(c + Vector2(-r * 0.65, r * 0.65), c + Vector2(r * 0.6, -r * 0.6), col, w)
draw_line(c + Vector2(r * 0.65, r * 0.65), c + Vector2(-r * 0.6, -r * 0.6), col, w)
"Regeneration":
draw_line(c + Vector2(0, -r * 0.7), c + Vector2(0, r * 0.7), col, w * 1.5)
draw_line(c + Vector2(-r * 0.7, 0), c + Vector2(r * 0.7, 0), col, w * 1.5)
"Spott":
draw_line(c + Vector2(0, -r * 0.7), c + Vector2(0, r * 0.2), col, w * 1.6)
draw_circle(c + Vector2(0, r * 0.6), w * 0.95, col)
"Flink":
for i in 2:
var x := c.x - r * 0.55 + i * r * 0.55
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)
# --- Elementsymbole --------------------------------------------------------------
func glyph(elem: String, c: Vector2, s: float, alpha := 0.9) -> void:
var col := lighter(ELEM[elem], P.elem_light)
col.a = alpha
if P.glyph_outline:
for o in [Vector2(-2, 0), Vector2(2, 0), Vector2(0, -2), Vector2(0, 2), Vector2(2, 2), Vector2(-2, 2)]:
_glyph(elem, c + o, s, Color(0.04, 0.03, 0.06, alpha))
elif P.glyph_shadow:
_glyph(elem, c + Vector2(2, 3), s, Color(0, 0, 0, alpha * 0.45))
_glyph(elem, c, s, col)
_glyph_light(elem, c, s, Color(1, 1, 1, alpha * 0.35))
func _glyph(elem: String, c: Vector2, s: float, col: Color) -> void:
var h := s / 2
match elem:
"Feuer":
for i in 3:
var off := Vector2((i - 1) * s * 0.32, (0 if i == 1 else s * 0.12))
var hh := h * (1.0 if i == 1 else 0.68)
var w := hh * 0.75
draw_colored_polygon(PackedVector2Array([c + off + Vector2(0, -hh), c + off + Vector2(w * 0.55, -hh * 0.25), c + off + Vector2(w * 0.4, hh * 0.45),
c + off + Vector2(0, hh), c + off + Vector2(-w * 0.4, hh * 0.45), c + off + Vector2(-w * 0.55, -hh * 0.25)]), col)
"Wasser":
for i in 2:
var y := c.y + (i - 0.5) * s * 0.3
for k in 3:
var x := c.x - s * 0.4 + k * s * 0.4
draw_arc(Vector2(x, y), s * 0.2, PI, TAU, 12, col, maxf(2, s * 0.09), true)
"Erde":
for i in 3:
var off := (i - 1) * s * 0.4
var hs := h * (1.0 if i == 1 else 0.65)
draw_colored_polygon(PackedVector2Array([c + Vector2(off - hs, h * 0.7), c + Vector2(off, -hs), c + Vector2(off + hs, h * 0.7)]), col)
draw_line(c + Vector2(-h, h * 0.7), c + Vector2(h, h * 0.7), col, maxf(2, s * 0.08))
"Luft":
for i in 3:
var r := h * (1.0 - i * 0.28)
draw_arc(c + Vector2(i * s * 0.06, 0), r, PI * 0.2 + i * 0.4, PI * 1.5 + i * 0.4, 20, col, maxf(2, s * 0.09), true)
## Lichtkante: kleiner heller Kern.
func _glyph_light(elem: String, c: Vector2, s: float, col: Color) -> void:
var h := s / 2
match elem:
"Feuer":
var w := h * 0.75
draw_colored_polygon(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)]), Color(1.0, 0.9, 0.5, col.a * 2))
"Erde":
draw_colored_polygon(PackedVector2Array([c + Vector2(0, -h), c + Vector2(h * 0.35, -h * 0.4), c + Vector2(-h * 0.1, -h * 0.4)]), col)
_:
pass
# --- Karten ------------------------------------------------------------------------
## Kartenfläche in Elementfarbe mit Rahmen; frame: Rahmenfarbe (Seltenheit) oder null.
func card_body(rect: Rect2, elem: String, frame = null, shadow_size := 6) -> void:
var base: Color = ELEM[elem]
var fc: Color = frame if frame != null else P.card_border
if frame != null:
glow(rect, P.card_radius, fc, 7)
else:
shadow(rect, P.card_radius, shadow_size)
fill(rect, P.card_radius, lighter(base, P.card_grad), darker(base, 1.0 - P.card_grad * 1.6))
vignette(rect, P.card_radius, P.card_vignette)
if P.card_inner.a > 0:
outline(rect.grow(-P.card_border_w - 1), P.card_radius - P.card_border_w, P.card_inner, 1)
outline(rect, P.card_radius, fc, P.card_border_w)
func name_band(rect: Rect2, y: float, h: float, name: String, fsize: int, col = null) -> void:
var band := Rect2(rect.position.x + P.band_inset, y, rect.size.x - P.band_inset * 2, h)
if P.band_inset > 0:
fill(band, h / 2, P.band_bg, P.band_bg)
else:
draw_rect(band, P.band_bg)
var c: Color = col if col != null else P.band_text
txt(fit(name, fsize, band.size.x - 10), fsize, c, band.get_center(), "center", true)
## Kleine Karte (Hand, Feld): 110×140.
func card_small(rect: Rect2, name: String, elem: String, cost: int, atk: int, hp: int, frame = null, name_col = null) -> void:
card_body(rect, elem, frame)
glyph(elem, rect.position + Vector2(rect.size.x / 2, rect.size.y * 0.35), rect.size.x * 0.56)
name_band(rect, rect.position.y + rect.size.y * 0.5, 22, name, 11, name_col)
stat(rect.position + Vector2(17, rect.size.y - 17), 11, str(atk), P.amber, 13)
stat(rect.position + Vector2(rect.size.x - 17, rect.size.y - 17), 11, str(hp), P.blue, 13)
crystal(rect.position + Vector2(17, 17), 11, str(cost), 13)
## Große Karte (Vorschau): 170×238, mit Eigenschaft.
func card_big(rect: Rect2, name: String, elem: String, cost: int, atk: int, hp: int, kw: String, frame = null, name_col = null) -> void:
card_body(rect, elem, frame, 10)
glyph(elem, rect.position + Vector2(rect.size.x / 2, rect.size.y * 0.2), rect.size.x * 0.56)
name_band(rect, rect.position.y + rect.size.y * 0.36, 26, name, 14, name_col)
var y := rect.position.y + rect.size.y * 0.36 + 40
keyword_icon(Vector2(rect.position.x + 40, y), 9, kw, lighter(ELEM[elem], 0.45))
txt(kw, 12, P.text, Vector2(rect.position.x + 56, y), "midleft")
var box := Rect2(rect.position.x + 8, y + 18, rect.size.x - 16, 54)
draw_rect(box, Color(0.06, 0.06, 0.09, 0.6))
txt("Greift zweimal je Zug", 11, P.text, box.get_center())
stat(rect.position + Vector2(22, rect.size.y - 22), 15, str(atk), P.amber, 17)
stat(rect.position + Vector2(rect.size.x - 22, rect.size.y - 22), 15, str(hp), P.blue, 17)
crystal(rect.position + Vector2(22, 22), 14, str(cost), 17)
func card_back(rect: Rect2, deg := 0.0) -> void:
var c := rect.get_center()
draw_set_transform(c, deg_to_rad(deg))
var r := Rect2(-rect.size / 2, rect.size)
shadow(r, P.card_radius, 6)
fill(r, P.card_radius, P.back_bg, P.back_bg2)
outline(r.grow(-9), P.card_radius - 4, P.back_inner, 2)
draw_circle(Vector2.ZERO, 22, Color(P.back_inner, 0.5), true, -1, true)
draw_arc(Vector2.ZERO, 22, 0, TAU, 48, P.back_border, 2, true)
outline(r, P.card_radius, P.back_border, P.card_border_w + 1)
txt("?", 30, P.back_border, Vector2(0, 0), "center", true)
draw_set_transform(Vector2.ZERO)
func deck(rect: Rect2, count: String, label := "") -> void:
for i in range(3, 0, -1):
var r := Rect2(rect.position + Vector2(i * 3, -i * 3), rect.size)
fill(r, P.card_radius, darker(P.back_bg, 0.8), darker(P.back_bg2, 0.8))
outline(r, P.card_radius, Color(0, 0, 0, 0.5), 1)
shadow(rect, P.card_radius, 6)
fill(rect, P.card_radius, P.back_bg, P.back_bg2)
outline(rect.grow(-9), P.card_radius - 4, P.back_inner, 2)
outline(rect, P.card_radius, Color(P.back_border, 0.6), 2)
if label != "":
txt(label, 17, P.muted, rect.get_center() + Vector2(0, -14), "center", true)
txt("◆ 0", 17, P.mana, rect.get_center() + Vector2(0, 14), "center", true)
else:
txt(count, 22, P.text, rect.get_center(), "center", true)
txt(count, 11, P.muted, Vector2(rect.get_center().x, rect.end.y - 22), "center")
func hero_box(rect: Rect2, name: String, life: String, border = null) -> void:
panel(rect, border)
txt(name, 17, P.text, Vector2(rect.get_center().x, rect.position.y + 12), "midtop", true)
txt("♥", 24, P.life, Vector2(rect.position.x + 24, rect.position.y + 46), "center", true)
txt(life, 24, P.life, Vector2(rect.position.x + 42, rect.position.y + 46), "midleft", true)
txt("◆", 20, P.mana, Vector2(rect.end.x - 54, rect.position.y + 46), "center", true)
txt("0", 24, P.mana, Vector2(rect.end.x - 14, rect.position.y + 46), "midright", true)
func element_row(rect: Rect2, elem: String, kw: String, filled: int, played: String) -> void:
var col: Color = ELEM[elem]
fill(rect, 6, Color(col, 0.16), Color(col, 0.10))
outline(rect, 6, Color(col, 0.45), 1)
txt(elem, 13, P.text, Vector2(rect.position.x + 10, rect.get_center().y), "midleft", true)
for k in 2:
keyword_icon(Vector2(rect.position.x + 86 + k * 24, rect.get_center().y), 8, kw, lighter(col, 0.45) if k < filled else Color(col, 0.35))
txt(played, 15, lighter(col, 0.45), Vector2(rect.position.x + 142, rect.get_center().y), "midleft", true)
# --- Hintergrund ---------------------------------------------------------------------
func background() -> void:
fill(Rect2(Vector2.ZERO, size), 0, P.bg_top, P.bg_bottom)
var c := Vector2(size.x * 0.5, size.y * 0.42)
for i in range(8, 0, -1):
draw_circle(c, size.x * 0.09 * i, Color(1, 1, 1, 0.012))
if P.vignette > 0:
for i in 6:
outline(Rect2(Vector2.ZERO, size).grow(-i * 14.0), 0, Color(0, 0, 0, P.vignette * 0.12), 28)
# --- Der Bildschirm ------------------------------------------------------------------
func _draw() -> void:
background()
# Gegner: Hand oben, Stapel und Kasten rechts
for i in 4:
var r := Rect2(410 + i * 120, 11, 110, 140)
var t := i - 1.5
card_back(Rect2(r.position + Vector2(0, -t * t * 3), r.size), -t * 3)
deck(Rect2(1090, 10, 130, 182), "35")
hero_box(Rect2(1050, 200, 210, 70), "Gegner", "35", P.mana)
button(Rect2(20, 48, 210, 32), "Überspringen", 13)
element_row(Rect2(20, 222, 210, 28), "Feuer", "Doppelschlag", 1, "1")
# Bretter
for j in 5:
var r := Rect2(345 + j * 120, 220, 110, 140)
if j == 0:
card_small(r, "Schwelender Drache", "Feuer", 1, 0, 5)
else:
slot(r)
for j in 5:
var r := Rect2(345 + j * 120, 440, 110, 140)
if j == 0:
card_small(r, "Rieselnder Geist", "Wasser", 1, 1, 2)
else:
slot(r)
element_row(Rect2(20, 442, 210, 28), "Wasser", "Regeneration", 1, "1")
# Hand
var hand := [["Leiser Hauch", "Luft", 0, 0, 1, null, null], ["Glimmender Wolf", "Feuer", 1, 1, 3, P.rare, lighter(P.rare, 0.25)],
["Stiller Tropfen", "Wasser", 0, 0, 1, null, null], ["Fester Kiesel", "Erde", 1, 1, 1, P.ultra, lighter(P.ultra, 0.25)]]
var total := hand.size() * 110 + (hand.size() - 1) * 10
for i in hand.size():
var h = hand[i]
var t := i - (hand.size() - 1) / 2.0
var r := Rect2(240 + (800 - total) / 2 + i * 120, 610 + t * t * 3, 110, 140)
draw_set_transform(r.get_center(), deg_to_rad(t * 3))
card_small(Rect2(-r.size / 2, r.size), h[0], h[1], h[2], h[3], h[4], h[5], h[6])
draw_set_transform(Vector2.ZERO)
# Eigener Kasten, Stapel, Vorschau
hero_box(Rect2(1050, 526, 210, 70), "Spieler", "30")
deck(Rect2(1090, 606, 130, 182), "35", "Ziehen")
card_big(Rect2(1070, 278, 170, 238), "Glimmender Wolf", "Feuer", 1, 1, 3, "Doppelschlag", P.rare, lighter(P.rare, 0.25))
txt("Dinger · Richtung: " + P.get("name", "?"), 12, P.muted, Vector2(20, 780), "midleft")