- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
205 lines
7.1 KiB
GDScript
205 lines
7.1 KiB
GDScript
# 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,
|
||
}
|