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
+276
View File
@@ -0,0 +1,276 @@
# item_enchant_system.gd —— Metin2 40250 装备词条重铸与精炼附魔洗炼系统 1:1
# 对照 40250 服务端 char_item.cpp, item_attribute.cpp, constants.cpp
class_name ItemEnchantSystem
extends RefCounted
const ItemAttrSystem = preload("res://item_attr_system.gd")
signal enchant_applied(target_slot: int, scroll_vnum: int, success: bool, new_attrs: Array)
signal rare_enchant_applied(target_slot: int, success: bool, rare_attrs: Array)
signal enchant_failed(target_slot: int, reason: String)
const VNUM_ATTR_ADD := 71085 # 添加属性秘籍 (添加第 1~4 条)
const VNUM_ATTR_CHANGE := 71084 # 属性改变秘籍 (洗炼现有 1~5 条)
const VNUM_BLESSING_MARBLE := 70024 # 祝福宝珠 (添加第 5 条属性)
const VNUM_RARE_ADD := 71151 # 稀有属性秘籍 (添加第 6~7 条稀有词条)
const VNUM_RARE_CHANGE := 71152 # 稀有属性改变 (单独洗炼第 6~7 条稀有词条)
const ADD_PERCENT_TABLE := [100, 80, 60, 50, 30] # 官方各阶段成功率
const RARE_ADD_PERCENT := 50 # 稀有词条附加成功率
# 稀有词条池 (6~7 槽位特有词条)
const RARE_ATTR_POOL := {
101: {"name": "全技能伤害加成", "values": [1, 2, 3, 5, 8], "weight": 8},
102: {"name": "首领怪物伤害加成", "values": [2, 4, 6, 8, 12], "weight": 8},
103: {"name": "魔石怪物伤害加成", "values": [3, 5, 8, 10, 15], "weight": 10},
104: {"name": "受首领伤害减免", "values": [2, 4, 6, 8, 10], "weight": 8},
105: {"name": "物理防御无视概率", "values": [1, 2, 3, 5, 7], "weight": 6}
}
# 检查是否为有效附魔卷轴
static func is_enchant_scroll(vnum: int) -> bool:
return vnum in [VNUM_ATTR_ADD, VNUM_ATTR_CHANGE, VNUM_BLESSING_MARBLE, VNUM_RARE_ADD, VNUM_RARE_CHANGE]
# 检查物品是否为可附魔装备
static func is_enchantable_item(item: Dictionary) -> bool:
if item.is_empty():
return false
# 检查是否上锁保护
if item.get("is_locked", false):
return false
var vnum: int = int(item.get("vnum", 0))
# 箭矢、消耗品不可附魔 (箭矢 8000~8009)
if vnum >= 8000 and vnum <= 8009:
return false
var itype: int = int(item.get("type", 1)) # 默认为 1 (Weapon) 或 2 (Armor)
return itype in [1, 2]
# 对背包内的装备使用附魔卷轴
func apply_scroll(scroll_slot: int, target_slot: int, inventory: Array, force_success: bool = false) -> Dictionary:
if scroll_slot < 0 or scroll_slot >= inventory.size() or inventory[scroll_slot] == null:
return {"ok": false, "reason": "INVALID_SCROLL_SLOT", "msg": "附魔卷轴格子无效!"}
if target_slot < 0 or target_slot >= inventory.size() or inventory[target_slot] == null:
return {"ok": false, "reason": "INVALID_TARGET_SLOT", "msg": "目标装备格子无效!"}
var scroll: Dictionary = inventory[scroll_slot]
var target: Dictionary = inventory[target_slot]
var scroll_vnum: int = int(scroll.get("vnum", 0))
if not is_enchant_scroll(scroll_vnum):
return {"ok": false, "reason": "NOT_AN_ENCHANT_SCROLL", "msg": "此物品不是附魔洗炼卷轴!"}
if not is_enchantable_item(target):
enchant_failed.emit(target_slot, "ITEM_CANNOT_BE_ENCHANTED")
return {"ok": false, "reason": "ITEM_CANNOT_BE_ENCHANTED", "msg": "此装备无法进行词条附魔或已被安全上锁!"}
if not target.has("attrs"):
target["attrs"] = []
if not target.has("rare_attrs"):
target["rare_attrs"] = []
var res: Dictionary = {}
match scroll_vnum:
VNUM_ATTR_ADD:
res = _handle_add_attribute(target, force_success)
VNUM_ATTR_CHANGE:
res = _handle_change_attribute(target)
VNUM_BLESSING_MARBLE:
res = _handle_blessing_marble(target, force_success)
VNUM_RARE_ADD:
res = _handle_rare_add(target, force_success)
VNUM_RARE_CHANGE:
res = _handle_rare_change(target)
if not res.get("ok", false):
enchant_failed.emit(target_slot, res.get("reason", "FAILED"))
return res
# 扣除 1 个卷轴
_consume_scroll(scroll_slot, inventory)
if res.get("success", false):
if scroll_vnum in [VNUM_RARE_ADD, VNUM_RARE_CHANGE]:
rare_enchant_applied.emit(target_slot, true, target["rare_attrs"])
else:
enchant_applied.emit(target_slot, scroll_vnum, true, target["attrs"])
else:
enchant_applied.emit(target_slot, scroll_vnum, false, target["attrs"])
return res
# 扣除背包中的卷轴
func _consume_scroll(slot: int, inventory: Array) -> void:
var item: Dictionary = inventory[slot]
var count: int = int(item.get("count", 1))
if count > 1:
item["count"] = count - 1
else:
inventory[slot] = null
# 71085: 添加第 1~4 条属性
func _handle_add_attribute(target: Dictionary, force_success: bool) -> Dictionary:
var attrs: Array = target["attrs"]
if attrs.size() >= 4:
return {"ok": false, "reason": "MAX_BASE_ATTRS_REACHED", "msg": "该装备已有 4 条属性!请使用祝福宝珠开辟第 5 条属性。"}
var rate: int = ADD_PERCENT_TABLE[attrs.size()]
var roll: int = randi_range(1, 100)
var is_succ: bool = force_success or (roll <= rate)
if not is_succ:
return {"ok": true, "success": false, "msg": "附魔失败,卷轴化为灰烬,未能追加新属性!"}
var new_attr = _roll_random_attribute(attrs, target.get("type", 1))
attrs.append(new_attr)
return {
"ok": true,
"success": true,
"new_attr": new_attr,
"total_attrs": attrs.size(),
"msg": "附魔成功!装备获得了新词条【%s +%d】!" % [new_attr["name"], new_attr["value"]]
}
# 71084: 改变所有现有 1~5 条属性
func _handle_change_attribute(target: Dictionary) -> Dictionary:
var attrs: Array = target["attrs"]
if attrs.is_empty():
return {"ok": false, "reason": "NO_ATTRS_TO_CHANGE", "msg": "该装备没有任何属性词条,无法进行洗炼!"}
var count: int = attrs.size()
attrs.clear()
for i in range(count):
var attr = _roll_random_attribute(attrs, target.get("type", 1))
attrs.append(attr)
return {
"ok": true,
"success": true,
"attrs": attrs,
"msg": "洗炼成功!%d 条词条已全新重铸!" % count
}
# 70024: 祝福宝珠 (添加第 5 条属性)
func _handle_blessing_marble(target: Dictionary, force_success: bool) -> Dictionary:
var attrs: Array = target["attrs"]
if attrs.size() < 4:
return {"ok": false, "reason": "NEED_4_ATTRS_FIRST", "msg": "装备必须先拥有 4 条属性,才能使用祝福宝珠!"}
if attrs.size() >= 5:
return {"ok": false, "reason": "ALREADY_HAS_5_ATTRS", "msg": "该装备已拥有 5 条属性,达到上限!"}
var rate: int = ADD_PERCENT_TABLE[4] # 30%
var roll: int = randi_range(1, 100)
var is_succ: bool = force_success or (roll <= rate)
if not is_succ:
return {"ok": true, "success": false, "msg": "祝福宝珠破碎,未能为装备附加第 5 条属性!"}
var new_attr = _roll_random_attribute(attrs, target.get("type", 1))
attrs.append(new_attr)
return {
"ok": true,
"success": true,
"new_attr": new_attr,
"total_attrs": 5,
"msg": "神圣光芒降临!成功为装备铭刻第 5 条核心属性【%s +%d】!" % [new_attr["name"], new_attr["value"]]
}
# 71151: 稀有属性附加 (第 6~7 条)
func _handle_rare_add(target: Dictionary, force_success: bool) -> Dictionary:
var rares: Array = target["rare_attrs"]
if rares.size() >= 2:
return {"ok": false, "reason": "MAX_RARE_ATTRS", "msg": "已达到 2 条稀有词条上限!"}
var roll: int = randi_range(1, 100)
var is_succ: bool = force_success or (roll <= RARE_ADD_PERCENT)
if not is_succ:
return {"ok": true, "success": false, "msg": "稀有附魔失败,未能附加稀有词条!"}
var new_rare = _roll_random_rare_attribute(rares)
rares.append(new_rare)
return {
"ok": true,
"success": true,
"rare_attr": new_rare,
"total_rares": rares.size(),
"msg": "稀有附魔成功!觉醒了金色稀有词条【%s +%d%%】!" % [new_rare["name"], new_rare["value"]]
}
# 71152: 稀有属性改变 (单独洗炼第 6~7 条)
func _handle_rare_change(target: Dictionary) -> Dictionary:
var rares: Array = target["rare_attrs"]
if rares.is_empty():
return {"ok": false, "reason": "NO_RARES_TO_CHANGE", "msg": "该装备尚无任何稀有词条!"}
var count: int = rares.size()
rares.clear()
for i in range(count):
var r = _roll_random_rare_attribute(rares)
rares.append(r)
return {
"ok": true,
"success": true,
"rare_attrs": rares,
"msg": "稀有词条洗炼成功!%d 条稀有属性已重新激活!" % count
}
# 生成不重复的普通属性
func _roll_random_attribute(existing_attrs: Array, item_type: int) -> Dictionary:
var existing_ids: Array = []
for a in existing_attrs:
existing_ids.append(a.get("type", 0))
# 选用武器或防具属性池
var pool = ItemAttrSystem.WEAPON_ATTR_POOL if item_type == 1 else ItemAttrSystem.ARMOR_ATTR_POOL
var candidates: Array = []
for type_id in pool.keys():
if not (type_id in existing_ids):
candidates.append(type_id)
if candidates.is_empty():
candidates = pool.keys()
var picked_id: int = candidates[randi() % candidates.size()]
var entry = pool[picked_id]
var tier_idx: int = randi() % entry["values"].size()
var val: int = entry["values"][tier_idx]
return {
"type": picked_id,
"name": entry["name"],
"value": val,
"tier": tier_idx + 1
}
# 生成不重复的稀有属性
func _roll_random_rare_attribute(existing_rares: Array) -> Dictionary:
var existing_ids: Array = []
for a in existing_rares:
existing_ids.append(a.get("type", 0))
var candidates: Array = []
for type_id in RARE_ATTR_POOL.keys():
if not (type_id in existing_ids):
candidates.append(type_id)
if candidates.is_empty():
candidates = RARE_ATTR_POOL.keys()
var picked_id: int = candidates[randi() % candidates.size()]
var entry = RARE_ATTR_POOL[picked_id]
var tier_idx: int = randi() % entry["values"].size()
var val: int = entry["values"][tier_idx]
return {
"type": picked_id,
"name": entry["name"],
"value": val,
"tier": tier_idx + 1
}