saumwacht-godot/scripts/luneraria/ui/theme/lune_glyphs.gd
Martin Stickelbroeck 1b540f2a55 G3 (Zwischenstand): neues Ingame-HUD; Projekt wechselt nach Unity
Nicht abgenommen: Nach dem ersten Spieltest wurde entschieden, Champions of
Luneraria nach Unity 6 zu portieren. Dieser Stand ist die Referenz für das
HUD im Unity-Projekt (../luneraria-unity).

- HUD neu aus Komponenten mit eigenem Theme, Porträts aus den echten Modellen
  (einmal gerendert, zwischengespeichert), Lane-Übersicht mit deterministischer
  Bedrohung aus öffentlichem Zustand, Bossgrotte, nächste Welle, Ereignisse,
  Minimap mit zusammengefassten Armeen, Held/Items/Talente, Einheitendetails,
  Angriffschampions, Bereit-Sperre, UI-Größe, Team-Formen statt nur Farben
- Privatsphäre über LuneHudModel: fremdes Gold und fremde Entwürfe erscheinen nie
- Keine Regeländerung: Regeln 91/91, M2 73/73, Netz 43/43; verify_g3 715/715;
  verify_m1 38/38 und verify_m2 14/14 mit angepassten UI-Treibern
- Spieltest bis Welle 8 über die echte Oberfläche, Befunde in G3-HUD.md
- HELDEN-KONZEPT.md: vom Projektinhaber vorgegebene Helden (nicht umgesetzt)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:59:55 +02:00

282 lines
15 KiB
GDScript

