## UiSkin - the game's UI look (our own procedural art, no image files, Kanit font). ## v2 (Graphics v2): textured parchment panels + glossy buttons in place of hud.gd's flat StyleBoxes. ## v3 (UI v3, owner 2026-09-30, style reference Draconia Saga - nothing copied): a storybook parchment ## "page" with a dark wood frame and a woven triangle band along its top / bottom edge, a dark leather ## title pill with a gold medallion, a round orange close button with a gold X, cream / gold tabs with ## diamond studs, glossy buttons (normal / hover glow / pressed / disabled), recessed inset panels, gold ## list cards, rarity-tinted item slots, round gold hotbar rings, dark HUD glass, glossy bars, and the ## window open / close animation (scale + fade, 0.16 s). ## Every function works for any window: other agents (mount / gacha / stable windows) call these too. ## - skin_theme(theme) theme-wide: font, panels, buttons, tooltips, scroll bars, line edits ## - window_panel(), inset(), card(on), glass(alpha), pill(), plate() StyleBoxes ## - buttons(base), tab_styles(on), close_styles(), bar_styles(col, h), slot_box(col), ring(kind) ## - decorate_window(hud, panel, plaque, bar, title, close) hud._window() look + animation ## - open_anim(panel) / close_window(panel) / set_open(panel, on) ## - label(text, size, col, weight), gold_title(...) Kanit labels ## "--gfxlegacy" keeps the old flat look, "--ui2" the v2 look (A/B shots). const GfxLook = preload("res://scripts/gfx_look.gd") const Kanit = preload("res://scripts/ui/kanit.gd") const PARCH := Color("#f2e5c4") const FRAME := Color("#97764a") const GOLD := Color("#e2bf6a") # ---- UI v3 palette (measured on the reference captures, then tuned; see docs/PAUSE_NOTES.md "UI v3") const PAGE := Color("#f6ebcd") # parchment page const PAGE_EDGE := Color("#dcc494") # burnt page edge const WOOD := Color("#4f3322") # window frame const WOOD_HI := Color("#8a5e3e") const WOOD_DK := Color("#2a170c") const BAND := Color("#dcc28a") # woven band on the page const LEATHER := Color("#5a3a29") # title pill const LEATHER_DK := Color("#3a2317") const STITCH := Color("#a07a55") const TITLE_TXT := Color("#f6d98e") const INSET := Color("#eee1bf") const INSET_LINE := Color("#d6c296") const TAB := Color("#f4e7c6") const TAB_ON := Color("#f7d77f") const TAB_LINE := Color("#cdb384") const TAB_ON_LINE := Color("#c4923a") const TXT := Color("#5b4128") # dark brown text on parchment const TXT_SOFT := Color("#8a7052") const GREEN := Color("#3fa564") const GOLDBTN := Color("#f0b43a") const RED := Color("#d9573a") const RARITY := {"common": Color("#c8a878"), "uncommon": Color("#6cc24a"), "rare": Color("#5a9cf0"), "epic": Color("#b77cf0"), "legend": Color("#f0b43a"), "mythic": Color("#ef6a4a")} static var _cache := {} const Box9 = preload("res://scripts/ui/box9.gd") const Skin4 = preload("res://scripts/ui/skin4.gd") ## UI textures are painted at S x their layout size (crisp at 1080p / 4K, mipmapped for 720p) and kept in ## user://ui3_cache (painting is slow in GDScript; bump CACHE_VER when a painter changes). const S := 2 const CACHE_VER := "u3b1" static func _box() -> StyleBox: var b = Box9.new() b.scale_px = float(S) return b ## Texture for `key`: from the disk cache, else painted by `paint` (an Image) and cached; with mipmaps. static func _mk(key: String, paint: Callable) -> Texture2D: var path := "user://ui3_cache/%s_%s.png" % [CACHE_VER, key.replace("#", "").replace(" ", "")] var img: Image = null if FileAccess.file_exists(path): img = Image.load_from_file(path) if img == null or img.is_empty(): img = paint.call() DirAccess.make_dir_recursive_absolute("user://ui3_cache") img.save_png(path) img.generate_mipmaps() return ImageTexture.create_from_image(img) ## UI v3 on (default); "--ui2" / "--gfxlegacy" = the older looks. static func v3() -> bool: if _cache.has("_v3"): return _cache["_v3"] var on := not GfxLook.legacy() and not ("--ui2" in OS.get_cmdline_user_args()) _cache["_v3"] = on return on ## UI v4 "painted storybook" (default; ui/skin4.gd): cream panels with vine / flower / crystal corners, green ## tabs, cream / green buttons. "--ui3" (or settings ui_style = "v3") keeps the v3 wood + parchment look. static func v4() -> bool: if _cache.has("_v4"): return _cache["_v4"] var on := v3() and not ("--ui3" in OS.get_cmdline_user_args()) and Skin4.has("win") var st = Engine.get_main_loop().root.get_node_or_null("/root/Settings") if Engine.get_main_loop() is SceneTree else null if on and st != null and st.has_method("val") and str(st.val("ui_style")) == "v3": on = false _cache["_v4"] = on return on ## No animations in the self-test (windows must open / close at once for the checks). static func animate() -> bool: if not _cache.has("_anim"): _cache["_anim"] = not ("--selftest" in OS.get_cmdline_user_args()) return _cache["_anim"] ## Sharper UI on a viewport: mipmapped texture filtering (2x UI textures + icons stay crisp at 720p) and ## pixel-snapped 2D transforms (no half-pixel blur on text / borders). static func crisp(vp: Viewport) -> void: if vp == null or not v3(): return vp.canvas_item_default_texture_filter = Viewport.DEFAULT_CANVAS_ITEM_TEXTURE_FILTER_LINEAR_WITH_MIPMAPS vp.snap_2d_transforms_to_pixel = true ## Rhythmic shimmer for round skill icons (owner: "วิบๆ เป็นจังหวะ"): a light glint sweeps across every ~2.2 s, ## plus a soft glow pulse; additive, masked to the icon circle. One shared material (all icons in rhythm). const SHIMMER_CODE := """ shader_type canvas_item; render_mode blend_add; uniform float period = 2.2; uniform float strength = 1.0; void fragment() { vec2 uv = UV - vec2(0.5); float r = length(uv); float mask = 1.0 - smoothstep(0.43, 0.47, r); float t = mod(TIME, period) / period; float p = t / 0.32; float band = 0.0; if (p < 1.0) { float x = (UV.x + UV.y) * 0.5 - (p * 1.5 - 0.25); band = exp(-x * x / 0.0035); } float pulse = 0.5 + 0.5 * sin(TIME * 6.2831853 / period); float glow = (0.05 + 0.07 * pulse) * (1.0 - r * 1.8); float a = (band * 0.55 + max(glow, 0.0)) * mask * strength; COLOR = vec4(vec3(1.0, 0.97, 0.86) * a, a); } """ static func shimmer_mat() -> ShaderMaterial: if not _cache.has("_shimmer"): var sh := Shader.new() sh.code = SHIMMER_CODE var m := ShaderMaterial.new() m.shader = sh _cache["_shimmer"] = m return _cache["_shimmer"] ## A ColorRect with the shimmer, filling `parent` (inset px from its edges). static func add_shimmer(parent: Control, inset: float = 0.0) -> Control: if not v3() or parent == null or parent.get_node_or_null("Shimmer") != null: return null var cr := ColorRect.new() cr.name = "Shimmer" cr.color = Color.WHITE cr.material = shimmer_mat() cr.mouse_filter = Control.MOUSE_FILTER_IGNORE cr.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) cr.offset_left = inset cr.offset_top = inset cr.offset_right = -inset cr.offset_bottom = -inset parent.add_child(cr) return cr static func font(weight: String = "Regular") -> Font: return Kanit.font(weight) # ======================================================================== theme static func skin_theme(theme: Theme) -> void: if GfxLook.legacy(): return var flat = theme.get_stylebox("panel", "PanelContainer") var tip = theme.get_stylebox("panel", "TooltipPanel") var names := ["normal", "hover", "pressed", "disabled"] var flats: Array = [] for n in names: flats.append(theme.get_stylebox(n, "Button")) var base: Color = (flats[0] as StyleBoxFlat).bg_color if flats[0] is StyleBoxFlat else Color("#58b84c") if not v3(): theme.set_stylebox("panel", "PanelContainer", parchment(flat as StyleBoxFlat)) theme.set_stylebox("panel", "TooltipPanel", parchment(tip as StyleBoxFlat, true)) var tex := buttons(base, flats) for i in names.size(): theme.set_stylebox(names[i], "Button", tex[i]) return # Kanit everywhere (Thai + Latin), a touch smaller so the taller Thai line keeps the layouts theme.default_font = font("Regular") theme.set_font("font", "Button", font("Medium")) theme.set_font("font", "TooltipLabel", font("Regular")) theme.set_stylebox("panel", "PanelContainer", window_panel(flat as StyleBoxFlat)) theme.set_stylebox("panel", "TooltipPanel", tooltip_panel(tip as StyleBoxFlat)) var tex3 := buttons(base, flats) for i in names.size(): theme.set_stylebox(names[i], "Button", tex3[i]) theme.set_color("font_color", "TooltipLabel", TXT) # scroll bars: slim brown grabber on a faint groove for sb_type in ["VScrollBar", "HScrollBar"]: var track := _flat(Color(0.47, 0.35, 0.2, 0.13), Color(0, 0, 0, 0), 0, 5) track.set_content_margin_all(3) theme.set_stylebox("scroll", sb_type, track) theme.set_stylebox("scroll_focus", sb_type, track) var gr := _flat(Color("#b48d58"), Color("#8a6436"), 1, 5) gr.set_content_margin_all(3) theme.set_stylebox("grabber", sb_type, gr) var gh := _flat(Color("#c99f62"), Color("#8a6436"), 1, 5) gh.set_content_margin_all(3) theme.set_stylebox("grabber_highlight", sb_type, gh) theme.set_stylebox("grabber_pressed", sb_type, gh) # text fields: light recessed paper, gold rim when typing var le := _flat(Color("#fbf5e4"), Color("#cdb384"), 1, 8) le.content_margin_left = 10 le.content_margin_right = 10 le.content_margin_top = 4 le.content_margin_bottom = 4 theme.set_stylebox("normal", "LineEdit", le) var lf: StyleBoxFlat = le.duplicate() lf.border_color = Color("#d9a23c") lf.set_border_width_all(2) theme.set_stylebox("focus", "LineEdit", lf) theme.set_color("font_color", "LineEdit", TXT) theme.set_color("font_placeholder_color", "LineEdit", Color(TXT_SOFT, 0.8)) theme.set_color("caret_color", "LineEdit", TXT) # thin warm separators var sep := StyleBoxLine.new() sep.color = Color("#d6bf8f") sep.thickness = 2 sep.grow_begin = -6 sep.grow_end = -6 theme.set_stylebox("separator", "HSeparator", sep) var vsep := StyleBoxLine.new() vsep.color = Color("#d6bf8f") vsep.thickness = 2 vsep.vertical = true theme.set_stylebox("separator", "VSeparator", vsep) # ======================================================================== StyleBoxes ## v2 parchment panel (small = tooltip: thinner frame, no corner studs). static func parchment(flat: StyleBoxFlat, small: bool = false) -> StyleBox: if GfxLook.legacy(): return flat var key := "parch_%s" % small if not _cache.has(key): _cache[key] = _mk(key, func(): return _parch_image(small)) var sb = _box() sb.texture = _cache[key] var m := 22 if not small else 14 sb.texture_margin_left = m sb.texture_margin_right = m sb.texture_margin_top = m sb.texture_margin_bottom = m sb.expand_margin_left = 5 sb.expand_margin_right = 5 sb.expand_margin_top = 4 sb.expand_margin_bottom = 6 _copy_margins(sb, flat, 10.0) return sb ## v3 window page: dark wood frame, parchment with burnt edges, woven triangle bands top / bottom ## (the band tiles along the edge, so it never stretches). Margins copied from `flat` (layout kept). static func window_panel(flat: StyleBox = null) -> StyleBox: if not v3(): return parchment(flat as StyleBoxFlat) if flat is StyleBoxFlat else parchment(_flat(PARCH, FRAME, 3, 12)) if v4(): return Skin4.window_panel(flat) if not _cache.has("win3"): _cache["win3"] = _mk("win3", func(): return _win_image()) var sb = _box() sb.texture = _cache["win3"] sb.texture_margin_left = WIN_M sb.texture_margin_right = WIN_M sb.texture_margin_top = WIN_M sb.texture_margin_bottom = WIN_M sb.axis_stretch_horizontal = 2 sb.expand_margin_left = WIN_PAD sb.expand_margin_right = WIN_PAD sb.expand_margin_top = WIN_PAD - 1 sb.expand_margin_bottom = WIN_PAD + 2 _copy_margins(sb, flat, 12.0) return sb ## Tooltip card: the same page, lighter frame, no bands. static func tooltip_panel(flat: StyleBox = null) -> StyleBox: if not v3(): return parchment(flat as StyleBoxFlat, true) if flat is StyleBoxFlat else flat if v4(): return Skin4.tooltip_panel(flat) if not _cache.has("tip3"): _cache["tip3"] = _mk("tip3", func(): return _tip_image()) var sb = _box() sb.texture = _cache["tip3"] for side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]: sb.set_texture_margin(side, 16.0) sb.expand_margin_left = 4 sb.expand_margin_right = 4 sb.expand_margin_top = 3 sb.expand_margin_bottom = 5 _copy_margins(sb, flat, 10.0) return sb ## Recessed cream area inside a window (lists, stat blocks, the bag grid). tint: 0 normal, 1 lighter. static func inset(margin: float = 8.0, tint: int = 0) -> StyleBox: var key := "inset_%d" % tint if not v3(): return _flat(Color("#efe0bb") if tint == 0 else Color("#fbf3de"), Color(FRAME, 0.45), 1, 10) if v4(): return Skin4.inset(margin, tint) if not _cache.has(key): _cache[key] = _mk(key, func(): return _inset_image(tint)) var sb = _box() sb.texture = _cache[key] for side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]: sb.set_texture_margin(side, 10.0) sb.set_content_margin(side, margin) return sb ## `c` inside a recessed inset panel (v3); older looks: `c` itself. static func boxed(c: Control, margin: float = 8.0, tint: int = 0) -> Control: if not v3(): return c var p := PanelContainer.new() p.name = str(c.name) + "Box" if str(c.name) != "" else "Box" p.add_theme_stylebox_override("panel", inset(margin, tint)) p.size_flags_horizontal = c.size_flags_horizontal p.size_flags_vertical = c.size_flags_vertical p.add_child(c) return p ## List card (skill list, class slots, shop rows): light paper; on = gold selected card. static func card(on: bool = false, margin: float = 8.0) -> StyleBox: if not v3(): return _flat(Color("#f6d68a") if on else Color("#fbf3de"), Color(FRAME, 0.6), 2, 10) if v4(): return Skin4.card(on, margin) var key := "card_%s" % on if not _cache.has(key): _cache[key] = _mk(key, func(): return _tab_image(2 if on else 3, 44, 40, 9.0)) var sb = _box() sb.texture = _cache[key] for side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]: sb.set_texture_margin(side, 12.0) sb.set_content_margin(side, margin) return sb ## Dark HUD glass over the 3D view (chat log, quest tracker, loot strip, party): soft vertical fade, a ## faint gold hairline, rounded. Content margins like hud._dark(). static func glass(alpha: float = 0.55, margin: float = 7.0) -> StyleBox: if not v3(): var f := _flat(Color(0.08, 0.06, 0.05, alpha), Color(0, 0, 0, 0), 0, 8) f.set_content_margin_all(margin) return f var key := "glass_%d" % roundi(alpha * 100.0) if not _cache.has(key): _cache[key] = _mk(key, func(): return _glass_image(alpha)) var sb = _box() sb.texture = _cache[key] for side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]: sb.set_texture_margin(side, 12.0) sb.set_content_margin(side, margin) return sb ## Dark leather pill (title plates: minimap area name, BR plate, NPC name). medal = gold medallion left. static func pill(medal: bool = false) -> StyleBox: var key := "pill_%s" % medal if not _cache.has(key): _cache[key] = _mk(key, func(): return _pill_image(medal)) var sb = _box() sb.texture = _cache[key] sb.texture_margin_left = PILL_L if medal else 18 sb.texture_margin_right = 18 sb.texture_margin_top = 12 sb.texture_margin_bottom = 12 sb.axis_stretch_horizontal = 2 sb.content_margin_left = (PILL_L + 6) if medal else 16 sb.content_margin_right = 20 sb.content_margin_top = 3 sb.content_margin_bottom = 4 return sb ## Glossy buttons from a base colour: [normal, hover, pressed, disabled]. `flats` = the flat styles ## (content margins copied). v3 maps the three hud colours onto the Draconia-like green / gold / red. static func buttons(base: Color, flats: Array = []) -> Array: if GfxLook.legacy(): return flats if not v3(): return _buttons_v2(base, flats) if v4(): return Skin4.buttons(base, flats) var b := _v3_base(base) var out: Array = [] for i in 4: var key := "btn3_%s_%d" % [b.to_html(false), i] if not _cache.has(key): _cache[key] = _mk(key, func(): return _btn3_image(b, i)) var sb = _box() sb.texture = _cache[key] for side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]: sb.set_texture_margin(side, 11.0) # the texture holds a 3 px glow / shadow ring outside the button sb.expand_margin_left = 3 sb.expand_margin_right = 3 sb.expand_margin_top = 3 sb.expand_margin_bottom = 3 var f = flats[i] if i < flats.size() else null if f is StyleBox: _copy_margins(sb, f, 3.0) else: sb.content_margin_left = 12 sb.content_margin_right = 12 sb.content_margin_top = 3 sb.content_margin_bottom = 4 if i == 2: # pressed: the face moves down a pixel sb.content_margin_top += 1 sb.content_margin_bottom = maxf(0.0, sb.content_margin_bottom - 1) out.append(sb) return out ## Hud colour -> v3 button colour. static func _v3_base(base: Color) -> Color: if base.is_equal_approx(Color("#58b84c")): return GREEN if base.is_equal_approx(Color("#e0a030")): return GOLDBTN if base.is_equal_approx(Color("#e07030")): return RED return base ## Tab chip: [normal, hover, pressed] for a tab that is `on` (gold) or not (cream). static func tab_styles(on: bool, margin_x: float = 16.0, margin_y: float = 3.0) -> Array: if v4(): return Skin4.tab_styles(on, margin_x, margin_y) var out: Array = [] for i in 3: var kind := 1 if on else (4 if i > 0 else 0) var key := "tab_%d" % kind if not _cache.has(key): _cache[key] = _mk(key, func(): return _tab_image(kind, 44, 30, 7.0)) var sb = _box() sb.texture = _cache[key] for side in [SIDE_LEFT, SIDE_RIGHT]: sb.set_texture_margin(side, 15.0) for side in [SIDE_TOP, SIDE_BOTTOM]: sb.set_texture_margin(side, 9.0) sb.content_margin_left = margin_x sb.content_margin_right = margin_x sb.content_margin_top = margin_y sb.content_margin_bottom = margin_y + 1 out.append(sb) return out ## Styles a Button as a tab chip (dark brown text on cream / gold, no outline). static func style_tab(b: Button, on: bool, fs: int = 0) -> void: if not v3(): return var st := tab_styles(on) b.add_theme_stylebox_override("normal", st[0]) b.add_theme_stylebox_override("hover", st[1]) b.add_theme_stylebox_override("pressed", st[2]) b.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) for c in ["font_color", "font_hover_color", "font_pressed_color", "font_focus_color", "font_hover_pressed_color"]: b.add_theme_color_override(c, Color("#4a2f18") if on else TXT) b.add_theme_constant_override("outline_size", 0) b.add_theme_font_override("font", font("Medium") if on else font("Regular")) if v4(): Skin4.tab_font(b, on) if fs > 0: b.add_theme_font_size_override("font_size", fs) ## Side tab of a window's tab column (settings): dark leather with gold text; the open one gold. static func style_side_tab(b: Button, on: bool) -> void: if not v3(): return if v4(): # v4: the same glossy green (open) / cream tabs as the top tabs, left aligned var st := tab_styles(on, 18.0, 5.0) b.add_theme_stylebox_override("normal", st[0]) b.add_theme_stylebox_override("hover", st[1]) b.add_theme_stylebox_override("pressed", st[2]) b.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) Skin4.tab_font(b, on) b.add_theme_font_override("font", font("SemiBold") if on else font("Medium")) return var n: StyleBox var h: StyleBox if on: n = tab_styles(true, 18.0, 4.0)[0] h = n else: n = pill(false) n.content_margin_left = 18 n.content_margin_right = 12 h = n.duplicate() h.set("modulate_color", Color(1.3, 1.2, 1.1)) b.add_theme_stylebox_override("normal", n) b.add_theme_stylebox_override("hover", h) b.add_theme_stylebox_override("pressed", h) b.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) for c in ["font_color", "font_hover_color", "font_pressed_color", "font_focus_color"]: b.add_theme_color_override(c, Color("#4a2f18") if on else Color("#f6dc98")) b.add_theme_constant_override("outline_size", 0) b.add_theme_font_override("font", font("SemiBold") if on else font("Medium")) ## Round close button: [normal, hover, pressed]. static func close_styles() -> Array: if v4(): return Skin4.close_styles() var out: Array = [] for i in 3: var key := "close_%d" % i if not _cache.has(key): _cache[key] = _mk(key, func(): return _close_image(i)) var sb = _box() sb.texture = _cache[key] sb.set_content_margin_all(0) sb.expand_margin_left = 2 sb.expand_margin_right = 2 sb.expand_margin_top = 2 sb.expand_margin_bottom = 2 out.append(sb) return out ## Progress bar [background, fill] for colour `col`: dark rounded groove with a gold-brown rim, glossy ## gradient fill. static func bar_styles(col: Color, h: int = 16) -> Array: var bg := _flat(Color(0.16, 0.1, 0.06, 0.72), Color(0.62, 0.46, 0.26, 0.9), 1, maxi(3, h / 2)) bg.set_content_margin_all(0) if not v3(): var fg := _flat(col, Color(0, 0, 0, 0), 0, 4) return [bg, fg] var key := "bar_%s" % col.to_html(false) if not _cache.has(key): _cache[key] = _mk(key, func(): return _bar_image(col)) var sb = _box() sb.texture = _cache[key] sb.texture_margin_left = 4 sb.texture_margin_right = 4 sb.expand_margin_top = -1 sb.expand_margin_bottom = -1 sb.expand_margin_left = -1 sb.expand_margin_right = -1 return [bg, sb] ## Item slot plate tinted by the rarity / type colour `col` (pastel fill, patterned, darker rim, a light ## corner curl) - the icon is drawn on it. static func slot_box(col: Color, px: float = 64.0) -> StyleBox: if v4(): return Skin4.slot_box(px, col) var key := "slot_%s" % col.to_html(false) if not _cache.has(key): _cache[key] = _mk(key, func(): return _slot_image(col)) var sb = _box() sb.texture = _cache[key] var m := clampf(px * 0.22, 5.0, 16.0) for side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]: sb.set_texture_margin(side, m) sb.set_content_margin(side, maxf(1.0, px * 0.06)) return sb ## Empty bag / equipment cell: a soft beige hollow. static func empty_slot() -> StyleBox: if not v3(): return _flat(Color(0.12, 0.09, 0.06, 0.55), Color(0.55, 0.42, 0.26, 0.5), 1, 11) if v4(): return Skin4.empty_slot() if not _cache.has("slot_empty"): _cache["slot_empty"] = _mk("slot_empty", func(): return _empty_slot_image()) var sb = _box() sb.texture = _cache["slot_empty"] for side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]: sb.set_texture_margin(side, 12.0) return sb ## Round ring styles (hotbar slot / AUTO / small round buttons): kind "hot" [normal, hover, pressed], ## "auto_off" / "auto_on" (one style each, in a 1-array), "gear". static func ring(kind: String) -> Array: if v4() and kind in ["hot", "gear"] and Skin4.has("ring_hot_0"): return Skin4.rings() if v4() and kind in ["auto_off", "auto_on"] and Skin4.has("ring_" + kind): var sb = Skin4.box("ring_" + kind, 22.0, 0.0, 3.0) sb.set("whole", true) return [sb] var out: Array = [] var states := 3 if kind in ["hot", "gear"] else 1 for i in states: var key := "ring_%s_%d" % [kind, i] if not _cache.has(key): _cache[key] = _mk(key, func(): return _ring_image(kind, i)) var sb = _box() sb.texture = _cache[key] var m := 22.0 if kind != "gear" else 12.0 for side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]: sb.set_texture_margin(side, m) sb.set_content_margin(side, 3.0) # a ring stays round at any size sb.whole = true out.append(sb) return out ## Coloured header banner of a tooltip / detail card (rarity gradient, light rim, sparkle dots). static func banner(col: Color) -> StyleBox: if v4() and Skin4.has("banner"): return Skin4.banner(col) var key := "banner_%s" % col.to_html(false) if not _cache.has(key): _cache[key] = _mk(key, func(): return _banner_image(col)) var sb = _box() sb.texture = _cache[key] for side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]: sb.set_texture_margin(side, 14.0) sb.content_margin_left = 10 sb.content_margin_right = 10 sb.content_margin_top = 7 sb.content_margin_bottom = 7 return sb ## Small pill badge (refine "+7", "Lv 10", "NEW"): solid colour, light rim. static func badge(col: Color) -> StyleBox: var sb := _flat(col, col.lightened(0.45), 1, 7) sb.content_margin_left = 5 sb.content_margin_right = 5 sb.content_margin_top = 0 sb.content_margin_bottom = 1 sb.shadow_color = Color(0, 0, 0, 0.3) sb.shadow_size = 2 sb.shadow_offset = Vector2(0, 1) return sb ## v2 window title plaque (hud._window): dark glossy wood with a gold rim; margins from the flat style. static func plaque(flat: StyleBoxFlat) -> StyleBox: if GfxLook.legacy(): return flat var key := "plaque_%s" % flat.bg_color.to_html(false) if not _cache.has(key): _cache[key] = _mk(key, func(): return _btn_image(flat.bg_color, false, false, flat.border_color)) var sb = _box() sb.texture = _cache[key] for side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]: sb.set_texture_margin(side, 9.0) _copy_margins(sb, flat, 3.0) return sb # ======================================================================== labels ## Kanit label (weight Regular / Medium / SemiBold / Bold). static func label(text: String, size: int, col: Color, weight: String = "Regular", outline: int = 0, outline_col: Color = Color(0.16, 0.08, 0.03, 0.9)) -> Label: var l := Label.new() l.text = text l.add_theme_font_override("font", font(weight)) l.add_theme_font_size_override("font_size", size) l.add_theme_color_override("font_color", col) if outline > 0: l.add_theme_color_override("font_outline_color", outline_col) l.add_theme_constant_override("outline_size", outline) return l ## Big gold number / title (BR, window headers): warm gold, brown outline, soft drop shadow. static func gold_title(text: String, size: int) -> Label: var l := label(text, size, Color("#ffd35c"), "Bold", maxi(4, size / 5), Color("#5a2c0c")) l.add_theme_color_override("font_shadow_color", Color(0.1, 0.04, 0.0, 0.45)) l.add_theme_constant_override("shadow_offset_x", 0) l.add_theme_constant_override("shadow_offset_y", 2) return l # ======================================================================== windows ## hud._window() look: an empty plaque row, the title in a leather pill with a gold medallion (still the ## drag handle, a spacer after it drags too), the round close button; open animation on show. static func decorate_window(hud, p: Control, plaque: PanelContainer, bar: HBoxContainer, title: Label, x: Button) -> void: if not v3(): return var ps := StyleBoxEmpty.new() ps.content_margin_bottom = 2 plaque.add_theme_stylebox_override("panel", ps) if v4(): var wkey := "" if hud != null and hud.get("windows") is Dictionary: for k in hud.windows: if hud.windows[k] == p: wkey = str(k) Skin4.style_title(title, wkey) else: style_title(title) title.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN title.custom_minimum_size = Vector2(240, 36) var sp := Control.new() sp.name = "TitleSpacer" sp.size_flags_horizontal = Control.SIZE_EXPAND_FILL sp.mouse_filter = Control.MOUSE_FILTER_STOP sp.mouse_default_cursor_shape = Control.CURSOR_MOVE if hud and hud.has_method("_drag_window"): sp.gui_input.connect(hud._drag_window.bind(p)) bar.add_child(sp) bar.move_child(sp, title.get_index() + 1) style_close(x) # the close button fades the window out (hud's own "hide" lambda goes; other listeners - the click # sound - stay) for c in x.pressed.get_connections(): var cb: Callable = c["callable"] if hud != null and cb.get_object() == hud: x.pressed.disconnect(cb) x.pressed.connect(close_window.bind(p)) hook_anim(p) ## Title label in the leather pill. static func style_title(title: Label, size: int = 18) -> void: if v4(): Skin4.style_title(title, str(title.get_meta("ui4_key", "")), size + 4) return title.add_theme_stylebox_override("normal", pill(true)) title.add_theme_font_override("font", font("SemiBold")) title.add_theme_font_size_override("font_size", size) title.add_theme_color_override("font_color", TITLE_TXT) title.add_theme_color_override("font_shadow_color", Color(0.08, 0.03, 0.0, 0.6)) title.add_theme_constant_override("shadow_offset_y", 1) title.add_theme_constant_override("outline_size", 0) title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER ## Round orange close button with a gold X (drawn in the texture). static func style_close(x: Button, d: float = 34.0) -> void: var st := close_styles() x.add_theme_stylebox_override("normal", st[0]) x.add_theme_stylebox_override("hover", st[1]) x.add_theme_stylebox_override("pressed", st[2]) x.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) x.text = "" x.custom_minimum_size = Vector2(d, d) x.size_flags_vertical = Control.SIZE_SHRINK_CENTER ## Open animation (scale 0.94 -> 1, fade in, 0.16 s) every time `p` is shown. static func hook_anim(p: Control) -> void: if p.has_meta("ui3_anim"): return p.set_meta("ui3_anim", true) p.visibility_changed.connect(func(): if p.visible and not p.has_meta("ui3_closing"): open_anim(p)) static func open_anim(p: Control) -> void: if not animate() or not p.is_inside_tree(): p.modulate.a = 1.0 p.scale = Vector2.ONE return var tw0 = (p.get_meta("ui3_tw") if p.has_meta("ui3_tw") else null) if tw0 is Tween and (tw0 as Tween).is_valid(): (tw0 as Tween).kill() # pivot at the centre once the (container) size is known p.pivot_offset = p.size * 0.5 p.scale = Vector2(0.94, 0.94) p.modulate.a = 0.0 var tw := p.create_tween().set_parallel(true) tw.tween_property(p, "scale", Vector2.ONE, 0.16).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) tw.tween_property(p, "modulate:a", 1.0, 0.12) p.set_meta("ui3_tw", tw) # a deferred pivot fix: the first open of a window has no size yet (func(): if is_instance_valid(p): p.pivot_offset = p.size * 0.5).call_deferred() ## Close with the reverse animation (0.12 s), then hide. static func close_window(p: Control) -> void: if not p.visible: return if not animate() or not p.is_inside_tree(): p.visible = false return var tw0 = (p.get_meta("ui3_tw") if p.has_meta("ui3_tw") else null) if tw0 is Tween and (tw0 as Tween).is_valid(): (tw0 as Tween).kill() p.pivot_offset = p.size * 0.5 p.set_meta("ui3_closing", true) var tw := p.create_tween().set_parallel(true) tw.tween_property(p, "scale", Vector2(0.95, 0.95), 0.12).set_ease(Tween.EASE_IN) tw.tween_property(p, "modulate:a", 0.0, 0.12) tw.chain().tween_callback(func(): p.visible = false p.scale = Vector2.ONE p.modulate.a = 1.0 p.remove_meta("ui3_closing")) p.set_meta("ui3_tw", tw) ## Show / hide with the animation. static func set_open(p: Control, on: bool) -> void: if on: if p.has_meta("ui3_closing"): var tw0 = (p.get_meta("ui3_tw") if p.has_meta("ui3_tw") else null) if tw0 is Tween and (tw0 as Tween).is_valid(): (tw0 as Tween).kill() p.remove_meta("ui3_closing") p.visible = true open_anim(p) else: p.visible = true elif v3(): close_window(p) else: p.visible = false # ======================================================================== image painters # Small images painted once (cached ImageTextures); everything scales through 9-slice StyleBoxTextures. const WIN_M := 30 const WIN_P := 16 const WIN_PAD := 6 const PILL_L := 44 static func _flat(bg: Color, border: Color = Color(0, 0, 0, 0), bw: int = 0, radius: int = 6) -> StyleBoxFlat: var sb := StyleBoxFlat.new() sb.bg_color = bg sb.border_color = border sb.set_border_width_all(bw) sb.set_corner_radius_all(radius) sb.set_content_margin_all(8) sb.anti_aliasing = true return sb static func _copy_margins(sb: StyleBox, flat: StyleBox, fallback: float) -> void: for side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]: var v := flat.get_content_margin(side) if flat != null else -1.0 sb.set_content_margin(side, v if v >= 0.0 else fallback) ## Signed distance to a rounded rectangle (centre c, half size h, radius r): < 0 inside. static func _sd(p: Vector2, c: Vector2, h: Vector2, r: float) -> float: var q := (p - c).abs() - h + Vector2(r, r) return Vector2(maxf(q.x, 0.0), maxf(q.y, 0.0)).length() + minf(maxf(q.x, q.y), 0.0) - r ## Distance from p to the segment a-b. static func _seg(p: Vector2, a: Vector2, b: Vector2) -> float: var ab := b - a var t := clampf((p - a).dot(ab) / maxf(ab.length_squared(), 0.0001), 0.0, 1.0) return p.distance_to(a + ab * t) static func _over(dst: Color, src: Color) -> Color: if src.a <= 0.0: return dst return dst.blend(src) static func _with_a(c: Color, a: float) -> Color: return Color(c.r, c.g, c.b, clampf(a, 0.0, 1.0)) ## The window page (see window_panel()). static func _win_image() -> Image: var W := WIN_M * 2 + WIN_P var H := WIN_M * 2 + 8 var img := Image.create(W * S, H * S, false, Image.FORMAT_RGBA8) var pad := float(WIN_PAD) var x0 := pad var y0 := pad - 1.0 var x1 := W - pad var y1 := H - pad - 2.0 var c := Vector2((x0 + x1) * 0.5, (y0 + y1) * 0.5) var h := Vector2((x1 - x0) * 0.5, (y1 - y0) * 0.5) var r := 11.0 var fw := 5.0 for y in H * S: for x in W * S: var p := Vector2((x + 0.5) / S, (y + 0.5) / S) var d := _sd(p, c, h, r) var ds := _sd(p - Vector2(0, 3.0), c, h, r) var col := Color(0.1, 0.05, 0.02, 0.42 * (1.0 - smoothstep(-3.0, float(WIN_PAD) + 1.0, ds))) var cov := clampf(0.5 - d * S, 0.0, 1.0) if cov > 0.0: var fill: Color if d > -fw: # dark wood frame, lit from above, a dark lip outside and inside var lit := clampf(1.0 - (p.y - y0) / 14.0, 0.0, 1.0) fill = WOOD.lerp(WOOD_HI, 0.55 * lit) var t := -d / fw fill = fill.lerp(WOOD.lightened(0.12), 0.35 * (1.0 - absf(t - 0.5) * 2.0)) if d > -1.0 or d < -fw + 1.0: fill = WOOD_DK else: var e := -d - fw fill = PAGE # burnt / aged page edge, a light rim right inside the frame fill = fill.lerp(PAGE_EDGE, 0.6 * (1.0 - smoothstep(0.0, 9.0, e))) if e < 1.3: fill = fill.lerp(Color("#fff5da"), 0.55) # woven band: a row of small triangles on a line, top (pointing down) + bottom (up) var side_k := smoothstep(10.0, 22.0, minf(p.x - x0, x1 - p.x)) var top := p.y - (y0 + fw) var bot := (y1 - fw) - p.y for band in [[top, 1.0], [bot, -1.0]]: var yb: float = band[0] if yb > 5.0 and yb < 17.0 and side_k > 0.0: var u := fposmod(p.x - x0, float(WIN_P)) / float(WIN_P) var a := 0.0 if yb < 7.0: a = 0.85 else: var v := (yb - 7.0) / 8.0 var wdt := 1.0 - v var tri := absf(u - 0.5) * 2.0 if tri < wdt: a = 0.7 if tri < wdt - 0.18 else 1.0 # a small diamond between the triangles var du := absf(u) if u < 0.5 else absf(1.0 - u) if absf(yb - 10.0) + du * float(WIN_P) < 2.2: a = 0.9 fill = fill.lerp(BAND if a < 0.95 else BAND.darkened(0.12), a * side_k) fill.a = 1.0 col = _over(col, _with_a(fill, cov)) if cov < 1.0 else fill img.set_pixel(x, y, col) return img static func _tip_image() -> Image: var W := 48 var img := Image.create(W * S, W * S, false, Image.FORMAT_RGBA8) var c := Vector2(W / 2.0, W / 2.0 - 1.0) var h := Vector2(W / 2.0 - 4.0, W / 2.0 - 4.5) for y in W * S: for x in W * S: var p := Vector2((x + 0.5) / S, (y + 0.5) / S) var d := _sd(p, c, h, 9.0) var ds := _sd(p - Vector2(0, 2.0), c, h, 9.0) var col := Color(0.1, 0.05, 0.02, 0.35 * (1.0 - smoothstep(-2.0, 5.0, ds))) var cov := clampf(0.5 - d * S, 0.0, 1.0) if cov > 0.0: var fill := PAGE.lightened(0.03) fill = fill.lerp(PAGE_EDGE, 0.45 * (1.0 - smoothstep(0.0, 7.0, -d - 2.0))) if d > -2.2: fill = Color("#6b4630").lerp(Color("#9c7148"), clampf(1.0 - (p.y - 4.0) / 16.0, 0.0, 1.0)) elif d > -3.4: fill = fill.lerp(Color("#fff3d2"), 0.7) fill.a = 1.0 col = _over(col, _with_a(fill, cov)) if cov < 1.0 else fill img.set_pixel(x, y, col) return img static func _inset_image(tint: int) -> Image: var W := 32 var img := Image.create(W * S, W * S, false, Image.FORMAT_RGBA8) var c := Vector2(W / 2.0, W / 2.0) var h := Vector2(W / 2.0 - 0.5, W / 2.0 - 0.5) var base := INSET if tint == 0 else Color("#faf2dc") for y in W * S: for x in W * S: var p := Vector2((x + 0.5) / S, (y + 0.5) / S) var d := _sd(p, c, h, 8.0) var cov := clampf(0.5 - d * S, 0.0, 1.0) if cov <= 0.0: img.set_pixel(x, y, Color(0, 0, 0, 0)) continue var fill := base # recessed: a soft shade under the top edge fill = fill.darkened(0.07 * (1.0 - smoothstep(0.0, 5.0, p.y - 1.0))) if d > -1.2: fill = INSET_LINE if tint == 0 else Color("#e2d0a6") elif d > -2.2 and p.y > c.y: fill = fill.lerp(Color(1, 1, 1), 0.35) fill.a = cov img.set_pixel(x, y, fill) return img ## kind: 0 tab, 1 tab on (gold), 2 card on (gold), 3 card, 4 tab hover. static func _tab_image(kind: int, W: int, H: int, r: float) -> Image: var img := Image.create(W * S, H * S, false, Image.FORMAT_RGBA8) var c := Vector2(W / 2.0, H / 2.0) var h := Vector2(W / 2.0 - 0.5, H / 2.0 - 0.5) var gold := kind == 1 or kind == 2 var top := TAB_ON.lightened(0.18) if gold else (TAB.lightened(0.05) if kind != 4 else Color("#fbe9b8")) var bot := TAB_ON.darkened(0.06) if gold else (TAB.darkened(0.04) if kind != 4 else Color("#f3d994")) if kind == 3: top = Color("#fbf4e0") bot = Color("#f3e8cb") var line := TAB_ON_LINE if gold else (TAB_LINE if kind != 3 else Color("#dccaa0")) var stud := Color("#a8741f") if gold else Color("#c6a66c") for y in H * S: for x in W * S: var p := Vector2((x + 0.5) / S, (y + 0.5) / S) var d := _sd(p, c, h, r) var cov := clampf(0.5 - d * S, 0.0, 1.0) if cov <= 0.0: img.set_pixel(x, y, Color(0, 0, 0, 0)) continue var fill := top.lerp(bot, clampf(p.y / H, 0.0, 1.0)) if d > -1.3: fill = line elif d > -2.4 and p.y < c.y: fill = fill.lerp(Color(1, 1, 1), 0.45 if gold else 0.3) elif gold and d > -3.0: fill = fill.lerp(Color("#ffe9a8"), 0.3) # diamond studs left / right (tabs only) if kind in [0, 1, 4]: for sx in [6.5, W - 6.5]: var dd := absf(p.x - sx) + absf(p.y - c.y) if dd < 3.0: fill = stud.lightened(0.25) if p.y < c.y else stud elif dd < 3.9: fill = fill.lerp(stud.darkened(0.3), 0.5) fill.a = cov img.set_pixel(x, y, fill) return img static func _glass_image(alpha: float) -> Image: var W := 32 var img := Image.create(W * S, W * S, false, Image.FORMAT_RGBA8) var c := Vector2(W / 2.0, W / 2.0) var h := Vector2(W / 2.0 - 0.5, W / 2.0 - 0.5) for y in W * S: for x in W * S: var p := Vector2((x + 0.5) / S, (y + 0.5) / S) var d := _sd(p, c, h, 10.0) var cov := clampf(0.5 - d * S, 0.0, 1.0) if cov <= 0.0: img.set_pixel(x, y, Color(0, 0, 0, 0)) continue var t := clampf(p.y / W, 0.0, 1.0) var fill := Color(0.09, 0.06, 0.04, alpha * lerpf(1.0, 0.82, t)) if d > -1.2: fill = Color(0.9, 0.76, 0.45, 0.42 if p.y < c.y else 0.22) elif d > -2.2 and p.y < 6.0: fill = Color(1.0, 0.9, 0.62, 0.18) fill.a *= cov img.set_pixel(x, y, fill) return img ## Leather pill (+ gold medallion on the left): dark gradient, dashed stitching top and bottom. static func _pill_image(medal: bool) -> Image: var L := PILL_L if medal else 18 var W := L + 8 + 18 var H := 34 var img := Image.create(W * S, H * S, false, Image.FORMAT_RGBA8) var bx0 := 6.0 if medal else 0.5 var c := Vector2((bx0 + W - 0.5) * 0.5, H / 2.0) var h := Vector2((W - 0.5 - bx0) * 0.5, H / 2.0 - 2.0) var mc := Vector2(20.0, H / 2.0) var mr := 15.5 for y in H * S: for x in W * S: var p := Vector2((x + 0.5) / S, (y + 0.5) / S) var col := Color(0, 0, 0, 0) # soft shadow under the pill var ds := _sd(p - Vector2(0, 1.5), c, h, h.y) col = _over(col, Color(0.05, 0.02, 0.0, 0.35 * (1.0 - smoothstep(-1.0, 3.0, ds)))) var d := _sd(p, c, h, h.y) var cov := clampf(0.5 - d * S, 0.0, 1.0) if cov > 0.0: var t := clampf((p.y - 2.0) / (H - 4.0), 0.0, 1.0) var fill := LEATHER.lerp(LEATHER_DK, t) if d > -1.2: fill = Color("#24130a") elif d > -2.4 and p.y < c.y: fill = fill.lerp(Color("#9a6e4c"), 0.55) # dashed stitching 5 px inside, top and bottom (period 8) var sy := [7.0, H - 7.0] for yy in sy: if absf(p.y - yy) < 0.75 and fposmod(p.x, 8.0) < 4.5 and p.x > (40.0 if medal else 12.0) and p.x < W - 12.0: fill = fill.lerp(STITCH, 0.8) fill.a = 1.0 col = _over(col, _with_a(fill, cov)) if medal: var md := p.distance_to(mc) # medallion shadow col = _over(col, Color(0, 0, 0, 0.35 * (1.0 - smoothstep(mr - 1.0, mr + 2.5, p.distance_to(mc + Vector2(0, 1.5)))))) var mcov := clampf((mr - md) * S + 0.5, 0.0, 1.0) if mcov > 0.0: var q := (p - mc) / mr var g := Color("#fff0b8").lerp(Color("#c1913c"), clampf(0.55 + q.y * 0.5 + q.length() * 0.3, 0.0, 1.0)) if md > mr - 1.3: g = Color("#5a3810") elif md > mr - 3.2: g = Color("#f6d67e").lerp(Color("#a57428"), clampf(0.5 + q.y * 0.6, 0.0, 1.0)) elif md > mr - 4.2: g = Color("#7a5116") else: # emblem: an eight-spoke compass star in dark gold var a := absf(fposmod(atan2(q.y, q.x) + PI / 8.0, PI / 4.0) - PI / 8.0) var star := md < (mr - 5.0) * (1.0 - a * 1.6) if star: g = Color("#8a5a18").lerp(Color("#b8842c"), clampf(-q.y, 0.0, 1.0)) if md < 2.4: g = Color("#fff4c8") g.a = mcov col = _over(col, g) img.set_pixel(x, y, col) return img ## Buttons v3 (state 0 normal, 1 hover, 2 pressed, 3 disabled): 3 px glow / shadow ring outside. static func _btn3_image(base: Color, state: int) -> Image: var W := 40 var img := Image.create(W * S, W * S, false, Image.FORMAT_RGBA8) var pad := 3.0 var c := Vector2(W / 2.0, W / 2.0) var h := Vector2(W / 2.0 - pad, W / 2.0 - pad) var r := 8.0 var b := base if state == 1: b = base.lightened(0.1) elif state == 2: b = base.darkened(0.12) elif state == 3: b = Color("#b9ad96") var top := b.lightened(0.3) if state != 2 else b.darkened(0.04) var bot := b.darkened(0.16) if state != 2 else b.lightened(0.05) var rim := b.darkened(0.5) if state != 3 else Color("#8f8470") for y in W * S: for x in W * S: var p := Vector2((x + 0.5) / S, (y + 0.5) / S) var d := _sd(p, c, h, r) var col := Color(0, 0, 0, 0) if state == 1: # hover: warm gold glow around the button col = Color(1.0, 0.86, 0.45, 0.55 * (1.0 - smoothstep(-1.0, pad + 0.5, d))) elif state != 2: var ds := _sd(p - Vector2(0, 1.6), c, h, r) col = Color(0.12, 0.06, 0.02, 0.35 * (1.0 - smoothstep(-1.0, 2.2, ds))) var cov := clampf(0.5 - d * S, 0.0, 1.0) if cov > 0.0: var t := clampf((p.y - pad) / (W - pad * 2.0), 0.0, 1.0) var fill := top.lerp(bot, t) if state != 2 and state != 3 and t < 0.46: fill = fill.lerp(Color.WHITE, 0.1) if d > -1.3: fill = rim elif d > -2.5 and t < 0.5 and state != 2: fill = fill.lerp(Color.WHITE, 0.45) elif d > -2.5 and t > 0.55: fill = fill.darkened(0.12) fill.a = 1.0 col = _over(col, _with_a(fill, cov)) img.set_pixel(x, y, col) return img ## Round close button: dark lip, gold ring, orange face, gold X (0 normal, 1 hover, 2 pressed). static func _close_image(state: int) -> Image: var W := 38 var img := Image.create(W * S, W * S, false, Image.FORMAT_RGBA8) var c := Vector2(W / 2.0, W / 2.0) var R := 16.5 var face_t := Color("#f2a55a") if state != 2 else Color("#c96c30") var face_b := Color("#b4531f") if state != 2 else Color("#99421a") if state == 1: face_t = face_t.lightened(0.12) face_b = face_b.lightened(0.1) for y in W * S: for x in W * S: var p := Vector2((x + 0.5) / S, (y + 0.5) / S) var d := p.distance_to(c) var col := Color(0, 0, 0, 0) if state == 1: col = Color(1.0, 0.85, 0.45, 0.5 * (1.0 - smoothstep(R - 1.0, R + 2.5, d))) else: col = Color(0.1, 0.04, 0.0, 0.4 * (1.0 - smoothstep(R - 1.0, R + 2.0, p.distance_to(c + Vector2(0, 1.5))))) var cov := clampf((R - d) * S + 0.5, 0.0, 1.0) if cov > 0.0: var q := (p - c) / R var fill: Color if d > R - 1.3: fill = Color("#4a1f0a") elif d > R - 4.0: fill = Color("#fbe19a").lerp(Color("#a8782c"), clampf(0.5 + q.y * 0.7, 0.0, 1.0)) elif d > R - 5.0: fill = Color("#6a300e") else: fill = face_t.lerp(face_b, clampf(0.45 + q.y * 0.6, 0.0, 1.0)) if q.y < -0.25 and d < R - 6.0: fill = fill.lerp(Color.WHITE, 0.12) # the X: gold bars with a dark edge var k := R * 0.36 var sx := minf(_seg(p, c + Vector2(-k, -k), c + Vector2(k, k)), _seg(p, c + Vector2(k, -k), c + Vector2(-k, k))) if sx < 2.6: fill = Color("#fff0b0").lerp(Color("#e7b64a"), clampf(0.5 + q.y, 0.0, 1.0)) elif sx < 3.6: fill = fill.lerp(Color("#6a2a0a"), 0.8) fill.a = 1.0 col = _over(col, _with_a(fill, cov)) img.set_pixel(x, y, col) return img static func _bar_image(col: Color) -> Image: var W := 12 var H := 16 var img := Image.create(W * S, H * S, false, Image.FORMAT_RGBA8) for y in H * S: for x in W * S: var t := (y + 0.5) / (H * S) var fill := col.lightened(0.28).lerp(col.darkened(0.2), t) if t < 0.42: fill = fill.lerp(Color.WHITE, 0.16) if y < S: fill = fill.lerp(Color.WHITE, 0.35) # rounded ends var ex := minf(x + 0.5, W * S - x - 0.5) / S var a := 1.0 if ex < 3.0: var ey := absf(t - 0.5) * 2.0 a = clampf((ex / 3.0) * 1.6 - ey * ey * 0.8 + 0.2, 0.0, 1.0) fill.a = a img.set_pixel(x, y, fill) return img ## Item slot plate in `col` (pastel), with a faint diamond pattern and a light corner curl. static func _slot_image(col: Color) -> Image: var W := 64 var img := Image.create(W * S, W * S, false, Image.FORMAT_RGBA8) var c := Vector2(W / 2.0, W / 2.0) var h := Vector2(W / 2.0 - 0.5, W / 2.0 - 0.5) var r := 11.0 var light := col.lerp(Color.WHITE, 0.58) var mid := col.lerp(Color.WHITE, 0.32) var rim := col.darkened(0.18) for y in W * S: for x in W * S: var p := Vector2((x + 0.5) / S, (y + 0.5) / S) var d := _sd(p, c, h, r) var cov := clampf(0.5 - d * S, 0.0, 1.0) if cov <= 0.0: img.set_pixel(x, y, Color(0, 0, 0, 0)) continue var t := clampf(p.y / W, 0.0, 1.0) var fill := light.lerp(mid, t) # soft radial light in the middle (the icon sits on a glow) fill = fill.lerp(Color.WHITE, 0.18 * (1.0 - smoothstep(0.0, W * 0.5, p.distance_to(c)))) # faint diamond lattice var u := fposmod(p.x + p.y, 12.0) var v := fposmod(p.x - p.y, 12.0) if u < 1.0 or v < 1.0: fill = fill.lerp(mid, 0.35) if d > -2.0: fill = rim elif d > -3.2: fill = fill.lerp(Color.WHITE, 0.5) # corner curl (top left): a light arc var cd := p.distance_to(Vector2(3.5, 3.5)) if absf(cd - 11.0) < 1.1 and p.x < 14.0 and p.y < 14.0 and d < -2.0: fill = fill.lerp(Color.WHITE, 0.75) elif cd < 9.5 and d < -2.0 and p.x + p.y < 14.0: fill = fill.lerp(Color.WHITE, 0.3) fill.a = cov img.set_pixel(x, y, fill) return img static func _empty_slot_image() -> Image: var W := 32 var img := Image.create(W * S, W * S, false, Image.FORMAT_RGBA8) var c := Vector2(W / 2.0, W / 2.0) var h := Vector2(W / 2.0 - 0.5, W / 2.0 - 0.5) for y in W * S: for x in W * S: var p := Vector2((x + 0.5) / S, (y + 0.5) / S) var d := _sd(p, c, h, 9.0) var cov := clampf(0.5 - d * S, 0.0, 1.0) if cov <= 0.0: img.set_pixel(x, y, Color(0, 0, 0, 0)) continue var fill := Color("#e6d6b0").lerp(Color("#ecdfbd"), clampf(p.y / W, 0.0, 1.0)) fill = fill.darkened(0.06 * (1.0 - smoothstep(0.0, 4.0, p.y - 1.0))) if d > -1.2: fill = Color("#d2bd8e") fill.a = cov img.set_pixel(x, y, fill) return img ## Round rings: "hot" (hotbar slot 64 px: gold ring, dark glass centre), "auto_off" / "auto_on" (84 px), ## "gear" (32 px small round button). static func _ring_image(kind: String, state: int) -> Image: var W := 64 if kind == "hot" else (84 if kind.begins_with("auto") else 32) var img := Image.create(W * S, W * S, false, Image.FORMAT_RGBA8) var c := Vector2(W / 2.0, W / 2.0) var R := W / 2.0 - 1.5 var ring_w := 4.0 if kind != "gear" else 2.6 var on := kind == "auto_on" for y in W * S: for x in W * S: var p := Vector2((x + 0.5) / S, (y + 0.5) / S) var d := p.distance_to(c) var q := (p - c) / R var col := Color(0.05, 0.02, 0.0, 0.35 * (1.0 - smoothstep(R - 1.0, R + 1.5, p.distance_to(c + Vector2(0, 1.2))))) if state == 1 or on: col = _over(col, Color(1.0, 0.85, 0.4, (0.55 if on else 0.4) * (1.0 - smoothstep(R - 2.0, R + 1.5, d)))) var cov := clampf((R - d) * S + 0.5, 0.0, 1.0) if cov > 0.0: var fill: Color if d > R - 1.1: fill = Color("#3a220c") elif d > R - 1.1 - ring_w: var lit := clampf(0.5 - q.y * 0.6 - q.x * 0.15, 0.0, 1.0) fill = Color("#9a6a24").lerp(Color("#ffe7a0"), lit) if state == 1: fill = fill.lightened(0.15) elif d > R - 2.1 - ring_w: fill = Color("#4a2c10") else: if on: fill = Color("#ffcf5a").lerp(Color("#e0801c"), clampf(0.45 + q.y * 0.6 + q.length() * 0.2, 0.0, 1.0)) if q.y < -0.2: fill = fill.lerp(Color.WHITE, 0.18 * (1.0 - smoothstep(-0.2, -0.9, q.y) * 0.0)) else: var a := 0.66 if state != 2 else 0.8 var g := Color(0.16, 0.11, 0.07).lerp(Color(0.05, 0.035, 0.025), clampf(q.length(), 0.0, 1.0)) if state == 2: g = g.lerp(Color(0.5, 0.35, 0.12), 0.35) fill = Color(g.r, g.g, g.b, a) # a faint inner ring line if absf(d - (R - ring_w - 6.0)) < 0.6 and kind != "gear": fill = _over(fill, Color(0.95, 0.8, 0.45, 0.25)) # ornament studs on the AUTO ring if kind.begins_with("auto") and d > R - ring_w - 9.0 and d < R - ring_w - 2.5: var ang := fposmod(atan2(q.y, q.x) + PI / 8.0, PI / 4.0) - PI / 8.0 if absf(ang) * d < 1.6: fill = _over(fill, Color(1.0, 0.86, 0.5, 0.75 if not on else 0.9)) fill.a = fill.a * cov if fill.a < 1.0 else cov col = _over(col, fill) img.set_pixel(x, y, col) return img static func _banner_image(col: Color) -> Image: var W := 48 var H := 40 var img := Image.create(W * S, H * S, false, Image.FORMAT_RGBA8) var c := Vector2(W / 2.0, H / 2.0) var h := Vector2(W / 2.0 - 0.5, H / 2.0 - 0.5) var top := col.lightened(0.18) var bot := col.darkened(0.25) for y in H * S: for x in W * S: var p := Vector2((x + 0.5) / S, (y + 0.5) / S) var d := _sd(p, c, h, 9.0) var cov := clampf(0.5 - d * S, 0.0, 1.0) if cov <= 0.0: img.set_pixel(x, y, Color(0, 0, 0, 0)) continue var fill := top.lerp(bot, clampf(p.y / H, 0.0, 1.0)) if d > -1.3: fill = Color("#f3d27a") elif d > -2.6: fill = col.darkened(0.45) elif d > -3.6 and p.y < c.y: fill = fill.lerp(Color.WHITE, 0.3) fill.a = cov img.set_pixel(x, y, fill) return img # ---- v2 painters (kept for "--ui2") static func _buttons_v2(base: Color, flats: Array) -> Array: var cols := [base, base.lightened(0.14), base.darkened(0.18), Color(0.72, 0.66, 0.56)] var out: Array = [] for i in 4: var key := "btn_%s_%d" % [cols[i].to_html(false), i] if not _cache.has(key): _cache[key] = _mk(key, func(): return _btn_image(cols[i], i == 2, i == 3)) var sb = _box() sb.texture = _cache[key] for side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]: sb.set_texture_margin(side, 9.0) var f = flats[i] if i < flats.size() else null if f is StyleBox: _copy_margins(sb, f, 3.0) else: sb.content_margin_left = 9 sb.content_margin_right = 9 sb.content_margin_top = 3 sb.content_margin_bottom = 3 out.append(sb) return out static func _parch_image(small: bool) -> Image: var W := 80 if not small else 56 var img := Image.create(W * S, W * S, false, Image.FORMAT_RGBA8) var c := Vector2(W / 2.0, W / 2.0 - 1.0) var h := Vector2(W / 2.0 - 5.0, W / 2.0 - 5.0) var r := 12.0 if not small else 7.0 var fw := 3.0 if not small else 2.0 var noise := FastNoiseLite.new() noise.seed = 7 noise.frequency = 0.09 var top := PARCH.lightened(0.07) var bot := PARCH.darkened(0.05) var edge := Color("#d9bf8c") for y in W * S: for x in W * S: var p := Vector2((x + 0.5) / S, (y + 0.5) / S) var d := _sd(p, c, h, r) var ds := _sd(p - Vector2(0, 2.0), c, h, r) var sh := 0.32 * (1.0 - smoothstep(-2.0, 5.0, ds)) var col := Color(0.2, 0.1, 0.03, sh) var cov := clampf(0.5 - d * S, 0.0, 1.0) if cov > 0.0: var t := clampf((p.y - (c.y - h.y)) / (2.0 * h.y), 0.0, 1.0) var fill := top.lerp(bot, t) var n := noise.get_noise_2d(x, y * 0.6) * 0.025 fill = Color(fill.r + n, fill.g + n, fill.b + n * 0.6) fill = fill.lerp(edge, 0.35 * (1.0 - smoothstep(-13.0, -fw - 1.0, d))) if d > -fw: var lit := 0.5 - 0.5 * clampf((p.y - c.y) / h.y, -1.0, 1.0) fill = FRAME.darkened(0.25).lerp(FRAME.lightened(0.22), lit) if d > -0.9: fill = FRAME.darkened(0.45) elif d > -fw - 1.6 and d < -fw - 0.4: fill = fill.lerp(GOLD, 0.75) if not small: for sx in [-1.0, 1.0]: for sy in [-1.0, 1.0]: var sc := c + Vector2(sx * (h.x - 9.0), sy * (h.y - 9.0)) var dd := absf(p.x - sc.x) + absf(p.y - sc.y) if dd < 3.6: fill = GOLD.lightened(0.25) if (p.y < sc.y) else GOLD.darkened(0.2) elif dd < 4.6: fill = fill.lerp(FRAME.darkened(0.3), 0.8) fill.a = 1.0 col = Color(col.r, col.g, col.b, col.a * (1.0 - cov)).blend(Color(fill.r, fill.g, fill.b, cov)) if cov < 1.0 else fill img.set_pixel(x, y, col) return img static func _btn_image(base: Color, pressed: bool, disabled: bool, rim: Color = Color(0, 0, 0, 0)) -> Image: var W := 32 var img := Image.create(W * S, W * S, false, Image.FORMAT_RGBA8) var c := Vector2(W / 2.0, W / 2.0) var h := Vector2(W / 2.0 - 0.5, W / 2.0 - 0.5) var r := 8.0 var top := base.lightened(0.28) if not pressed else base.darkened(0.08) var bot := base.darkened(0.14) if not pressed else base.lightened(0.06) var border := base.darkened(0.5) if rim.a <= 0.0 else rim for y in W * S: for x in W * S: var p := Vector2((x + 0.5) / S, (y + 0.5) / S) var d := _sd(p, c, h, r) var cov := clampf(0.5 - d * S, 0.0, 1.0) if cov <= 0.0: img.set_pixel(x, y, Color(0, 0, 0, 0)) continue var t := clampf(p.y / W, 0.0, 1.0) var fill := top.lerp(bot, t) if not pressed and not disabled and p.y < W * 0.46: fill = fill.lerp(Color.WHITE, 0.14) if d > -2.0: fill = border elif d > -3.0 and p.y < c.y and not pressed: fill = fill.lerp(Color.WHITE, 0.4) fill.a = cov img.set_pixel(x, y, fill) return img