feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复

- 桥梁与静态物体高度采样修复:
  - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
This commit is contained in:
shen
2026-09-19 08:51:25 -07:00
parent 1db9e9a129
commit 66d217b313
650 changed files with 53046 additions and 1586 deletions
+295 -21
View File
@@ -35,11 +35,50 @@ static func _make(spec: Dictionary, ctx: Dictionary, parent_size: Vector2i) -> C
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)
@@ -47,11 +86,17 @@ static func _make(spec: Dictionary, ctx: Dictionary, parent_size: Vector2i) -> C
# 位置: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": y = (parent_size.y - h) / 2 + y
"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)
@@ -59,7 +104,7 @@ static func _make(spec: Dictionary, ctx: Dictionary, parent_size: Vector2i) -> C
if nm != "":
ctx["nodes"][nm] = node
var self_size := Vector2i(w, h) if w > 0 else parent_size
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)
@@ -85,36 +130,109 @@ static func _make_by_type(type: String, spec: Dictionary, ctx: Dictionary, sz: V
p.set_meta("titlebar_h", 24)
return p
"titlebar":
var t := Panel.new()
t.set_meta("is_titlebar", true)
return t
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"])))
match _str(spec.get("text_horizontal_align", "left")):
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.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_: 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":
"button", "toggle_button", "radio_button":
var b := Button.new()
b.text = _str(spec.get("text", ""))
b.toggle_mode = (type == "toggle_button")
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))
if ovr: b.add_theme_stylebox_override("hover", _sb_tex(ovr))
if dwn: b.add_theme_stylebox_override("pressed", _sb_tex(dwn))
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
"radio_button":
var cb := CheckBox.new()
cb.text = _str(spec.get("text", ""))
return cb
"image", "expanded_image", "ani_image", "mark":
var ir := TextureRect.new()
ir.texture = UiAssets.load_tex(assets, _str(spec.get("image", "")))
@@ -138,11 +256,13 @@ static func _make_by_type(type: String, spec: Dictionary, ctx: Dictionary, sz: V
return ln
"listbox", "candidate_list":
return ItemList.new()
"gauge", "bar", "horizontalbar":
"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。
@@ -153,6 +273,8 @@ static func _make_by_type(type: String, spec: Dictionary, ctx: Dictionary, sz: V
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"])
@@ -161,10 +283,33 @@ static func _make_by_type(type: String, spec: Dictionary, ctx: Dictionary, sz: V
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":
return HSlider.new()
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()
_:
@@ -174,14 +319,20 @@ static func _make_by_type(type: String, spec: Dictionary, ctx: Dictionary, sz: V
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 != "":
np = UiKit.board(assets, prefix, 32, 128)
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.set_anchors_preset(Control.PRESET_FULL_RECT)
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)
@@ -191,6 +342,123 @@ static func _board(assets: String, prefix: String, sz: Vector2i) -> Panel:
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 := []
@@ -205,15 +473,21 @@ static func _slot_cells(spec: Dictionary) -> Array:
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, "y": row * ys,
"w": 32, "h": 32})
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: