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
+204
View File
@@ -0,0 +1,204 @@
# metin_socket_system.gd —— 魔石镶嵌与打孔清除系统(40250 官方 1:1 对齐)
# 对齐源码:
# - 40250 char_item.cpp:5004-5080 (case ITEM_METIN: 镶嵌魔石、30% 成功率、碎石 28960)
# - 40250 char_item.cpp:4510-4540 (case USE_CLEAN_SOCKET: 卷轴 71006 清除 28960 碎石残渣)
# - 40250 char_item.cpp:53 (const int ITEM_BROKEN_METIN_VNUM = 28960)
# - 40250 ItemData.h (ITEM_METIN, ITEM_WEAPON, ITEM_ARMOR)
class_name MetinSocketSystem
extends RefCounted
const ITEM_BROKEN_METIN_VNUM := 28960 # 碎石残片 Vnum (40250 官方标准)
const VNUM_CLEAN_SOCKET_SCROLL := 71006 # 提石卷轴 / 清除残渣卷轴 (USE_CLEAN_SOCKET)
const ITEM_TYPE_WEAPON := 1
const ITEM_TYPE_ARMOR := 2
const ITEM_TYPE_METIN := 10
const WEARABLE_WEAPON := 1 << 0
const WEARABLE_BODY := 1 << 1
# 魔石基础分类(根据 40250 官方属性定义,同一分类不可在同一装备上重复镶嵌)
# 例如:突击石、穿透石、致命石、冷却石、怪物石、保护石、防御石、回避石、生命石等
const METIN_GROUPS := {
# 穿透石 (28030..28430)
"PENETRATE": [28030, 28130, 28230, 28330, 28430],
# 致命石 / 暴击石 (28031..28431)
"CRITICAL": [28031, 28131, 28231, 28331, 28431],
# 冷却石 / 施法石 (28032..28432)
"COOLDOWN": [28032, 28132, 28232, 28332, 28432],
# 战士突击石 (28033..28433)
"WARRIOR": [28033, 28133, 28233, 28333, 28433],
# 刺客突击石 (28034..28434)
"ASSASSIN": [28034, 28134, 28234, 28334, 28434],
# 武士突击石 (28035..28435)
"SURA": [28035, 28135, 28235, 28335, 28435],
# 萨满突击石 (28036..28436)
"SHAMAN": [28036, 28136, 28236, 28336, 28436],
# 怪物石 (28037..28437)
"MONSTER": [28037, 28137, 28237, 28337, 28437],
# 躲闪石 / 回避石 (28038..28438)
"EVASION": [28038, 28138, 28238, 28338, 28438],
# 抵挡石 / 格挡石 (28039..28439)
"BLOCK": [28039, 28139, 28239, 28339, 28439],
# 坚韧石 / 魔法石 (28040..28440)
"MAGIC": [28040, 28140, 28240, 28340, 28440],
# 活力石 / 生命石 (28041..28441)
"VITALITY": [28041, 28141, 28241, 28341, 28441],
# 防御石 (28042..28442)
"DEFENSE": [28042, 28142, 28242, 28342, 28442],
# 速度石 (28043..28443)
"SPEED": [28043, 28143, 28243, 28343, 28443],
}
# 官方默认镶嵌成功率:30%40250 char_item.cpp:5070 number(1, 100) <= 30
const DEFAULT_METIN_SUCCESS_RATE := 30
## 检查是否为魔石
static func is_metin_stone(vnum: int, proto: Dictionary = {}) -> bool:
if proto and int(proto.get("type", 0)) == ITEM_TYPE_METIN:
return true
return vnum >= 28000 and vnum <= 28450
## 检查是否为清除魔石残渣卷轴
static func is_clean_socket_scroll(vnum: int) -> bool:
return vnum == VNUM_CLEAN_SOCKET_SCROLL
## 获取魔石的类别分组(用于防同类同插槽重复)
static func get_metin_group(vnum: int) -> String:
for grp in METIN_GROUPS.keys():
if vnum in METIN_GROUPS[grp]:
return grp
return str(vnum / 100)
## 检查装备能否镶嵌该魔石(武器魔石 vs 防具魔石)
static func can_attach_metin(stone_vnum: int, stone_proto: Dictionary, target_proto: Dictionary) -> Dictionary:
var target_type := int(target_proto.get("type", 0))
if target_type not in [ITEM_TYPE_WEAPON, ITEM_TYPE_ARMOR]:
return {"ok": false, "code": "CANNOT_SOCKET_TYPE", "msg": "此物品类型无法镶嵌魔石。"}
var stone_wear := int(stone_proto.get("wear_flag", 0))
if target_type == ITEM_TYPE_ARMOR:
# 防具魔石必须匹配 WEARABLE_BODY (衣服)
var sub := int(target_proto.get("sub_type", 0))
if sub != 0: # 0 = ARMOR_BODY
return {"ok": false, "code": "ARMOR_NOT_BODY", "msg": "防具类魔石只能镶嵌在盔甲上。"}
elif target_type == ITEM_TYPE_WEAPON:
var sub := int(target_proto.get("sub_type", 0))
if sub == 5: # WEAPON_ARROW
return {"ok": false, "code": "WEAPON_ARROW", "msg": "箭矢无法镶嵌魔石。"}
return {"ok": true}
## 执行魔石镶嵌 (40250 char_item.cpp:5004-5080)
static func socket_metin_stone(stone_vnum: int, stone_proto: Dictionary,
target_item: Dictionary, target_proto: Dictionary,
force_success := false, force_fail := false) -> Dictionary:
var check := can_attach_metin(stone_vnum, stone_proto, target_proto)
if not bool(check.get("ok", false)):
return check
var sockets: Array = target_item.get("sockets", [1, 1, 1]).duplicate()
while sockets.size() < 3:
sockets.append(1)
var stone_grp := get_metin_group(stone_vnum)
# 检查装备上是否已有同类魔石 (40250 char_item.cpp:5035 item->GetValue(5) == p->alValues[5])
for s in sockets:
var sv := int(s)
if sv > 2 and sv != ITEM_BROKEN_METIN_VNUM:
if get_metin_group(sv) == stone_grp:
return {
"ok": false,
"code": "DUPLICATE_METIN",
"msg": "装备上已经镶嵌了同类型的魔石,无法重复镶嵌!"
}
# 寻找可用孔位 (40250: socket >= 1 && socket <= 2 表示开孔可用状态)
var target_slot := -1
for i in sockets.size():
var sv := int(sockets[i])
if sv >= 1 and sv <= 2:
target_slot = i
break
if target_slot == -1:
return {
"ok": false,
"code": "NO_AVAILABLE_SOCKET",
"msg": "此装备没有可用的空魔石插槽(已插满或被碎石堵塞)。"
}
# 判定成功/失败概率(40250 官方 30% 成功)
var success := false
if force_success:
success = true
elif force_fail:
success = false
else:
success = (randi() % 100 + 1) <= DEFAULT_METIN_SUCCESS_RATE
if success:
sockets[target_slot] = stone_vnum
target_item["sockets"] = sockets
return {
"ok": true,
"code": "SUCCESS",
"msg": "魔石成功镶嵌到了装备上!",
"consumed": true,
"socket_index": target_slot,
"result_vnum": stone_vnum,
}
else:
# 镶嵌失败:碎石堵塞孔位 (ITEM_BROKEN_METIN_VNUM = 28960)
sockets[target_slot] = ITEM_BROKEN_METIN_VNUM
target_item["sockets"] = sockets
return {
"ok": false,
"code": "BROKEN_METIN",
"msg": "魔石破碎了,碎石残渣堵塞了插槽!",
"consumed": true,
"socket_index": target_slot,
"result_vnum": ITEM_BROKEN_METIN_VNUM,
}
## 执行提石卷轴 / 清除碎石残渣 (40250 char_item.cpp:4510 USE_CLEAN_SOCKET)
static func clean_broken_sockets(target_item: Dictionary) -> Dictionary:
var sockets: Array = target_item.get("sockets", [1, 1, 1]).duplicate()
while sockets.size() < 3:
sockets.append(1)
var has_broken := false
for s in sockets:
if int(s) == ITEM_BROKEN_METIN_VNUM:
has_broken = true
break
if not has_broken:
return {
"ok": false,
"code": "NO_BROKEN_STONE",
"msg": "该装备插槽内没有碎石残渣,无需清除。"
}
# 40250 char_item.cpp:4526 算法:
# 1. 把所有有效魔石(不等于 28960 且不为 0)按序向前压实
# 2. 剩余的孔位重新置为 1(可用空孔)
var cleaned: Array = []
for s in sockets:
var sv := int(s)
if sv != ITEM_BROKEN_METIN_VNUM and sv > 2:
cleaned.append(sv)
while cleaned.size() < sockets.size():
cleaned.append(1) # 恢复为空孔位 1
target_item["sockets"] = cleaned
return {
"ok": true,
"code": "SUCCESS",
"msg": "成功清除了装备上的碎石残渣,插槽已恢复可用!",
"consumed": true,
"sockets": cleaned,
}