Files
aetherbound-guild/runtime/ui/shared/asset_chrome.gd
T
wushenghua 5cf76a07b2 fix(m57-c): per-region readiness chrome and typography
Stop reusing one plaque for every region. The header is now a compartmented
band with dividers, icons and left-aligned values; the bottom is a carved dock
band with four independent command plates (icon above label, wider green
primary) instead of buttons inside a long frame; the warning stack is three
cards with severity icons; the summary is a parchment card with shape markers,
values and pip rows; and typography uses explicit tracking and embolden so
titles, values and captions read as different ranks. View contract names and
z-order are unchanged.
2026-09-13 15:25:55 +08:00

129 lines
5.3 KiB
GDScript

class_name ABGAssetChrome
extends RefCounted
## Shared asset nine-patch chrome for first-play pages.
##
## Large parchment/wood surfaces use the approved generated UI components with
## their decorative borders isolated as nine-patch corners. Small controls use
## the size-independent shared vector material (`market_chrome.gd`) so they
## never stretch ornament pixels. Text and values stay engine-rendered.
const MarketChrome = preload("res://ui/shared/market_chrome.gd")
const PAPER_PANEL = preload("res://assets/ui_m52_components/v2_trimmed/chrome/parchment_panel.png")
const ACTION_PLATE = preload("res://assets/ui_m52_components/v2_trimmed/chrome/action_button.png")
const NAME_PLATE = preload("res://assets/ui_m52_components/v2_trimmed/frames/name_plate.png")
const TITLE_PLAQUE = preload("res://assets/ui_m52_market/v2/title_plaque.png")
const WOOD_PLATE = preload("res://assets/ui_m52_market/v2/wood_button.png")
const INK := Color("#2f2417")
const INK_SOFT := Color("#5e4a32")
const IVORY := Color("#f6ecd4")
static var _scaled_cache := {}
## Release the downscaled textures held by the static cache. Pages must call
## this when they leave the tree so a strict run does not report leaked RIDs.
static func clear_cache() -> void:
_scaled_cache.clear()
## Plain parchment panel. Defaults keep the wood frame in the corners for a
## medium-sized region; callers scale down and adjust margins per region.
static func paper(node_name: String, area: Rect2, scale := 0.55, h_margin := 85.0, v_margin := 75.0) -> NinePatchRect:
return plate(node_name, area, PAPER_PANEL, scale, h_margin, v_margin)
## Wide wood rail (top rail, bottom rail). `name_plate` has a thin frame and a
## plain parchment center, so it stretches cleanly across a full canvas width.
static func rail(node_name: String, area: Rect2, scale := 0.5, h_margin := 105.0, v_margin := 45.0) -> NinePatchRect:
return plate(node_name, area, NAME_PLATE, scale, h_margin, v_margin)
static func plate(node_name: String, area: Rect2, source: Texture2D, scale: float, h_margin: float, v_margin: float) -> NinePatchRect:
var node := NinePatchRect.new()
node.name = node_name
node.texture = scaled(source, scale)
node.patch_margin_left = maxi(2, int(round(h_margin * scale)))
node.patch_margin_right = maxi(2, int(round(h_margin * scale)))
node.patch_margin_top = maxi(2, int(round(v_margin * scale)))
node.patch_margin_bottom = maxi(2, int(round(v_margin * scale)))
node.axis_stretch_horizontal = NinePatchRect.AXIS_STRETCH_MODE_STRETCH
node.axis_stretch_vertical = NinePatchRect.AXIS_STRETCH_MODE_STRETCH
node.draw_center = true
node.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
node.mouse_filter = Control.MOUSE_FILTER_IGNORE
place(node, area)
return node
## Size-independent shared vector material for small controls (name plates,
## command plates). `variant` is a `market_chrome.gd` variant name.
static func market(node_name: String, area: Rect2, variant: String, accent := "blue") -> Control:
var node = MarketChrome.new().configure(variant, accent)
node.name = node_name
node.resized.connect(node.queue_redraw)
place(node, area)
return node
static func scaled(source: Texture2D, scale: float) -> Texture2D:
if is_equal_approx(scale, 1.0):
return source
var key := "%s@%.3f" % [source.resource_path, scale]
var cached: Texture2D = _scaled_cache.get(key)
if cached != null:
return cached
var image := source.get_image()
image.resize(
maxi(2, int(round(float(image.get_width()) * scale))),
maxi(2, int(round(float(image.get_height()) * scale))),
Image.INTERPOLATE_LANCZOS
)
var texture := ImageTexture.create_from_image(image)
_scaled_cache[key] = texture
return texture
static func place(control: Control, area: Rect2) -> void:
control.anchor_left = area.position.x
control.anchor_top = area.position.y
control.anchor_right = area.end.x
control.anchor_bottom = area.end.y
control.offset_left = 0.0
control.offset_top = 0.0
control.offset_right = 0.0
control.offset_bottom = 0.0
## Typography: one base family with explicit tracking and weight so titles,
## values and captions read as different ranks instead of one flat label size.
static func styled_font(tracking := 0.0, embolden := 0.0) -> FontVariation:
var variation := FontVariation.new()
variation.base_font = preload("res://assets/fonts/NotoSansSC.ttf")
variation.spacing_glyph = int(round(tracking))
variation.variation_embolden = embolden
return variation
static func styled_label(value: String, actual_size: int, color := INK, tracking := 0.0, embolden := 0.0) -> Label:
var label := Label.new()
label.text = value
label.add_theme_font_override("font", styled_font(tracking, embolden))
label.add_theme_font_size_override("font_size", actual_size)
label.add_theme_color_override("font_color", color)
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
return label
static func ink_label(value: String, color := INK, design_size := 14.0) -> Label:
var label := Label.new()
label.text = value
label.add_theme_font_override("font", preload("res://assets/fonts/NotoSansSC.ttf"))
label.add_theme_font_size_override("font_size", int(design_size))
label.add_theme_color_override("font_color", color)
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
return label