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")