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
+111
View File
@@ -0,0 +1,111 @@
# marriage_system.gd —— Metin2 40250 结婚戒指与伴侣瞬移传送系统 1:1
# 对照 40250 服务端 wedding.cpp, char_item.cpp (Wedding Ring 70301, NPC 20017 Musician)
class_name MarriageSystem
extends RefCounted
signal couple_warped(target_map: String, target_pos: Vector3)
const NPC_MUSICIAN := 20017 # 月下老人 / 乐师
const ITEM_WEDDING_RING := 70301 # 结婚戒指
const WARP_SP_COST := 50 # 每次传送消耗 50 SP
const WARP_COOLDOWN := 180.0 # 3 分钟传送冷却
# 禁止传送进入的特殊副本/区域
const FORBIDDEN_MAPS := ["deviltower_f7", "deviltower_f8", "deviltower_f9", "oxevent"]
var is_married: bool = false
var spouse_name: String = ""
var love_points: int = 0 # 0 ~ 100%
var warp_cooldown: float = 0.0
# 在月下老人处登记结婚 (wedding.cpp)
func marry(player_name: String, partner_name: String, ring_item: Dictionary, player_level: int) -> Dictionary:
if is_married:
return {"ok": false, "reason": "ALREADY_MARRIED", "msg": "你已经结过婚了!"}
if player_level < 25:
return {"ok": false, "reason": "LEVEL_TOO_LOW", "msg": "角色等级未满 25 级,无法缔结婚约!"}
var vnum: int = int(ring_item.get("vnum", 0))
if vnum != ITEM_WEDDING_RING:
return {"ok": false, "reason": "NO_WEDDING_RING", "msg": "缔结婚约必须携带结婚戒指!"}
is_married = true
spouse_name = partner_name
love_points = 50 # 初始爱意值 50%
warp_cooldown = 0.0
# 戒指上刻录伴侣名字
ring_item["inscribed_name"] = partner_name
ring_item["is_bound"] = true
return {
"ok": true,
"spouse_name": spouse_name,
"love_points": love_points,
"msg": "恭喜!你与【%s】在月下老人的见证下正式结为夫妻!" % spouse_name
}
# 离婚解除婚约
func divorce() -> Dictionary:
if not is_married:
return {"ok": false, "reason": "NOT_MARRIED"}
var old_spouse = spouse_name
is_married = false
spouse_name = ""
love_points = 0
warp_cooldown = 0.0
return {"ok": true, "msg": "已与【%s】解除婚姻关系。" % old_spouse}
# 增加爱意值
func add_love_points(amount: int) -> int:
if is_married:
love_points = clampi(love_points + amount, 0, 100)
return love_points
# 夫妻瞬移传送 (char_item.cpp:UseItemEx 70301)
func warp_to_spouse(current_sp: int, spouse_location: Dictionary, is_dead: bool) -> Dictionary:
if not is_married:
return {"ok": false, "reason": "NOT_MARRIED", "msg": "你尚未结婚,无法使用夫妻传送!"}
if is_dead:
return {"ok": false, "reason": "PLAYER_DEAD", "msg": "死亡状态下无法进行传送!"}
if warp_cooldown > 0.0:
return {"ok": false, "reason": "COOLDOWN_ACTIVE", "remaining": warp_cooldown, "msg": "结婚戒指传送冷却中!"}
if current_sp < WARP_SP_COST:
return {"ok": false, "reason": "NOT_ENOUGH_SP", "msg": "传送所需内力值不足 (需要 %d SP)" % WARP_SP_COST}
if spouse_location.is_empty() or not bool(spouse_location.get("is_online", false)):
return {"ok": false, "reason": "SPOUSE_OFFLINE", "msg": "伴侣当前不在线!"}
var map_id: String = String(spouse_location.get("map", ""))
if map_id in FORBIDDEN_MAPS:
return {"ok": false, "reason": "FORBIDDEN_ZONE", "msg": "伴侣当前处于特殊封闭区域,无法瞬移传送到其身边!"}
var pos: Vector3 = spouse_location.get("pos", Vector3.ZERO)
# 扣除 SP 并进入冷却
warp_cooldown = WARP_COOLDOWN
couple_warped.emit(map_id, pos)
return {
"ok": true,
"target_map": map_id,
"target_pos": pos,
"sp_cost": WARP_SP_COST,
"cooldown": WARP_COOLDOWN,
"msg": "心有灵犀!正在瞬间传送到伴侣身边..."
}
# 逐帧更新冷却
func update(dt: float) -> void:
if warp_cooldown > 0.0:
warp_cooldown = maxf(0.0, warp_cooldown - dt)
func get_info() -> Dictionary:
return {
"is_married": is_married,
"spouse_name": spouse_name,
"love_points": love_points,
"warp_cooldown": warp_cooldown
}