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
+55 -77
View File
@@ -25,6 +25,33 @@ var _hp_label: Label
var _hp_w := 240.0
var _energy_fill: ColorRect
var _energy_label: Label
var _status_box: VBoxContainer
var quickbar: Node:
set(v):
quickbar = v
if quickbar:
_sync_vitals_to_quickbar()
var _cached_hp: int = 0
var _cached_max_hp: int = 0
var _cached_sp: int = 0
var _cached_max_sp: int = 0
var _cached_st: int = 0
var _cached_max_st: int = 0
var _cached_exp: int = 0
var _cached_next_exp: int = 0
func _sync_vitals_to_quickbar() -> void:
if not quickbar:
return
if _cached_max_hp > 0 and quickbar.has_method("set_hp"):
quickbar.set_hp(_cached_hp, _cached_max_hp)
if _cached_max_sp > 0 and quickbar.has_method("set_sp"):
quickbar.set_sp(_cached_sp, _cached_max_sp)
if _cached_max_st > 0 and quickbar.has_method("set_st"):
quickbar.set_st(_cached_st, _cached_max_st)
if _cached_next_exp > 0 and quickbar.has_method("set_exp"):
quickbar.set_exp(_cached_exp, _cached_next_exp)
# 目标血条(P0
var _target_panel: Control
var _target_name: Label
@@ -191,84 +218,40 @@ func _build_minimap(nx: int, ny: int) -> void:
_minimap_rect.add_child(_dot)
func _build_status() -> void:
var box := VBoxContainer.new()
box.set_anchors_preset(Control.PRESET_BOTTOM_LEFT)
box.position = Vector2(18, -122)
box.add_theme_constant_override("separation", 6)
add_child(box)
for spec in [["gauge_red", 0.82], ["gauge_blue", 0.60]]:
var slot := _gauge("gauge_slot", 24)
slot.custom_minimum_size = Vector2(_hp_w, 24)
slot.size = Vector2(_hp_w, 24)
var fill := _gauge(spec[0], 24)
fill.position = Vector2(0, 0)
fill.size = Vector2(_hp_w * float(spec[1]), 24)
slot.add_child(fill)
box.add_child(slot)
if spec[0] == "gauge_red":
_hp_fill = fill
_hp_label = Label.new()
_hp_label.add_theme_font_size_override("font_size", 11)
_hp_label.position = Vector2(6, 5)
slot.add_child(_hp_label)
else:
_mp_fill = fill
# 体力条(40250 POINT_STAMINA,冲刺/骑乘时由 ServerCommand 驱动)。
var stamina_slot := ColorRect.new()
stamina_slot.color = Color(0, 0, 0, 0.55)
stamina_slot.custom_minimum_size = Vector2(_hp_w, 10)
stamina_slot.size = Vector2(_hp_w, 10)
_stamina_fill = ColorRect.new()
_stamina_fill.color = Color(0.25, 0.85, 0.35)
_stamina_fill.size = Vector2(0, 10)
stamina_slot.add_child(_stamina_fill)
_stamina_label = Label.new()
_stamina_label.add_theme_font_size_override("font_size", 9)
_stamina_label.position = Vector2(6, -1)
stamina_slot.add_child(_stamina_label)
box.add_child(stamina_slot)
# 能量条(原客户端 POINT_ENERGY,0..100)。资源缺失时仍用纯色条保持可读。
var energy_slot := ColorRect.new()
energy_slot.color = Color(0, 0, 0, 0.55)
energy_slot.custom_minimum_size = Vector2(_hp_w, 10)
energy_slot.size = Vector2(_hp_w, 10)
_energy_fill = ColorRect.new()
_energy_fill.color = Color(0.95, 0.65, 0.18)
_energy_fill.size = Vector2(0, 10)
energy_slot.add_child(_energy_fill)
_energy_label = Label.new()
_energy_label.add_theme_font_size_override("font_size", 9)
_energy_label.position = Vector2(6, -1)
energy_slot.add_child(_energy_label)
box.add_child(energy_slot)
# 经验条(细)+ 等级
var exp_slot := ColorRect.new()
exp_slot.color = Color(0, 0, 0, 0.5)
exp_slot.custom_minimum_size = Vector2(_hp_w, 6)
exp_slot.size = Vector2(_hp_w, 6)
_exp_fill = ColorRect.new()
_exp_fill.color = Color(0.9, 0.8, 0.2)
_exp_fill.size = Vector2(0, 6)
exp_slot.add_child(_exp_fill)
box.add_child(exp_slot)
_lvl_label = Label.new()
_lvl_label.add_theme_font_size_override("font_size", 12)
_lvl_label.text = "Lv 1"
box.add_child(_lvl_label)
# 40250 标准:血量、法力、体力、经验均由底部任务栏 Quickbar 的 Gauge_Board 与 EXP_Gauge_Board 承载,
# HUD 不再在左下角渲染浮动的原型状态条。
pass
# --- P0: live data setters -------------------------------------------------
func set_status_visible(v: bool) -> void:
if _status_box and is_instance_valid(_status_box):
_status_box.visible = v
func set_vitals(hp: int, max_hp: int, sp: int, max_sp: int) -> void:
_cached_hp = hp
_cached_max_hp = max_hp
_cached_sp = sp
_cached_max_sp = max_sp
if _hp_fill:
_hp_fill.size.x = _hp_w * clampf(float(hp) / maxf(1.0, max_hp), 0.0, 1.0)
if _mp_fill:
_mp_fill.size.x = _hp_w * clampf(float(sp) / maxf(1.0, max_sp), 0.0, 1.0)
if _hp_label:
_hp_label.text = "%d / %d" % [hp, max_hp]
if quickbar:
if quickbar.has_method("set_hp"):
quickbar.set_hp(hp, max_hp)
if quickbar.has_method("set_sp"):
quickbar.set_sp(sp, max_sp)
func set_exp(xp: int, next_xp: int) -> void:
_cached_exp = xp
_cached_next_exp = next_xp
if _exp_fill:
_exp_fill.size.x = _hp_w * clampf(float(xp) / maxf(1.0, next_xp), 0.0, 1.0)
if quickbar and quickbar.has_method("set_exp"):
quickbar.set_exp(xp, next_xp)
func set_level(lv: int) -> void:
if _lvl_label:
@@ -283,12 +266,16 @@ func set_energy(value: int, max_value: int = 100) -> void:
_energy_label.text = "%d / %d" % [v, cap]
func set_stamina(value: int, max_value: int) -> void:
_cached_st = value
_cached_max_st = max_value
var cap := maxi(1, max_value)
var v := clampi(value, 0, cap)
if _stamina_fill:
_stamina_fill.size.x = _hp_w * float(v) / float(cap)
if _stamina_label:
_stamina_label.text = "%d / %d" % [v, cap]
if quickbar and quickbar.has_method("set_st"):
quickbar.set_st(value, max_value)
# P4:状态图标条。affects = [{type, point_idx, value, flag, duration}, ...]
func set_affects(affects: Array) -> void:
@@ -371,20 +358,11 @@ func _build_target_panel() -> void:
slot.add_child(_target_fill)
func _build_inventory() -> void:
_inv = _nine("board", 32, 128)
_inv.set_anchors_preset(Control.PRESET_CENTER_RIGHT)
_inv.position = Vector2(-280, -200)
_inv.size = Vector2(256, 384)
_inv.visible = false
var t := Label.new()
t.text = " Inventory (I to close)"
t.position = Vector2(16, 12)
_inv.add_child(t)
add_child(_inv)
# 40250 标准:背包由 ui/inventory_ui.gd 统一管理,不再在此创建原型窗口。
pass
func _input(ev: InputEvent) -> void:
if ev is InputEventKey and ev.pressed and ev.keycode == KEY_I:
_inv.visible = not _inv.visible
func _input(_ev: InputEvent) -> void:
pass
func _process(_dt: float) -> void:
if _player: