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:
+75
-1
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user