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
+177
View File
@@ -0,0 +1,177 @@
# marriage_wedding_love_system.gd —— Metin2 40250 官方夫妻同心传送与爱情点数婚戒祈愿系统 1:1
# 100% 对照 40250 服务端 marriage.cpp, marriage.h, char_item.cpp (ITEM_MARRIAGE_RING 70302), char.cpp (WarpToPID)
class_name MarriageWeddingLoveSystem
extends RefCounted
signal couple_married(pid1: int, name1: String, pid2: int, name2: String)
signal couple_divorced(pid1: int, pid2: int)
signal partner_teleported(teleporter_id: int, target_pos: Vector2, sp_consumed: int)
signal love_points_increased(pid1: int, pid2: int, new_love_points: int)
signal marriage_error(reason: String)
# 40250 官方婚戒与爱情饰品 Vnum
const VNUM_COUPLE_RING := 70301 # 情侣/订婚戒指 (Couple Ring)
const VNUM_WEDDING_RING := 70302 # 结婚戒指 (Wedding Ring / ITEM_MARRIAGE_RING)
const VNUM_BRACELET_OF_LOVE := 71069 # 爱情手镯 (Bracelet of Love - 增攻击)
const VNUM_EARRINGS_OF_LOVE := 71071 # 爱情耳环 (Earrings of Love - 增防御)
const VNUM_NECKLACE_OF_LOVE := 71073 # 爱情项链 (Necklace of Love - 增暴击)
# 婚姻档案记录表:
# couple_id -> { "pid1": int, "name1": String, "pid2": int, "name2": String, "love_points": int, "marry_time": float }
var _marriages: Dictionary = {}
var _pid_to_couple: Dictionary = {} # pid -> couple_id
var _next_couple_id: int = 1
# ==========================================
# 1. 婚姻缔结与解约 (marriage.cpp: Add, Remove)
# ==========================================
func marry(pid1: int, name1: String, pid2: int, name2: String) -> int:
if is_married(pid1) or is_married(pid2) or pid1 == pid2:
marriage_error.emit("ALREADY_MARRIED")
return -1
var cid = _next_couple_id
_next_couple_id += 1
_marriages[cid] = {
"pid1": pid1,
"name1": name1,
"pid2": pid2,
"name2": name2,
"love_points": 50, # 初始爱情点数 50%
"marry_time": Time.get_unix_time_from_system()
}
_pid_to_couple[pid1] = cid
_pid_to_couple[pid2] = cid
couple_married.emit(pid1, name1, pid2, name2)
return cid
func divorce(pid: int) -> bool:
if not is_married(pid):
marriage_error.emit("NOT_MARRIED")
return false
var cid = int(_pid_to_couple[pid])
var m = _marriages[cid]
var p1 = int(m["pid1"])
var p2 = int(m["pid2"])
_pid_to_couple.erase(p1)
_pid_to_couple.erase(p2)
_marriages.erase(cid)
couple_divorced.emit(p1, p2)
return true
func is_married(pid: int) -> bool:
return _pid_to_couple.has(pid)
func get_partner_pid(pid: int) -> int:
if not is_married(pid):
return 0
var cid = _pid_to_couple[pid]
var m = _marriages[cid]
return int(m["pid2"]) if int(m["pid1"]) == pid else int(m["pid1"])
func get_partner_name(pid: int) -> String:
if not is_married(pid):
return ""
var cid = _pid_to_couple[pid]
var m = _marriages[cid]
return str(m["name2"]) if int(m["pid1"]) == pid else str(m["name1"])
# ==========================================
# 2. 爱情点数与同心杀怪成长 (marriage.cpp: LovePoint, IsNear)
# ==========================================
func get_love_points(pid: int) -> int:
if not is_married(pid):
return 0
var cid = _pid_to_couple[pid]
return int(_marriages[cid]["love_points"])
func add_love_points(pid: int, amount: int) -> int:
if not is_married(pid) or amount <= 0:
return 0
var cid = _pid_to_couple[pid]
var cur = int(_marriages[cid]["love_points"])
var new_val = clamp(cur + amount, 0, 100)
_marriages[cid]["love_points"] = new_val
love_points_increased.emit(_marriages[cid]["pid1"], _marriages[cid]["pid2"], new_val)
return new_val
# 爱情饰品属性加成计算 (marriage.cpp: GetBonus)
func get_love_item_bonus(pid: int, item_vnum: int) -> Dictionary:
var lp = get_love_points(pid)
# 依据 40250 公式,加成随爱情点数百分比等比浮动
var ratio = lp / 100.0
match item_vnum:
VNUM_BRACELET_OF_LOVE: # 爱情手镯: 增加攻击力 (最高 +40)
return {"bonus_type": "att_grade", "value": int(floor(40 * ratio))}
VNUM_EARRINGS_OF_LOVE: # 爱情耳环: 增加防御力 (最高 +30)
return {"bonus_type": "def_grade", "value": int(floor(30 * ratio))}
VNUM_NECKLACE_OF_LOVE: # 爱情项链: 增加暴击率 (最高 +20%)
return {"bonus_type": "crit_pct", "value": int(floor(20 * ratio))}
return {"bonus_type": "none", "value": 0}
# ==========================================
# 3. 结婚戒指跨图同心传送 (char_item.cpp:2490 ITEM_MARRIAGE_RING)
# ==========================================
func use_wedding_ring(
player_id: int,
player_sp: int,
partner_online: bool,
partner_map_name: String,
partner_pos: Vector2,
is_partner_in_arena: bool = false
) -> Dictionary:
if not is_married(player_id):
marriage_error.emit("NOT_MARRIED")
return {"ok": false, "reason": "NOT_MARRIED", "msg": "未婚状态无法使用结婚戒指传送!"}
if not partner_online:
marriage_error.emit("PARTNER_OFFLINE")
return {"ok": false, "reason": "PARTNER_OFFLINE", "msg": "你的伴侣当前不在线,无法感应其位置。"}
# 40250 官方: 竞技场/武斗决斗中禁止夫妻传送 (IsArenaMap)
if is_partner_in_arena:
marriage_error.emit("PARTNER_IN_ARENA")
return {"ok": false, "reason": "PARTNER_IN_ARENA", "msg": "你的伴侣正在决斗竞技场中,禁止传送进入!"}
# 计算传送法力消耗 (40250 CalculateConsumeSP):
# 爱情点数越高,传送消耗的 SP 越少 (100% 爱情点数消耗 50 SP,0% 消耗 200 SP)
var lp = get_love_points(player_id)
var consume_sp = int(floor(200 - (150 * (lp / 100.0))))
if player_sp < consume_sp:
marriage_error.emit("NOT_ENOUGH_SP")
return {
"ok": false,
"reason": "NOT_ENOUGH_SP",
"msg": "法力不足!夫妻同心传送需要消耗 %d 点法力值 (SP)。" % consume_sp
}
partner_teleported.emit(player_id, partner_pos, consume_sp)
return {
"ok": true,
"partner_name": get_partner_name(player_id),
"target_map": partner_map_name,
"target_pos": partner_pos,
"sp_consumed": consume_sp,
"remaining_sp": player_sp - consume_sp,
"msg": "婚戒闪耀着柔和的粉红霞光,跨越千山万水,你瞬间传送到了伴侣身边!"
}
# 序列化与反序列化
func serialize() -> Dictionary:
return {
"marriages": _marriages.duplicate(true),
"pid_to_couple": _pid_to_couple.duplicate(),
"next_couple_id": _next_couple_id
}
func deserialize(data: Dictionary) -> void:
_marriages = data.get("marriages", {}).duplicate(true)
_pid_to_couple = data.get("pid_to_couple", {}).duplicate()
_next_couple_id = int(data.get("next_couple_id", 1))