- 桥梁与静态物体高度采样修复: - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程 - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题 - 新增 test_bridge_height_parity.gd 自动化对拍测试 - 40250 怪物击杀经验动效: - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附 - 40250 客户端全系统功能对齐(Batches 1-31): - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试 - 文档沉淀: - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
508 lines
19 KiB
GDScript
508 lines
19 KiB
GDScript
# UiBuild (P1) —— uiscript 嵌套 Dictionary → Godot Control 树。
|
||
#
|
||
# var spec := UiScript.new().parse_file(path)
|
||
# var r := UiBuild.build(spec, assets_root)
|
||
# add_child(r.root)
|
||
# r.nodes["accept"].pressed.connect(...) # 按 uiscript 里的 "name" 取节点
|
||
#
|
||
# 覆盖 uiscript 最常用的 ~18 种 type;其它降级为 Control / Button / TextureRect。
|
||
# board/thinboard 的九宫格用 ui_kit(真实 ymir 贴图,缺失则 StyleBoxFlat 兜底)。
|
||
extends RefCounted
|
||
|
||
const UiKit = preload("res://ui_kit.gd")
|
||
const UiAssets = preload("res://ui/ui_assets.gd")
|
||
|
||
# spec: 顶层 window dict。返回 { root: Control, nodes: {name: Control} }
|
||
static func build(spec: Dictionary, assets_root: String) -> Dictionary:
|
||
var ctx := {"assets": assets_root, "nodes": {}}
|
||
var root := _make(spec, ctx, Vector2i.ZERO)
|
||
if root == null:
|
||
root = Control.new()
|
||
return {"root": root, "nodes": ctx["nodes"]}
|
||
|
||
# --- recursion ---------------------------------------------------------
|
||
|
||
static func _i(spec: Dictionary, key: String, def := 0) -> int:
|
||
var v = spec.get(key, def)
|
||
return int(v) if (v is int or v is float or v is bool or v is String) else def
|
||
|
||
static func _str(v) -> String:
|
||
if v == null:
|
||
return ""
|
||
return v if v is String else str(v)
|
||
|
||
static func _make(spec: Dictionary, ctx: Dictionary, parent_size: Vector2i) -> Control:
|
||
var type := _str(spec.get("type", "window"))
|
||
var w := _i(spec, "width", 0)
|
||
var h := _i(spec, "height", 0)
|
||
if type == "titlebar" and h == 0:
|
||
h = 23
|
||
elif type == "horizontalbar" and h == 0:
|
||
h = 17
|
||
elif (type == "grid_table" or type == "slot") and (w == 0 or h == 0):
|
||
if w == 0 and spec.has("x_count"):
|
||
var xc := maxi(1, _i(spec, "x_count", 1))
|
||
var xs := _i(spec, "x_step", 32)
|
||
var xb := _i(spec, "x_blank", 0)
|
||
w = xc * (xs + xb)
|
||
if h == 0 and spec.has("y_count"):
|
||
var yc := maxi(1, _i(spec, "y_count", 1))
|
||
var ys := _i(spec, "y_step", 32)
|
||
var yb := _i(spec, "y_blank", 0)
|
||
h = yc * (ys + yb)
|
||
var node := _make_by_type(type, spec, ctx, Vector2i(w, h))
|
||
if node == null:
|
||
node = Control.new()
|
||
|
||
node.name = _safe_name(_str(spec.get("name", type)))
|
||
var halign := _str(spec.get("text_horizontal_align", ""))
|
||
var valign := _str(spec.get("text_vertical_align", ""))
|
||
var allalign := _str(spec.get("all_align", ""))
|
||
if type == "text":
|
||
if allalign == "center":
|
||
if w == 0: w = parent_size.x
|
||
if h == 0: h = parent_size.y
|
||
else:
|
||
if halign == "center" and w == 0:
|
||
w = parent_size.x if parent_size.x > 0 else 100
|
||
if (valign == "center" or _str(spec.get("vertical_align", "")) == "center") and h == 0:
|
||
h = parent_size.y if parent_size.y > 0 else 17
|
||
if (w == 0 or h == 0) and node is TextureRect and (node as TextureRect).texture != null:
|
||
var tex: Texture2D = (node as TextureRect).texture
|
||
if w == 0: w = tex.get_width()
|
||
if h == 0: h = tex.get_height()
|
||
elif (w == 0 or h == 0) and node is Button:
|
||
var btn := node as Button
|
||
var nrm_sb = btn.get_theme_stylebox("normal")
|
||
if nrm_sb is StyleBoxTexture and (nrm_sb as StyleBoxTexture).texture != null:
|
||
var tex: Texture2D = (nrm_sb as StyleBoxTexture).texture
|
||
if w == 0: w = tex.get_width()
|
||
if h == 0: h = tex.get_height()
|
||
|
||
if w > 0:
|
||
node.custom_minimum_size = Vector2(w, h)
|
||
node.size = Vector2(w, h)
|
||
|
||
# 位置:x/y + horizontal_align / vertical_align(相对父)
|
||
var x := _i(spec, "x", 0)
|
||
var y := _i(spec, "y", 0)
|
||
if type == "text" and halign == "center" and _i(spec, "width", 0) == 0:
|
||
x = 0
|
||
match _str(spec.get("horizontal_align", "left")):
|
||
"center": x = (parent_size.x - w) / 2 + x
|
||
"right": x = parent_size.x - w - x
|
||
match _str(spec.get("vertical_align", "top")):
|
||
"center":
|
||
if type == "text" and (valign == "center" or _str(spec.get("vertical_align", "")) == "center") and h == parent_size.y:
|
||
pass
|
||
else:
|
||
y = (parent_size.y - h) / 2 + y
|
||
"bottom": y = parent_size.y - h - y
|
||
node.position = Vector2(x, y)
|
||
|
||
var nm := _str(spec.get("name", ""))
|
||
if nm != "":
|
||
ctx["nodes"][nm] = node
|
||
|
||
var self_size := Vector2i(w, h) if (w > 0 and h > 0) else parent_size
|
||
for child in spec.get("children", []):
|
||
if child is Dictionary:
|
||
var c := _make(child, ctx, self_size)
|
||
if c:
|
||
node.add_child(c)
|
||
return node
|
||
|
||
static func _make_by_type(type: String, spec: Dictionary, ctx: Dictionary, sz: Vector2i) -> Control:
|
||
var assets: String = ctx["assets"]
|
||
match type:
|
||
"window", "box":
|
||
var c := Control.new()
|
||
c.mouse_filter = Control.MOUSE_FILTER_PASS
|
||
return c
|
||
"board", "thinboard", "board_with_titlebar":
|
||
var p := _board(assets, "thinboard" if type == "thinboard" else "board", sz)
|
||
if type == "board_with_titlebar" and spec.has("title"):
|
||
var tl := Label.new()
|
||
tl.name = "TitleText"
|
||
tl.text = _str(spec.get("title", ""))
|
||
tl.position = Vector2(12, 6)
|
||
p.add_child(tl)
|
||
p.set_meta("titlebar_h", 24)
|
||
return p
|
||
"titlebar":
|
||
return _titlebar(assets, sz.x, spec)
|
||
"text":
|
||
var lbl := Label.new()
|
||
lbl.text = _str(spec.get("text", ""))
|
||
if spec.has("text_color"):
|
||
lbl.add_theme_color_override("font_color", _argb(int(spec["text_color"])))
|
||
elif spec.has("color"):
|
||
lbl.add_theme_color_override("font_color", _argb(int(spec["color"])))
|
||
elif spec.has("r") and spec.has("g") and spec.has("b"):
|
||
var r := float(spec["r"])
|
||
var g := float(spec["g"])
|
||
var b := float(spec["b"])
|
||
var a := float(spec.get("a", 1.0))
|
||
lbl.add_theme_color_override("font_color", Color(r, g, b, a))
|
||
else:
|
||
lbl.add_theme_color_override("font_color", Color(0.8549, 0.8549, 0.8549))
|
||
|
||
var font_size := 12
|
||
var fs_spec := _str(spec.get("fontsize", "")).to_upper()
|
||
var fn_spec := _str(spec.get("fontname", "")).to_upper()
|
||
if fs_spec == "LARGE" or "LARGE" in fn_spec:
|
||
font_size = 14
|
||
elif fs_spec == "SMALL" or "SMALL" in fn_spec:
|
||
font_size = 9
|
||
lbl.add_theme_font_size_override("font_size", font_size)
|
||
|
||
var halign := _str(spec.get("text_horizontal_align", ""))
|
||
var valign := _str(spec.get("text_vertical_align", ""))
|
||
if _str(spec.get("all_align", "")) == "center":
|
||
halign = "center"
|
||
valign = "center"
|
||
match halign:
|
||
"center": lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
"right": lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||
_: lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||
match valign:
|
||
"center": lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
"bottom": lbl.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
||
_: lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
return lbl
|
||
"button", "toggle_button", "radio_button":
|
||
var b := Button.new()
|
||
b.text = _str(spec.get("text", ""))
|
||
b.toggle_mode = (type == "toggle_button" or type == "radio_button")
|
||
if spec.has("text_color"):
|
||
var col := _argb(int(spec["text_color"]))
|
||
b.add_theme_color_override("font_color", col)
|
||
b.add_theme_color_override("font_pressed_color", col)
|
||
b.add_theme_color_override("font_hover_color", col)
|
||
b.add_theme_color_override("font_focus_color", col)
|
||
elif spec.has("color"):
|
||
var col := _argb(int(spec["color"]))
|
||
b.add_theme_color_override("font_color", col)
|
||
b.add_theme_color_override("font_pressed_color", col)
|
||
b.add_theme_color_override("font_hover_color", col)
|
||
b.add_theme_color_override("font_focus_color", col)
|
||
b.vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
b.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
b.alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
b.clip_text = true
|
||
|
||
var btn_fsize := 12
|
||
var fs_spec := _str(spec.get("fontsize", "")).to_upper()
|
||
var fn_spec := _str(spec.get("fontname", "")).to_upper()
|
||
if fs_spec == "LARGE" or "LARGE" in fn_spec:
|
||
btn_fsize = 14
|
||
elif fs_spec == "SMALL" or "SMALL" in fn_spec:
|
||
btn_fsize = 9
|
||
b.add_theme_font_size_override("font_size", btn_fsize)
|
||
|
||
var nrm := UiAssets.load_tex(assets, _str(spec.get("default_image", "")))
|
||
var ovr := UiAssets.load_tex(assets, _str(spec.get("over_image", "")))
|
||
var dwn := UiAssets.load_tex(assets, _str(spec.get("down_image", "")))
|
||
var empty := StyleBoxEmpty.new()
|
||
empty.content_margin_left = 0
|
||
empty.content_margin_right = 0
|
||
empty.content_margin_top = 0
|
||
empty.content_margin_bottom = 0
|
||
if nrm:
|
||
b.add_theme_stylebox_override("normal", _sb_tex(nrm))
|
||
b.add_theme_stylebox_override("hover", _sb_tex(ovr if ovr else nrm))
|
||
b.add_theme_stylebox_override("pressed", _sb_tex(dwn if dwn else nrm))
|
||
b.add_theme_stylebox_override("disabled", _sb_tex(nrm))
|
||
b.add_theme_stylebox_override("focus", empty)
|
||
var tex_w := nrm.get_width()
|
||
var tex_h := nrm.get_height()
|
||
b.custom_minimum_size = Vector2(tex_w, tex_h)
|
||
b.size = Vector2(tex_w, tex_h)
|
||
# 40250 对齐:进入节点树时重设尺寸,防止被 Godot 默认最小高度撑大到 31px
|
||
b.tree_entered.connect(func():
|
||
b.size = b.custom_minimum_size
|
||
)
|
||
if btn_fsize + 6 > tex_h:
|
||
btn_fsize = maxi(8, tex_h - 6)
|
||
b.add_theme_font_size_override("font_size", btn_fsize)
|
||
else:
|
||
b.add_theme_stylebox_override("normal", empty)
|
||
b.add_theme_stylebox_override("hover", empty)
|
||
b.add_theme_stylebox_override("pressed", empty)
|
||
b.add_theme_stylebox_override("focus", empty)
|
||
b.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
return b
|
||
"image", "expanded_image", "ani_image", "mark":
|
||
var ir := TextureRect.new()
|
||
ir.texture = UiAssets.load_tex(assets, _str(spec.get("image", "")))
|
||
ir.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
ir.stretch_mode = TextureRect.STRETCH_SCALE if type == "expanded_image" \
|
||
else TextureRect.STRETCH_KEEP
|
||
ir.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
return ir
|
||
"editline":
|
||
var e := LineEdit.new()
|
||
e.text = _str(spec.get("text", ""))
|
||
e.secret = bool(spec.get("secret", false))
|
||
if spec.has("input_limit"):
|
||
e.max_length = int(spec["input_limit"])
|
||
return e
|
||
"line":
|
||
var ln := ColorRect.new()
|
||
ln.color = Color(1, 1, 1, 0.15)
|
||
if _i(spec, "height", 0) == 0:
|
||
ln.custom_minimum_size = Vector2(_i(spec, "width", 100), 1)
|
||
return ln
|
||
"listbox", "candidate_list":
|
||
return ItemList.new()
|
||
"gauge", "bar":
|
||
var pb := ProgressBar.new()
|
||
pb.show_percentage = false
|
||
pb.value = 100.0
|
||
return pb
|
||
"horizontalbar":
|
||
return _horizontalbar(assets, _i(spec, "width", 100))
|
||
"grid_table", "slotbar", "slot":
|
||
# Metin2 物品格容器:有 start_index/x_count 或显式 "slot" 元组 -> 建子格;
|
||
# 否则 grid_table 退化成普通 GridContainer。
|
||
var cells := _slot_cells(spec)
|
||
if cells.is_empty() and type == "grid_table":
|
||
var g := GridContainer.new()
|
||
g.columns = maxi(1, _i(spec, "x_count", _i(spec, "column", 1)))
|
||
return g
|
||
var s := Control.new()
|
||
s.set_meta("is_slot_container", true)
|
||
var base_img := _str(spec.get("image", ""))
|
||
var base_tex: Texture2D = UiAssets.load_tex(assets, base_img) if base_img != "" else null
|
||
for cd in cells:
|
||
var cell := Panel.new()
|
||
cell.name = "slot_%d" % int(cd["index"])
|
||
cell.position = Vector2(cd["x"], cd["y"])
|
||
cell.custom_minimum_size = Vector2(cd["w"], cd["h"])
|
||
cell.size = Vector2(cd["w"], cd["h"])
|
||
cell.set_meta("slot_index", int(cd["index"]))
|
||
cell.mouse_filter = Control.MOUSE_FILTER_STOP
|
||
if base_tex != null:
|
||
cell.add_theme_stylebox_override("panel", _sb_tex(base_tex))
|
||
s.add_child(cell)
|
||
return s
|
||
"sliderbar":
|
||
var slider := HSlider.new()
|
||
# 40250 sliderbar.sub (175×12), sliderbar_cursor.sub (53×12)
|
||
slider.custom_minimum_size = Vector2(175, 12)
|
||
slider.size = Vector2(175, 12)
|
||
slider.min_value = 0.0
|
||
slider.max_value = 1.0
|
||
slider.step = 0.01
|
||
var track_tex := UiAssets.load_tex(assets, "d:/ymir work/ui/game/windows/sliderbar.sub")
|
||
var grab_tex := UiAssets.load_tex(assets, "d:/ymir work/ui/game/windows/sliderbar_cursor.sub")
|
||
if track_tex:
|
||
var sb_track := StyleBoxTexture.new()
|
||
sb_track.texture = track_tex
|
||
sb_track.content_margin_left = 0
|
||
sb_track.content_margin_right = 0
|
||
sb_track.content_margin_top = 0
|
||
sb_track.content_margin_bottom = 0
|
||
slider.add_theme_stylebox_override("slider", sb_track)
|
||
if grab_tex:
|
||
slider.add_theme_icon_override("grabber", grab_tex)
|
||
slider.add_theme_icon_override("grabber_highlight", grab_tex)
|
||
slider.add_theme_icon_override("grabber_disabled", grab_tex)
|
||
return slider
|
||
"scrollbar", "thin_scrollbar":
|
||
return VScrollBar.new()
|
||
_:
|
||
return Control.new()
|
||
|
||
# --- helpers ---------------------------------------------------------
|
||
|
||
static func _board(assets: String, prefix: String, sz: Vector2i) -> Panel:
|
||
var p := Panel.new()
|
||
if sz.x > 0 and sz.y > 0:
|
||
p.custom_minimum_size = Vector2(sz)
|
||
p.size = Vector2(sz)
|
||
var np: NinePatchRect = null
|
||
if assets != "":
|
||
var m := 16 if prefix == "thinboard" else 32
|
||
var base_sz := 32 if prefix == "thinboard" else 128
|
||
np = UiKit.board(assets, prefix, m, base_sz)
|
||
if np and np.texture:
|
||
np.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
p.add_child(np)
|
||
np.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
p.get_child(0).show_behind_parent = true
|
||
p.add_theme_stylebox_override("panel", StyleBoxEmpty.new())
|
||
else:
|
||
var sb := StyleBoxFlat.new()
|
||
sb.bg_color = Color(0.10, 0.11, 0.14, 0.92)
|
||
sb.border_color = Color(0.35, 0.32, 0.26)
|
||
sb.set_border_width_all(1)
|
||
sb.set_corner_radius_all(3)
|
||
p.add_theme_stylebox_override("panel", sb)
|
||
return p
|
||
|
||
static func _titlebar(assets: String, width: int, spec: Dictionary) -> Control:
|
||
var t := Control.new()
|
||
var w := maxi(64, width)
|
||
t.custom_minimum_size = Vector2(w, 23)
|
||
t.size = Vector2(w, 23)
|
||
t.set_meta("is_titlebar", true)
|
||
|
||
# Background textures (left, center, right)
|
||
var left_tex := UiKit.ui_tex(assets, "titlebar_left")
|
||
var center_tex := UiKit.ui_tex(assets, "titlebar_center")
|
||
var right_tex := UiKit.ui_tex(assets, "titlebar_right")
|
||
if left_tex and center_tex and right_tex:
|
||
var img_left := TextureRect.new()
|
||
img_left.name = "TitleBarLeft"
|
||
img_left.texture = left_tex
|
||
img_left.position = Vector2.ZERO
|
||
img_left.size = Vector2(32, 23)
|
||
img_left.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
img_left.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
t.add_child(img_left)
|
||
|
||
var img_center := TextureRect.new()
|
||
img_center.name = "TitleBarCenter"
|
||
img_center.texture = center_tex
|
||
img_center.position = Vector2(32, 0)
|
||
img_center.size = Vector2(maxi(0, w - 64), 23)
|
||
img_center.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
img_center.stretch_mode = TextureRect.STRETCH_TILE
|
||
img_center.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
t.add_child(img_center)
|
||
|
||
var img_right := TextureRect.new()
|
||
img_right.name = "TitleBarRight"
|
||
img_right.texture = right_tex
|
||
img_right.position = Vector2(w - 32, 0)
|
||
img_right.size = Vector2(32, 23)
|
||
img_right.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
img_right.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
t.add_child(img_right)
|
||
else:
|
||
var p := Panel.new()
|
||
p.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
var sb := StyleBoxFlat.new()
|
||
sb.bg_color = Color(0.2, 0.2, 0.25, 0.9)
|
||
p.add_theme_stylebox_override("panel", sb)
|
||
p.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
t.add_child(p)
|
||
|
||
# Close Button
|
||
var btn_close := Button.new()
|
||
btn_close.name = "CloseButton"
|
||
var nrm := UiAssets.load_tex(assets, "d:/ymir work/ui/public/close_button_01.sub")
|
||
var ovr := UiAssets.load_tex(assets, "d:/ymir work/ui/public/close_button_02.sub")
|
||
var dwn := UiAssets.load_tex(assets, "d:/ymir work/ui/public/close_button_03.sub")
|
||
if nrm:
|
||
btn_close.add_theme_stylebox_override("normal", _sb_tex(nrm))
|
||
if ovr: btn_close.add_theme_stylebox_override("hover", _sb_tex(ovr))
|
||
if dwn: btn_close.add_theme_stylebox_override("pressed", _sb_tex(dwn))
|
||
var bw := nrm.get_width()
|
||
var bh := nrm.get_height()
|
||
btn_close.size = Vector2(bw, bh)
|
||
btn_close.position = Vector2(w - bw - 3, 3)
|
||
else:
|
||
btn_close.text = "×"
|
||
btn_close.size = Vector2(16, 16)
|
||
btn_close.position = Vector2(w - 19, 3)
|
||
btn_close.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
t.add_child(btn_close)
|
||
t.set_meta("close_button", btn_close)
|
||
return t
|
||
|
||
static func _horizontalbar(assets: String, width: int) -> Control:
|
||
var w := maxi(96, width)
|
||
var bar := Control.new()
|
||
bar.custom_minimum_size = Vector2(w, 17)
|
||
bar.size = Vector2(w, 17)
|
||
bar.mouse_filter = Control.MOUSE_FILTER_PASS
|
||
|
||
var left_tex := UiKit.ui_tex(assets, "horizontalbar_left")
|
||
var center_tex := UiKit.ui_tex(assets, "horizontalbar_center")
|
||
var right_tex := UiKit.ui_tex(assets, "horizontalbar_right")
|
||
|
||
if left_tex and center_tex and right_tex:
|
||
var img_left := TextureRect.new()
|
||
img_left.name = "HorizontalBarLeft"
|
||
img_left.texture = left_tex
|
||
img_left.position = Vector2.ZERO
|
||
img_left.size = Vector2(32, 17)
|
||
img_left.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
bar.add_child(img_left)
|
||
|
||
var img_center := TextureRect.new()
|
||
img_center.name = "HorizontalBarCenter"
|
||
img_center.texture = center_tex
|
||
img_center.position = Vector2(32, 0)
|
||
img_center.size = Vector2(maxi(0, w - 64), 17)
|
||
img_center.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
img_center.stretch_mode = TextureRect.STRETCH_TILE
|
||
img_center.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
bar.add_child(img_center)
|
||
|
||
var img_right := TextureRect.new()
|
||
img_right.name = "HorizontalBarRight"
|
||
img_right.texture = right_tex
|
||
img_right.position = Vector2(w - 32, 0)
|
||
img_right.size = Vector2(32, 17)
|
||
img_right.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
bar.add_child(img_right)
|
||
else:
|
||
var sb := StyleBoxFlat.new()
|
||
sb.bg_color = Color(0.2, 0.22, 0.25, 0.85)
|
||
var p := Panel.new()
|
||
p.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
p.add_theme_stylebox_override("panel", sb)
|
||
bar.add_child(p)
|
||
return bar
|
||
|
||
# slot 容器的子格:显式 "slot" 元组,或 start_index/x_count/y_count/x_step/y_step 网格。
|
||
static func _slot_cells(spec: Dictionary) -> Array:
|
||
var cells := []
|
||
if spec.get("slot", null) is Array:
|
||
for sd in spec["slot"]:
|
||
if sd is Dictionary:
|
||
cells.append({"index": _i(sd, "index"), "x": _i(sd, "x"), "y": _i(sd, "y"),
|
||
"w": _i(sd, "width", 32), "h": _i(sd, "height", 32)})
|
||
elif spec.has("x_count"):
|
||
var si := _i(spec, "start_index", 0)
|
||
var xc := maxi(1, _i(spec, "x_count", 1))
|
||
var yc := maxi(1, _i(spec, "y_count", 1))
|
||
var xs := _i(spec, "x_step", 32)
|
||
var ys := _i(spec, "y_step", 32)
|
||
var xb := _i(spec, "x_blank", 0)
|
||
var yb := _i(spec, "y_blank", 0)
|
||
for row in yc:
|
||
for col in xc:
|
||
cells.append({"index": si + row * xc + col, "x": col * (xs + xb), "y": row * (ys + yb),
|
||
"w": xs, "h": ys})
|
||
return cells
|
||
|
||
static func _sb_tex(t: Texture2D) -> StyleBoxTexture:
|
||
var sb := StyleBoxTexture.new()
|
||
sb.texture = t
|
||
sb.content_margin_left = 0
|
||
sb.content_margin_top = 0
|
||
sb.content_margin_right = 0
|
||
sb.content_margin_bottom = 0
|
||
return sb
|
||
|
||
static func _argb(v: int) -> Color:
|
||
# 0xAARRGGBB
|
||
var a := (v >> 24) & 0xff
|
||
var r := (v >> 16) & 0xff
|
||
var g := (v >> 8) & 0xff
|
||
var b := v & 0xff
|
||
if a == 0:
|
||
a = 255
|
||
return Color8(r, g, b, a)
|
||
|
||
static func _safe_name(s: String) -> String:
|
||
var out := s
|
||
for bad: String in [".", ":", "@", "/", "%", " "]:
|
||
out = out.replace(bad, "_")
|
||
return out if out != "" else "node"
|