class_name LuneGlyphs
extends RefCounted
## Vector symbols for the HUD, drawn from a unit square so they stay sharp at every UI scale
## and resolution. Team emblems follow the G2 world: Blau = Mondsichel (round shapes),
## Rot = Sonnenscheibe (rayed, angular shapes), the grotto = hexagon. Symbols are drawn
## with draw_* calls into any CanvasItem; nothing here allocates nodes.
const NAMES: Array[String] = ["coin", "emblem_0", "emblem_1", "emblem_2", "shape_0", "shape_1", "shape_2",
"tank", "melee", "ranged", "splash", "support", "siege", "crown", "champion", "hourglass", "tower",
"ancient", "warning", "check", "close", "bot", "offline", "clock", "heart", "tempo", "range", "armor",
"move", "star", "lock", "up", "lane_top", "lane_mid", "lane_bot", "grotto", "pause", "play", "menu",
"gear", "human", "sword", "basaltschild", "glutklinge", "rankenmantel", "funkenfokus", "quellamulett",
"kristallsplitter", "hero", "skull"]
static var _cache: Dictionary = {}
## Fits the unit square into rect (centred, aspect kept).
static func _frame(rect: Rect2) -> Transform2D:
var s: float = minf(rect.size.x, rect.size.y)
var origin: Vector2 = rect.position + (rect.size - Vector2(s, s)) * .5
return Transform2D(0.0, Vector2(s, s), 0.0, origin)
static func _pts(frame: Transform2D, points: Array) -> PackedVector2Array:
var result := PackedVector2Array()
for p: Vector2 in points:
result.append(frame * p)
return result
static func _rot(points: Array, angle: float, pivot: Vector2 = Vector2(.5, .5)) -> Array:
var result: Array = []
for p: Vector2 in points:
result.append(pivot + (p - pivot).rotated(angle))
return result
static func _fill(ci: CanvasItem, points: PackedVector2Array, color: Color) -> void:
if points.size() < 3:
return
ci.draw_colored_polygon(points, color)
var outline := points.duplicate()
outline.append(points[0])
ci.draw_polyline(outline, color, 1.0, true)
static func _line(ci: CanvasItem, frame: Transform2D, points: Array, color: Color, width: float) -> void:
ci.draw_polyline(_pts(frame, points), color, maxf(1.0, width * frame.x.x), true)
static func _circle(points: int, center: Vector2, radius: float, start: float = 0.0) -> Array:
var result: Array = []
for index: int in range(points):
var angle: float = start + TAU * float(index) / float(points)
result.append(center + Vector2(cos(angle), sin(angle)) * radius)
return result
static func _star(points: int, outer: float, inner: float, start: float = -PI * .5) -> Array:
var result: Array = []
for index: int in range(points * 2):
var angle: float = start + PI * float(index) / float(points)
result.append(Vector2(.5, .5) + Vector2(cos(angle), sin(angle)) * (outer if index % 2 == 0 else inner))
return result
## Crescent (Blau): outer disc minus an offset disc, computed once.
static func _crescent() -> Array:
if not _cache.has("crescent"):
var outer := PackedVector2Array(_circle(48, Vector2(.5, .5), .42))
var inner := PackedVector2Array(_circle(48, Vector2(.66, .4), .33))
var clipped: Array = Geometry2D.clip_polygons(outer, inner)
var result: Array = []
if not clipped.is_empty():
for p: Vector2 in clipped[0]:
result.append(p)
_cache["crescent"] = result
return _cache["crescent"]
static func _heart() -> Array:
if not _cache.has("heart"):
var result: Array = []
for index: int in range(40):
var t: float = TAU * float(index) / 40.0
var x: float = 16.0 * pow(sin(t), 3)
var y: float = 13.0 * cos(t) - 5.0 * cos(2.0 * t) - 2.0 * cos(3.0 * t) - cos(4.0 * t)
result.append(Vector2(.5 + x / 38.0, .47 - y / 38.0))
_cache["heart"] = result
return _cache["heart"]
static func _drop() -> Array:
var result: Array = [Vector2(.5, .16)]
for index: int in range(17):
var angle: float = -PI * .15 + PI * 1.3 * float(index) / 16.0
result.append(Vector2(.5, .64) + Vector2(cos(angle), sin(angle)) * .22)
return result
## Draws one symbol. `accent` is an optional second colour (inner details); by default a
## darkened version of `color`.
static func draw(ci: CanvasItem, glyph: String, rect: Rect2, color: Color, accent: Color = Color(0, 0, 0, 0)) -> void:
var f: Transform2D = _frame(rect)
var dark: Color = accent if accent.a > 0.0 else Color(color.darkened(.55), color.a)
match glyph:
"coin":
var c: Vector2 = f * Vector2(.5, .5)
var r: float = f.x.x * .42
ci.draw_circle(c, r, color)
ci.draw_arc(c, r, 0, TAU, 32, color.darkened(.35), maxf(1.0, r * .12), true)
ci.draw_arc(c, r * .62, 0, TAU, 24, color.darkened(.28), maxf(1.0, r * .1), true)
ci.draw_arc(c, r * .8, PI * 1.1, PI * 1.45, 8, Color(1, 1, 1, .55 * color.a), maxf(1.0, r * .1), true)
"emblem_0":
_fill(ci, _pts(f, _crescent()), color)
"emblem_1":
ci.draw_circle(f * Vector2(.5, .5), f.x.x * .2, color)
for index: int in range(8):
var angle: float = TAU * float(index) / 8.0 - PI * .5
var tip := Vector2(.5, .5) + Vector2(cos(angle), sin(angle)) * .47
var left := Vector2(.5, .5) + Vector2(cos(angle - .22), sin(angle - .22)) * .27
var right := Vector2(.5, .5) + Vector2(cos(angle + .22), sin(angle + .22)) * .27
_fill(ci, _pts(f, [left, tip, right]), color)
"emblem_2":
var hexagon: Array = _circle(6, Vector2(.5, .5), .42, -PI * .5)
hexagon.append(hexagon[0])
_line(ci, f, hexagon, color, .08)
_fill(ci, _pts(f, [Vector2(.5, .3), Vector2(.64, .5), Vector2(.5, .7), Vector2(.36, .5)]), color)
"shape_0":
ci.draw_circle(f * Vector2(.5, .5), f.x.x * .4, color)
"shape_1":
_fill(ci, _pts(f, [Vector2(.5, .06), Vector2(.94, .5), Vector2(.5, .94), Vector2(.06, .5)]), color)
"shape_2":
_fill(ci, _pts(f, _circle(6, Vector2(.5, .5), .44, -PI * .5)), color)
"tank", "armor":
var shield: Array = [Vector2(.2, .16), Vector2(.8, .16), Vector2(.8, .48), Vector2(.72, .66), Vector2(.5, .87), Vector2(.28, .66), Vector2(.2, .48)]
if glyph == "tank":
_fill(ci, _pts(f, shield), color)
_line(ci, f, [Vector2(.5, .24), Vector2(.5, .74)], dark, .07)
else:
shield.append(shield[0])
_line(ci, f, shield, color, .09)
"melee", "sword":
var blade: Array = [Vector2(.5, .06), Vector2(.56, .14), Vector2(.56, .62), Vector2(.44, .62), Vector2(.44, .14)]
_fill(ci, _pts(f, _rot(blade, PI * .25)), color)
_fill(ci, _pts(f, _rot([Vector2(.3, .62), Vector2(.7, .62), Vector2(.7, .69), Vector2(.3, .69)], PI * .25)), color)
_fill(ci, _pts(f, _rot([Vector2(.46, .69), Vector2(.54, .69), Vector2(.54, .88), Vector2(.46, .88)], PI * .25)), color)
"ranged":
var bow: Array = []
for index: int in range(13):
var angle: float = PI * .62 + PI * .76 * float(index) / 12.0
bow.append(Vector2(.78, .5) + Vector2(cos(angle), sin(angle)) * .5)
_line(ci, f, _rot(bow, -PI * .25), color, .08)
_line(ci, f, _rot([bow[0], bow[12]], -PI * .25), color, .03)
_line(ci, f, _rot([Vector2(.14, .5), Vector2(.82, .5)], -PI * .25), color, .06)
_fill(ci, _pts(f, _rot([Vector2(.9, .5), Vector2(.76, .41), Vector2(.76, .59)], -PI * .25)), color)
"splash":
_fill(ci, _pts(f, _star(8, .46, .2)), color)
ci.draw_circle(f * Vector2(.5, .5), f.x.x * .1, dark)
"support":
_fill(ci, _pts(f, [Vector2(.4, .14), Vector2(.6, .14), Vector2(.6, .4), Vector2(.86, .4), Vector2(.86, .6), Vector2(.6, .6),
Vector2(.6, .86), Vector2(.4, .86), Vector2(.4, .6), Vector2(.14, .6), Vector2(.14, .4), Vector2(.4, .4)]), color)
"siege":
_fill(ci, _pts(f, _rot([Vector2(.18, .18), Vector2(.74, .18), Vector2(.74, .42), Vector2(.18, .42)], -PI * .2)), color)
_fill(ci, _pts(f, _rot([Vector2(.42, .4), Vector2(.52, .4), Vector2(.52, .92), Vector2(.42, .92)], -PI * .2)), color)
"crown", "hero":
_fill(ci, _pts(f, [Vector2(.12, .74), Vector2(.12, .3), Vector2(.32, .5), Vector2(.5, .2), Vector2(.68, .5), Vector2(.88, .3), Vector2(.88, .74)]), color)
_fill(ci, _pts(f, [Vector2(.12, .78), Vector2(.88, .78), Vector2(.88, .88), Vector2(.12, .88)]), color)
if glyph == "crown":
ci.draw_circle(f * Vector2(.5, .6), f.x.x * .06, dark)
"skull":
_fill(ci, _pts(f, _circle(20, Vector2(.5, .44), .34)), color)
_fill(ci, _pts(f, [Vector2(.32, .6), Vector2(.68, .6), Vector2(.64, .86), Vector2(.36, .86)]), color)
ci.draw_circle(f * Vector2(.37, .45), f.x.x * .09, dark)
ci.draw_circle(f * Vector2(.63, .45), f.x.x * .09, dark)
"champion":
_fill(ci, _pts(f, [Vector2(.5, .06), Vector2(.82, .46), Vector2(.61, .46), Vector2(.61, .9), Vector2(.39, .9), Vector2(.39, .46), Vector2(.18, .46)]), color)
"hourglass":
_fill(ci, _pts(f, [Vector2(.24, .12), Vector2(.76, .12), Vector2(.56, .5), Vector2(.44, .5)]), color)
_fill(ci, _pts(f, [Vector2(.44, .5), Vector2(.56, .5), Vector2(.76, .88), Vector2(.24, .88)]), color)
_line(ci, f, [Vector2(.18, .1), Vector2(.82, .1)], color, .06)
_line(ci, f, [Vector2(.18, .9), Vector2(.82, .9)], color, .06)
"tower":
_fill(ci, _pts(f, [Vector2(.28, .92), Vector2(.28, .36), Vector2(.22, .36), Vector2(.22, .14), Vector2(.33, .14), Vector2(.33, .23),
Vector2(.44, .23), Vector2(.44, .14), Vector2(.56, .14), Vector2(.56, .23), Vector2(.67, .23), Vector2(.67, .14),
Vector2(.78, .14), Vector2(.78, .36), Vector2(.72, .36), Vector2(.72, .92)]), color)
_fill(ci, _pts(f, [Vector2(.44, .92), Vector2(.44, .72), Vector2(.5, .66), Vector2(.56, .72), Vector2(.56, .92)]), dark)
"ancient":
_fill(ci, _pts(f, [Vector2(.5, .05), Vector2(.76, .4), Vector2(.6, .95), Vector2(.4, .95), Vector2(.24, .4)]), color)
_line(ci, f, [Vector2(.5, .05), Vector2(.5, .95)], dark, .04)
_line(ci, f, [Vector2(.24, .4), Vector2(.5, .52), Vector2(.76, .4)], dark, .04)
"warning":
_fill(ci, _pts(f, [Vector2(.5, .08), Vector2(.94, .88), Vector2(.06, .88)]), color)
_fill(ci, _pts(f, [Vector2(.46, .34), Vector2(.54, .34), Vector2(.53, .64), Vector2(.47, .64)]), dark)
ci.draw_circle(f * Vector2(.5, .75), f.x.x * .05, dark)
"check":
_line(ci, f, [Vector2(.16, .52), Vector2(.4, .76), Vector2(.86, .24)], color, .13)
"close":
_line(ci, f, [Vector2(.2, .2), Vector2(.8, .8)], color, .12)
_line(ci, f, [Vector2(.8, .2), Vector2(.2, .8)], color, .12)
"bot", "gear":
var c: Vector2 = f * Vector2(.5, .5)
for index: int in range(8):
var angle: float = TAU * float(index) / 8.0
_fill(ci, _pts(f, _rot([Vector2(.43, .06), Vector2(.57, .06), Vector2(.57, .24), Vector2(.43, .24)], angle)), color)
ci.draw_circle(c, f.x.x * .3, color)
ci.draw_circle(c, f.x.x * .12, dark)
"human":
ci.draw_circle(f * Vector2(.5, .3), f.x.x * .17, color)
_fill(ci, _pts(f, [Vector2(.2, .9), Vector2(.24, .62), Vector2(.38, .52), Vector2(.62, .52), Vector2(.76, .62), Vector2(.8, .9)]), color)
"offline":
ci.draw_arc(f * Vector2(.5, .5), f.x.x * .38, 0, TAU, 32, color, maxf(1.0, f.x.x * .1), true)
_line(ci, f, [Vector2(.22, .78), Vector2(.78, .22)], color, .1)
"clock":
ci.draw_arc(f * Vector2(.5, .5), f.x.x * .4, 0, TAU, 32, color, maxf(1.0, f.x.x * .09), true)
_line(ci, f, [Vector2(.5, .24), Vector2(.5, .5), Vector2(.68, .62)], color, .08)
"heart":
_fill(ci, _pts(f, _heart()), color)
"tempo":
_fill(ci, _pts(f, [Vector2(.6, .04), Vector2(.22, .56), Vector2(.46, .56), Vector2(.38, .96), Vector2(.78, .42), Vector2(.54, .42), Vector2(.66, .04)]), color)
"range":
var c: Vector2 = f * Vector2(.5, .5)
ci.draw_arc(c, f.x.x * .38, 0, TAU, 32, color, maxf(1.0, f.x.x * .08), true)
ci.draw_arc(c, f.x.x * .18, 0, TAU, 24, color, maxf(1.0, f.x.x * .08), true)
ci.draw_circle(c, f.x.x * .06, color)
for tick: Array in [[Vector2(.5, .02), Vector2(.5, .16)], [Vector2(.5, .84), Vector2(.5, .98)], [Vector2(.02, .5), Vector2(.16, .5)], [Vector2(.84, .5), Vector2(.98, .5)]]:
_line(ci, f, tick, color, .07)
"move":
_line(ci, f, [Vector2(.18, .2), Vector2(.46, .5), Vector2(.18, .8)], color, .11)
_line(ci, f, [Vector2(.52, .2), Vector2(.8, .5), Vector2(.52, .8)], color, .11)
"star":
_fill(ci, _pts(f, _star(5, .47, .2)), color)
"lock":
_fill(ci, _pts(f, [Vector2(.22, .46), Vector2(.78, .46), Vector2(.78, .9), Vector2(.22, .9)]), color)
ci.draw_arc(f * Vector2(.5, .46), f.x.x * .19, PI, TAU, 16, color, maxf(1.0, f.x.x * .09), true)
ci.draw_circle(f * Vector2(.5, .64), f.x.x * .06, dark)
"up":
_fill(ci, _pts(f, [Vector2(.5, .08), Vector2(.86, .48), Vector2(.62, .48), Vector2(.62, .9), Vector2(.38, .9), Vector2(.38, .48), Vector2(.14, .48)]), color)
"lane_top", "lane_mid", "lane_bot":
var box: Array = [Vector2(.12, .12), Vector2(.88, .12), Vector2(.88, .88), Vector2(.12, .88), Vector2(.12, .12)]
_line(ci, f, box, Color(color, color.a * .3), .05)
var path: Array = [Vector2(.2, .8), Vector2(.2, .2), Vector2(.8, .2)] if glyph == "lane_top" else ([Vector2(.2, .8), Vector2(.8, .2)] if glyph == "lane_mid" else [Vector2(.2, .8), Vector2(.8, .8), Vector2(.8, .2)])
_line(ci, f, path, color, .12)
"grotto":
var arch: Array = [Vector2(.08, .9)]
for index: int in range(13):
var angle: float = PI + PI * float(index) / 12.0
arch.append(Vector2(.5, .62) + Vector2(cos(angle) * .42, sin(angle) * .5))
arch.append(Vector2(.92, .9))
_fill(ci, _pts(f, arch), color)
var mouth: Array = [Vector2(.3, .9)]
for index: int in range(9):
var angle: float = PI + PI * float(index) / 8.0
mouth.append(Vector2(.5, .72) + Vector2(cos(angle) * .2, sin(angle) * .26))
mouth.append(Vector2(.7, .9))
_fill(ci, _pts(f, mouth), dark)
"pause":
_fill(ci, _pts(f, [Vector2(.24, .16), Vector2(.42, .16), Vector2(.42, .84), Vector2(.24, .84)]), color)
_fill(ci, _pts(f, [Vector2(.58, .16), Vector2(.76, .16), Vector2(.76, .84), Vector2(.58, .84)]), color)
"play":
_fill(ci, _pts(f, [Vector2(.26, .14), Vector2(.84, .5), Vector2(.26, .86)]), color)
"menu":
for y: float in [.24, .5, .76]:
_line(ci, f, [Vector2(.16, y), Vector2(.84, y)], color, .1)
"basaltschild":
_fill(ci, _pts(f, _circle(6, Vector2(.5, .5), .44, -PI * .5)), color)
var inner: Array = _circle(6, Vector2(.5, .5), .27, -PI * .5)
inner.append(inner[0])
_line(ci, f, inner, dark, .07)
"glutklinge":
var blade: Array = [Vector2(.5, .06), Vector2(.57, .15), Vector2(.57, .66), Vector2(.43, .66), Vector2(.43, .15)]
_fill(ci, _pts(f, _rot(blade, PI * .25)), color)
_fill(ci, _pts(f, _rot([Vector2(.3, .66), Vector2(.7, .66), Vector2(.7, .73), Vector2(.3, .73)], PI * .25)), color)
_fill(ci, _pts(f, [Vector2(.2, .5), Vector2(.3, .3), Vector2(.36, .42), Vector2(.42, .22), Vector2(.5, .44), Vector2(.4, .6)]), dark if accent.a > 0.0 else Color("ffb070"))
"rankenmantel":
_fill(ci, _pts(f, [Vector2(.5, .1), Vector2(.66, .18), Vector2(.8, .9), Vector2(.5, .8), Vector2(.2, .9), Vector2(.34, .18)]), color)
_line(ci, f, [Vector2(.5, .22), Vector2(.42, .4), Vector2(.56, .56), Vector2(.46, .74)], dark, .05)
"funkenfokus":
ci.draw_arc(f * Vector2(.5, .5), f.x.x * .42, 0, TAU, 32, color, maxf(1.0, f.x.x * .08), true)
_fill(ci, _pts(f, [Vector2(.56, .16), Vector2(.3, .54), Vector2(.47, .54), Vector2(.42, .84), Vector2(.7, .44), Vector2(.53, .44), Vector2(.62, .16)]), color)
"quellamulett":
ci.draw_arc(f * Vector2(.5, .2), f.x.x * .14, PI * .9, PI * 2.1, 12, color, maxf(1.0, f.x.x * .06), true)
_fill(ci, _pts(f, _drop()), color)
ci.draw_circle(f * Vector2(.44, .66), f.x.x * .06, Color(1, 1, 1, .45 * color.a))
"kristallsplitter":
for spec: Array in [[Vector2(.5, .5), 0.0, .44], [Vector2(.3, .62), -.55, .3], [Vector2(.7, .62), .55, .3]]:
var center: Vector2 = spec[0]
var length: float = spec[2]
var shard: Array = [center + Vector2(0, -length), center + Vector2(.08, 0), center + Vector2(0, length * .45), center + Vector2(-.08, 0)]
_fill(ci, _pts(f, _rot(shard, spec[1], center)), color)
_:
ci.draw_circle(f * Vector2(.5, .5), f.x.x * .3, color)