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
+75 -1
View File
@@ -11,13 +11,16 @@ extends Node
const CursorManager = preload("res://ui/cursor_manager.gd")
const ItemTooltip = preload("res://ui/item_tooltip.gd")
const UiAssets = preload("res://ui/ui_assets.gd")
const SHOP_SLOT_COUNT := 40 # shop.SHOP_SLOT_COUNT (== SHOP_HOST_ITEM_MAX_NUM)
const ITEM_ANTIFLAG_SELL := 1 << 8
const ITEM_FLAG_COUNT_PER_1GOLD := 1 << 3
const USE_SHOP_LIMIT_RANGE_M := 10.0 # 40250 USE_SHOP_LIMIT_RANGE = 1000cm (uishop.py:483)
var client: Node
var proto: Node
var item_list # ItemListDB (RefCounted)
var _assets_root := ""
var _tooltip_builder: RefCounted
var _root: Control
var _tabbar: HBoxContainer
@@ -25,6 +28,8 @@ var _grid: GridContainer
var _err: Label
var _sell_quantity: SpinBox
var _active_tab := 0
var _open_char_pos := Vector3.ZERO
var _has_open_pos := false
var item_mouse: Node
var cursor_manager: Node
var audio: Node
@@ -42,13 +47,16 @@ func setup(m2client: Node, parent: Node, proto_node: Node = null, ilist: RefCoun
client = m2client
proto = proto_node
item_list = ilist
_assets_root = AssetRoot.path()
_tooltip_builder = ItemTooltip.new()
_tooltip_builder.setup(AssetRoot.path(), "en", proto)
_tooltip_builder.setup(_assets_root, "en", proto)
_build(parent)
if client.has_signal("shop_opened"):
client.shop_opened.connect(func(_v): open())
if client.has_signal("shop_closed"):
client.shop_closed.connect(_close)
if client.has_signal("shop_updated"):
client.shop_updated.connect(_on_shop_slot_updated)
if client.has_signal("shop_error"):
client.shop_error.connect(_on_error)
@@ -64,17 +72,63 @@ func get_mobile_window() -> Control:
return _root
func close() -> void:
if client and client.has_method("shop_close") and is_open():
client.shop_close()
_close()
func _get_main_char_pos() -> Variant:
if client == null:
return null
var vid := 0
if client.has_method("get_main_vid"):
vid = int(client.get_main_vid())
if vid != 0 and client.has_method("get_entity"):
var e: Dictionary = client.get_entity(vid)
if not e.is_empty() and e.has("pos"):
return e["pos"]
return null
func _process(_delta: float) -> void:
if not is_open() or not _has_open_pos:
return
var cur = _get_main_char_pos()
if cur is Vector3:
if _open_char_pos.distance_to(cur) > USE_SHOP_LIMIT_RANGE_M:
close()
func _icon(vnum: int) -> Texture2D:
if vnum <= 0:
return null
var rel := ""
if item_list and item_list.has_method("has") and item_list.has(vnum):
rel = item_list.icon(vnum)
elif item_list and item_list.has_method("icon"):
rel = item_list.icon(vnum)
if rel != "":
return UiAssets.load_tex(_assets_root, rel)
var exact_rel := "icon/item/%05d.tga" % vnum
var tex := UiAssets.load_tex(_assets_root, exact_rel)
if tex != null:
return tex
var fallback_rel := "icon/item/%05d.tga" % ((vnum / 10) * 10)
return UiAssets.load_tex(_assets_root, fallback_rel)
func open() -> void:
_root.visible = true
_err.text = ""
_sell_quantity.value = 1
_active_tab = 0
_set_mode(1)
var p = _get_main_char_pos()
if p is Vector3:
_open_char_pos = p
_has_open_pos = true
else:
_has_open_pos = false
refresh()
func _close() -> void:
_has_open_pos = false
if item_mouse and item_mouse.has_method("unregister_owner"):
item_mouse.unregister_owner(self)
if item_mouse.has_method("is_attached") and item_mouse.is_attached():
@@ -310,15 +364,35 @@ func _slot(it: Dictionary, pos: int) -> Button:
slot.text = _name_of(vnum).substr(0, 7)
if count > 1:
slot.text += " ×%d" % count
var tex := _icon(vnum)
if tex:
slot.icon = tex
slot.expand_icon = true
if _tooltip_builder:
var pd: Dictionary = proto.item(vnum) if proto and proto.has_method("item") else {}
slot.tooltip_text = _tooltip_builder.format(vnum, maxi(1, count), pd, it)
slot.tooltip_text += "\n价格:%d" % int(it.get("price", 0))
slot.pressed.connect(func() -> void: _buy_slot(pos, it))
slot.gui_input.connect(func(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
_direct_buy(pos, it)
)
else:
slot.disabled = true
return slot
func _direct_buy(pos: int, item: Dictionary) -> void:
if _mode != 1 or item.is_empty() or pos < 0:
return
if client and client.has_method("shop_buy"):
if not client.shop_buy(pos, 1):
_on_error("SEND_FAILED")
func _on_shop_slot_updated(_pos: int) -> void:
if not is_open():
return
refresh()
func _buy_slot(pos: int, item: Dictionary) -> void:
if _mode != 1:
_set_mode(1)