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
+175
View File
@@ -0,0 +1,175 @@
# jewelry_socket_system.gd —— Metin2 40250 首饰饰品金刚石打孔与精炼宝石镶嵌 1:1
# 对照 40250 服务端 char_item.cpp (PutGemInAccessory), item_attribute.cpp
class_name JewelrySocketSystem
extends RefCounted
const ITEM_DIAMOND := 50621 # 精炼金刚石
const MAX_SOCKETS := 3
const GEM_LIFETIME_SECS := 86400.0 # 24 小时战斗佩戴寿命
# 饰品支持打孔的分类 (14:手镯, 16:项链, 17:耳环)
const VALID_ACCESSORY_PREFIXES := [14, 16, 17]
# 饰品材质与对应精炼宝石映射 (base_vnum_pattern -> gem_vnum)
const ACCESSORY_GEM_MAPPING: Dictionary = {
"copper": 50624, # 铜首饰 -> 铜 50624
"silver": 50625, # 银首饰 -> 银 50625
"gold": 50626, # 金首饰 -> 金 50626
"jade": 50627, # 玉首饰 -> 玉 50627
"ebony": 50628, # 黑檀首饰 -> 黑檀 50628
"pearl": 50629, # 珍珠首饰 -> 珍珠 50629
"white_gold": 50630, # 白金首饰 -> 白金 50630
"crystal": 50631, # 水晶首饰 -> 水晶 50631
"amethyst": 50632, # 紫晶首饰 -> 紫晶 50632
"heavens_tear": 50633,# 天泪首饰 -> 天泪石 50633
"soul_crystal": 50634,# 灵魂水晶首饰 -> 灵魂水晶 50634
"ruby": 50635, # 红宝石首饰 -> 红宝石 50635
"garnet": 50636, # 石榴石首饰 -> 石榴石 50636
"emerald": 50637, # 绿宝石首饰 -> 绿宝石 50637
"sapphire": 50638 # 蓝宝石首饰 -> 蓝宝石 50638
}
# 打孔成功率 (0孔打第1孔: 70%, 1孔打第2孔: 50%, 2孔打第3孔: 30%)
const DRILL_CHANCES: Array[float] = [0.70, 0.50, 0.30]
# 宝石阶梯增益百分比 (第1孔: +10%, 第2孔: +10%, 第3孔: +20%)
const GEM_BONUS_TIERS: Array[float] = [0.10, 0.10, 0.20]
static func is_accessory(item: Dictionary) -> bool:
var vnum: int = int(item.get("vnum", 0))
var prefix: int = int(floor(float(vnum) / 1000.0))
return prefix in VALID_ACCESSORY_PREFIXES
# 检查是否可用金刚石打孔
static func can_drill_socket(item: Dictionary) -> Dictionary:
if not is_accessory(item):
return {"ok": false, "reason": "NOT_ACCESSORY", "msg": "金刚石只能用于手镯、项链或耳环打孔!"}
var socket_count: int = int(item.get("socket_count", 0))
if socket_count >= MAX_SOCKETS:
return {"ok": false, "reason": "SOCKETS_FULL", "msg": "该首饰已拥有最大的 3 个宝石孔槽!"}
return {"ok": true, "current_sockets": socket_count}
# 使用金刚石 50621 打孔
static func drill_socket(item: Dictionary, diamond_slot_item: Dictionary, force_success: bool = false) -> Dictionary:
var check = can_drill_socket(item)
if not check["ok"]:
return check
var dvnum: int = int(diamond_slot_item.get("vnum", 0))
if dvnum != ITEM_DIAMOND:
return {"ok": false, "reason": "NOT_DIAMOND", "msg": "打孔必须使用精炼金刚石!"}
var current_sockets: int = int(item.get("socket_count", 0))
var chance: float = DRILL_CHANCES[current_sockets]
var roll: float = 0.0 if force_success else randf()
var success: bool = roll < chance
if not item.has("sockets"):
item["sockets"] = [0, 0, 0]
if not item.has("gem_times"):
item["gem_times"] = [0.0, 0.0, 0.0]
if success:
item["socket_count"] = current_sockets + 1
return {
"ok": true,
"success": true,
"socket_count": item["socket_count"],
"msg": "打孔成功!已开辟出第 %d 个宝石孔槽!" % item["socket_count"]
}
else:
return {
"ok": true,
"success": false,
"socket_count": current_sockets,
"msg": "打孔失败!金刚石破碎,未能开出孔槽。"
}
# 镶嵌精炼矿石宝石
static func insert_gem(item: Dictionary, gem_item: Dictionary, required_gem_vnum: int = 0) -> Dictionary:
if not is_accessory(item):
return {"ok": false, "reason": "NOT_ACCESSORY"}
var socket_count: int = int(item.get("socket_count", 0))
if socket_count <= 0:
return {"ok": false, "reason": "NO_SOCKETS_DRILLED", "msg": "该首饰尚未打孔,请先使用金刚石开孔!"}
if not item.has("sockets"):
item["sockets"] = [0, 0, 0]
if not item.has("gem_times"):
item["gem_times"] = [0.0, 0.0, 0.0]
# 查找第一个空孔
var empty_slot := -1
for i in range(socket_count):
if int(item["sockets"][i]) == 0:
empty_slot = i
break
if empty_slot == -1:
return {"ok": false, "reason": "NO_EMPTY_SOCKET", "msg": "所有已开辟的孔槽都已镶嵌满宝石!"}
var gem_vnum: int = int(gem_item.get("vnum", 0))
if gem_vnum < 50622 or gem_vnum > 50638:
return {"ok": false, "reason": "INVALID_GEM", "msg": "这不是有效的精炼宝石!"}
if required_gem_vnum > 0 and gem_vnum != required_gem_vnum:
return {"ok": false, "reason": "GEM_MISMATCH", "msg": "此宝石与该首饰材质不匹配!"}
# 镶嵌成功
item["sockets"][empty_slot] = gem_vnum
item["gem_times"][empty_slot] = GEM_LIFETIME_SECS
var active_gems := count_active_gems(item)
var bonus_pct := calculate_bonus_percentage(active_gems)
return {
"ok": true,
"slot": empty_slot,
"gem_vnum": gem_vnum,
"active_gems": active_gems,
"total_bonus_pct": bonus_pct,
"lifetime": GEM_LIFETIME_SECS,
"msg": "宝石成功镶入第 %d 个孔槽!固有属性提升 %d%%" % [empty_slot + 1, int(round(bonus_pct * 100))]
}
# 统计当前生效的宝石数量
static func count_active_gems(item: Dictionary) -> int:
var count := 0
var sockets: Array = item.get("sockets", [])
var times: Array = item.get("gem_times", [])
for i in range(sockets.size()):
if int(sockets[i]) > 0 and float(times[i]) > 0.0:
count += 1
return count
# 计算宝石提供的总属性加成百分比 (0~3 颗宝石)
static func calculate_bonus_percentage(gem_count: int) -> float:
var pct: float = 0.0
for i in range(mini(gem_count, GEM_BONUS_TIERS.size())):
pct += GEM_BONUS_TIERS[i]
return pct
# 佩戴战斗时间自然衰减 (dt: 秒)
static func update_gem_endurance(item: Dictionary, dt: float) -> Dictionary:
var expired_gems: Array[int] = []
if not item.has("sockets") or not item.has("gem_times"):
return {"expired": expired_gems}
var sockets: Array = item["sockets"]
var times: Array = item["gem_times"]
for i in range(sockets.size()):
if int(sockets[i]) > 0 and float(times[i]) > 0.0:
times[i] = maxf(0.0, float(times[i]) - dt)
if float(times[i]) <= 0.0:
expired_gems.append(int(sockets[i]))
sockets[i] = 0 # 宝石破碎,孔槽重新清空
return {
"expired": expired_gems,
"active_gems": count_active_gems(item),
"total_bonus_pct": calculate_bonus_percentage(count_active_gems(item))
}