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
+114
View File
@@ -53,6 +53,14 @@ class FakeClient extends Node:
return "銀河聯盟" if gid == 9 else ""
func say(type: int, text: String) -> bool:
says.append([type, text]); return true
signal skills_changed
signal skill_group_changed(group: int)
var skills_list: Array = [
{"id": 31, "level": 10, "master": 0},
{"id": 121, "level": 5, "master": 0},
]
func get_skills() -> Array: return skills_list
func get_skill_group() -> int: return 1
var _fail := 0
func _ck(c: bool, m: String) -> void:
@@ -148,6 +156,112 @@ func _run() -> void:
cs._set_state("STATUS")
_ck(char_page.visible, "Character_Page visible again after SetState(STATUS)")
# --- Skill & Emoticon 拖拽到快捷栏 (40250 机制自检) ---
var mouse_ctl := preload("res://ui/mouse_controller.gd").new()
get_root().add_child(mouse_ctl)
mouse_ctl.setup(ui)
cs.item_mouse = mouse_ctl
var qb := preload("res://ui/quickbar.gd").new()
get_root().add_child(qb)
qb.item_mouse = mouse_ctl
qb.setup(fc, cs.skill_table, ui, func(): return null, assets)
cs.open("SKILL")
_ck(cs.is_open(), "SKILL tab open")
var active_slot: Control = cs._node("Skill_Active_Slot")
var slot1: Control = active_slot.get_node_or_null("slot_1") if active_slot else null
var slot1_btn: TextureButton = slot1.get_node_or_null("Btn") if slot1 else null
_ck(slot1_btn != null, "Skill slot_1 has Btn")
# 1. 模拟左键点击主动技能 (id 31) -> 成功 attach_skill 到鼠标
if slot1_btn:
var ev := InputEventMouseButton.new()
ev.button_index = MOUSE_BUTTON_LEFT
ev.pressed = true
slot1_btn.gui_input.emit(ev)
_ck(mouse_ctl.is_attached(), "mouse attached skill on left click")
var att := mouse_ctl.attached()
_ck(att.get("source") == "skill" and att.get("skill_id") == 31, "attached payload is skill 31")
# 2. 模拟拖拽与容差判定:鼠标在快捷栏槽位 0 边缘(例如向外偏移 6 像素)依然能准确吸附落入槽位 0
var slot0_ctrl: Control = qb._slots[0]["btn"]
var drop_pos: Vector2 = slot0_ctrl.get_global_rect().position + Vector2(-6, -6)
_ck(mouse_ctl._drop_at(drop_pos), "generous hitbox allows drop within tolerance")
var st0 := qb.mobile_slot_state(0)
_ck(st0.get("kind") == "skill" and int(st0.get("id")) == 31, "quickbar slot 0 has skill 31 (got %s)" % str(st0))
# 2b. 40250 点击拾取模式:点击技能后松开(未发生拖动),技能仍然附着在鼠标上
var ev_down := InputEventMouseButton.new()
ev_down.button_index = MOUSE_BUTTON_LEFT
ev_down.pressed = true
slot1_btn.gui_input.emit(ev_down)
var ev_up := InputEventMouseButton.new()
ev_up.button_index = MOUSE_BUTTON_LEFT
ev_up.pressed = false
mouse_ctl._input(ev_up)
_ck(mouse_ctl.is_attached(), "mouse still attached after stationary click release (40250 pickup mode)")
# 移到槽位 3 点击放置
var slot3_ctrl: Control = qb._slots[3]["btn"]
var ev_click_slot := InputEventMouseButton.new()
ev_click_slot.button_index = MOUSE_BUTTON_LEFT
ev_click_slot.pressed = true
ev_click_slot.position = slot3_ctrl.get_global_rect().get_center()
mouse_ctl._input(ev_click_slot)
_ck(not mouse_ctl.is_attached(), "clicking quickslot 3 drops attached skill")
var st3 := qb.mobile_slot_state(3)
_ck(st3.get("kind") == "skill" and int(st3.get("id")) == 31, "quickbar slot 3 has skill 31 (got %s)" % str(st3))
mouse_ctl.cancel()
# 3. 被动技能(如 Leadership 121slot 103)不可拖拽 / 附着
var etc_slot: Control = cs._node("Skill_ETC_Slot")
var slot103: Control = etc_slot.get_node_or_null("slot_103") if etc_slot else null
var slot103_btn: TextureButton = slot103.get_node_or_null("Btn") if slot103 else null
if slot103_btn:
var ev := InputEventMouseButton.new()
ev.button_index = MOUSE_BUTTON_LEFT
ev.pressed = true
slot103_btn.gui_input.emit(ev)
_ck(not mouse_ctl.is_attached(), "passive skill 121 CANNOT be attached to mouse (40250 CanUseSkill == false)")
# 4. Ctrl + 左键主动技能 -> 自动填入首个空快捷栏槽位 (40250 RequestAddToEmptyLocalQuickSlot)
if slot1_btn:
var ev_ctrl := InputEventMouseButton.new()
ev_ctrl.button_index = MOUSE_BUTTON_LEFT
ev_ctrl.pressed = true
ev_ctrl.ctrl_pressed = true
slot1_btn.gui_input.emit(ev_ctrl)
var st1 := qb.mobile_slot_state(1)
_ck(st1.get("kind") == "skill" and int(st1.get("id")) == 31, "Ctrl+Click auto assigns skill to quickslot 1 (got %s)" % str(st1))
# 5. 表情拖拽:切换到 EMOTICON,点击动作 1 附着到鼠标,放入快捷栏槽位 2
cs.open("EMOTICON")
var solo_slot: Control = cs._node("SoloEmotionSlot")
var emote1: Control = solo_slot.get_node_or_null("slot_1") if solo_slot else null
var emote1_btn: TextureButton = emote1.get_node_or_null("Btn") if emote1 else null
_ck(emote1_btn != null, "Emote slot_1 has Btn")
if emote1_btn:
var ev := InputEventMouseButton.new()
ev.button_index = MOUSE_BUTTON_LEFT
ev.pressed = true
emote1_btn.gui_input.emit(ev)
_ck(mouse_ctl.is_attached(), "mouse attached emote on left click")
var att_e := mouse_ctl.attached()
_ck(att_e.get("source") == "emotion" and att_e.get("emote_id") == 1, "attached payload is emote 1")
_ck(qb._drop_mouse_payload(att_e, 2), "drop emote onto quickbar slot 2 succeeds")
var st2 := qb.mobile_slot_state(2)
_ck(st2.get("kind") == "emote" and int(st2.get("id")) == 1, "quickbar slot 2 has emote 1 (got %s)" % str(st2))
mouse_ctl.cancel()
# --- TitleBar close button ---
var tb: Control = cs._node("Character_TitleBar")
var close_btn: Button = tb.find_child("CloseButton", true, false) if tb else null
_ck(close_btn != null, "Character_TitleBar has CloseButton")
if close_btn:
close_btn.pressed.emit()
_ck(not cs.is_open(), "CloseButton on TitleBar closes window")
cs.open()
cs.close()
_ck(not cs.is_open(), "character window closed")