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
+215
View File
@@ -0,0 +1,215 @@
# target_ui.gd —— 40250 目标信息板(TargetBoard1:1 实现
# 严格对照:
# root/uitarget.py:14-340 (TargetBoard)
# UserInterface/InstanceBase.cpp (CanViewTargetHP)
# root/localeinfo.py (TARGET_LEVEL_PAWN..KING)
extends Node
const UiAssets = preload("res://ui/ui_assets.gd")
const GRADE_NAMES := {
0: "1阶", # nonplayer.PAWN: TARGET_LEVEL_PAWN
1: "2阶", # nonplayer.S_PAWN: TARGET_LEVEL_S_PAWN
2: "3阶", # nonplayer.KNIGHT: TARGET_LEVEL_KNIGHT
3: "4阶", # nonplayer.S_KNIGHT: TARGET_LEVEL_S_KNIGHT
4: "5阶首领", # nonplayer.BOSS: TARGET_LEVEL_BOSS
5: "6阶王", # nonplayer.KING: TARGET_LEVEL_KING
}
const DEFAULT_BOARD_WIDTH := 250.0
const BOARD_HEIGHT := 40.0
const GAUGE_WIDTH := 130.0
signal whisper_requested(name: String)
signal exchange_requested(vid: int)
signal fight_requested(vid: int)
signal view_equipment_requested(vid: int)
var client: Node
var _root: Control
var _board: Panel
var _name_label: Label
var _hp_gauge_bg: ColorRect
var _hp_gauge_fill: ColorRect
var _hp_text: Label
var _close_button: Button
var _button_container: HBoxContainer
var _action_buttons: Dictionary = {}
var _current_vid := 0
var _current_name := ""
var _current_hp_pct := 100
var _is_pc := false
var _shown := false
var _assets_root := ""
func setup(m2client: Node, parent: Node, assets := "") -> void:
client = m2client
_assets_root = assets
_build(parent)
func is_open() -> bool:
return _shown and _root != null and _root.visible
func get_target_vid() -> int:
return _current_vid
func get_target_name() -> String:
return _current_name
func get_hp_percentage() -> int:
return _current_hp_pct
func open(vid: int, target_name: String, level := -1, grade := -1, is_pc := false) -> void:
_current_vid = vid
_is_pc = is_pc
var display_name := target_name
var prefix := ""
if level > 0:
prefix += "[Lv. %d] " % level
if grade in GRADE_NAMES:
prefix += "(%s) " % GRADE_NAMES[grade]
_current_name = prefix + display_name
_name_label.text = _current_name
# 40250 uitarget.py:301 板面自适应宽度:200 + 7 * nameLength
var calculated_width := maxf(DEFAULT_BOARD_WIDTH, 200.0 + 7.0 * float(_current_name.length()))
_update_layout(calculated_width, is_pc)
_shown = true
_root.visible = true
_update_screen_position()
func set_hp(hp_pct: int) -> void:
_current_hp_pct = clampi(hp_pct, 0, 100)
if _hp_gauge_fill:
_hp_gauge_fill.size.x = GAUGE_WIDTH * (_current_hp_pct / 100.0)
if _hp_text:
_hp_text.text = "%d%%" % _current_hp_pct
if _hp_gauge_bg and not _is_pc:
_hp_gauge_bg.visible = true
func close() -> void:
_shown = false
_current_vid = 0
_current_name = ""
if _root:
_root.visible = false
func _on_close_button_pressed() -> void:
if client and client.has_method("clear_target"):
client.clear_target()
close()
func _update_layout(width: float, is_pc: bool) -> void:
_board.size = Vector2(width, BOARD_HEIGHT)
if is_pc:
_hp_gauge_bg.visible = false
_button_container.visible = true
_button_container.position = Vector2(width - 200, 8)
else:
_hp_gauge_bg.visible = true
_button_container.visible = false
_hp_gauge_bg.position = Vector2(width - 175, 14)
_close_button.position = Vector2(width - 28, 10)
func _update_screen_position() -> void:
if _root == null or not is_open():
return
var screen_w := 800.0
var vp := _root.get_viewport()
if vp:
screen_w = vp.get_visible_rect().size.x
# 40250 uitarget.py:254 UpdatePosition: ScreenWidth // 2 - Width // 2, 10
_root.position = Vector2(screen_w / 2.0 - _board.size.x / 2.0, 10.0)
func _build(parent: Node) -> void:
_root = Control.new()
_root.name = "TargetBoard"
_root.set_anchors_preset(Control.PRESET_TOP_LEFT)
_root.size = Vector2(DEFAULT_BOARD_WIDTH, BOARD_HEIGHT)
_root.visible = false
parent.add_child(_root)
# 经典 ThinBoard 风格
_board = Panel.new()
_board.name = "Board"
_board.size = Vector2(DEFAULT_BOARD_WIDTH, BOARD_HEIGHT)
var bg := StyleBoxFlat.new()
bg.bg_color = Color(0.04, 0.05, 0.07, 0.88)
bg.border_color = Color(0.45, 0.38, 0.22, 0.95)
bg.set_border_width_all(1)
bg.set_corner_radius_all(2)
_board.add_theme_stylebox_override("panel", bg)
_root.add_child(_board)
# 目标名称标签
_name_label = Label.new()
_name_label.name = "Name"
_name_label.position = Vector2(23, 11)
_name_label.size = Vector2(200, 20)
_name_label.add_theme_font_size_override("font_size", 11)
_name_label.add_theme_color_override("font_color", Color(1.0, 0.92, 0.75))
_name_label.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
_name_label.add_theme_constant_override("outline_size", 2)
_board.add_child(_name_label)
# 红色 HP Gauge40250 130px 经典红槽)
_hp_gauge_bg = ColorRect.new()
_hp_gauge_bg.name = "HPGaugeBG"
_hp_gauge_bg.position = Vector2(DEFAULT_BOARD_WIDTH - 175, 14)
_hp_gauge_bg.size = Vector2(GAUGE_WIDTH, 12)
_hp_gauge_bg.color = Color(0.12, 0.02, 0.02, 0.95)
_board.add_child(_hp_gauge_bg)
_hp_gauge_fill = ColorRect.new()
_hp_gauge_fill.name = "HPGaugeFill"
_hp_gauge_fill.position = Vector2.ZERO
_hp_gauge_fill.size = Vector2(GAUGE_WIDTH, 12)
_hp_gauge_fill.color = Color(0.85, 0.12, 0.12, 1.0)
_hp_gauge_bg.add_child(_hp_gauge_fill)
_hp_text = Label.new()
_hp_text.name = "HPText"
_hp_text.set_anchors_preset(Control.PRESET_FULL_RECT)
_hp_text.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_hp_text.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_hp_text.add_theme_font_size_override("font_size", 9)
_hp_text.add_theme_color_override("font_color", Color(1, 1, 1, 0.95))
_hp_text.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.8))
_hp_text.add_theme_constant_override("outline_size", 1)
_hp_text.text = "100%"
_hp_gauge_bg.add_child(_hp_text)
# 关闭按钮 (X)
_close_button = Button.new()
_close_button.name = "CloseButton"
_close_button.position = Vector2(DEFAULT_BOARD_WIDTH - 28, 10)
_close_button.size = Vector2(18, 18)
_close_button.text = "×"
_close_button.add_theme_font_size_override("font_size", 12)
_close_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
_close_button.pressed.connect(_on_close_button_pressed)
_board.add_child(_close_button)
# PC 交互按钮条
_button_container = HBoxContainer.new()
_button_container.name = "ButtonContainer"
_button_container.position = Vector2(DEFAULT_BOARD_WIDTH - 200, 8)
_button_container.visible = false
_board.add_child(_button_container)
_add_action_btn("私聊", func(): whisper_requested.emit(_current_name))
_add_action_btn("交易", func(): exchange_requested.emit(_current_vid))
_add_action_btn("装备", func(): view_equipment_requested.emit(_current_vid))
func _add_action_btn(text: String, callback: Callable) -> void:
var btn := Button.new()
btn.text = text
btn.custom_minimum_size = Vector2(42, 22)
btn.add_theme_font_size_override("font_size", 10)
btn.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
btn.pressed.connect(callback)
_button_container.add_child(btn)