- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
232 lines
9.8 KiB
GDScript
232 lines
9.8 KiB
GDScript
# item_attr_system.gd —— 装备附魔与属性洗炼系统(40250 官方 1:1 对齐)
|
||
# 对齐源码:
|
||
# - 40250 char_item.cpp:4658-4720 (USE_ADD_ATTRIBUTE, Vnum 71085)
|
||
# - 40250 char_item.cpp:4553-4657 (USE_CHANGE_ATTRIBUTE, Vnum 71084)
|
||
# - 40250 char_item.cpp:4728-4780 (USE_ADD_ATTRIBUTE2 / 祝福宝珠, Vnum 70024)
|
||
# - 40250 constants.cpp:932 (aiItemAttributeAddPercent = [100, 80, 60, 50, 30, 0, 0])
|
||
# - 40250 item_attribute.cpp (PutAttributeWithLevel, HasAttr, GetAttributeSetIndex)
|
||
class_name ItemAttrSystem
|
||
extends RefCounted
|
||
|
||
# 核心 Vnum 常量(40250 官方标准)
|
||
const VNUM_ATTR_ADD := 71085 # 物品属性添加秘籍 (USE_ADD_ATTRIBUTE, 添加第 1~4 条)
|
||
const VNUM_ATTR_CHANGE := 71084 # 物品属性转换秘籍 (USE_CHANGE_ATTRIBUTE, 重置现有全部属性)
|
||
const VNUM_BLESSING_MARBLE := 70024 # 祝福宝珠 / 第五属性石 (USE_ADD_ATTRIBUTE2, 添加第 5 条)
|
||
|
||
# 40250 constants.cpp:932 官方附魔成功率表(前 4 条及第 5 条宝珠)
|
||
# 索引:现有词条数 -> 成功概率百分比
|
||
const ADD_PERCENT_TABLE := [100, 80, 60, 50, 30]
|
||
|
||
# 装备类型大类(ItemData.h)
|
||
const ITEM_TYPE_WEAPON := 1
|
||
const ITEM_TYPE_ARMOR := 2
|
||
|
||
# 属性池配置:针对武器与防具的标准 40250 词条池与数值范围(5 档位)
|
||
# 每个 apply_type: { name, values: [lv1, lv2, lv3, lv4, lv5], prob_weight }
|
||
const WEAPON_ATTR_POOL := {
|
||
1: {"name": "最大生命", "values": [500, 1000, 1500, 2000, 2500], "weight": 10},
|
||
2: {"name": "最大法力", "values": [100, 200, 300, 500, 800], "weight": 10},
|
||
3: {"name": "体质", "values": [2, 4, 6, 8, 12], "weight": 8},
|
||
4: {"name": "智力", "values": [2, 4, 6, 8, 12], "weight": 8},
|
||
5: {"name": "力量", "values": [2, 4, 6, 8, 12], "weight": 8},
|
||
6: {"name": "敏捷", "values": [2, 4, 6, 8, 12], "weight": 8},
|
||
7: {"name": "攻击速度", "values": [2, 4, 6, 8, 10], "weight": 8},
|
||
9: {"name": "施法速度", "values": [4, 8, 12, 16, 20], "weight": 8},
|
||
13: {"name": "眩晕概率", "values": [2, 3, 5, 8, 10], "weight": 6},
|
||
14: {"name": "缓慢概率", "values": [2, 3, 5, 8, 10], "weight": 6},
|
||
15: {"name": "暴击概率", "values": [2, 4, 6, 8, 10], "weight": 7},
|
||
16: {"name": "穿透概率", "values": [2, 4, 6, 8, 10], "weight": 7},
|
||
17: {"name": "对人族伤害", "values": [2, 4, 6, 8, 10], "weight": 6},
|
||
19: {"name": "对兽人伤害", "values": [4, 6, 10, 14, 20], "weight": 8},
|
||
21: {"name": "对不死族伤害", "values": [4, 6, 10, 14, 20], "weight": 8},
|
||
22: {"name": "对恶魔伤害", "values": [4, 6, 10, 14, 20], "weight": 8},
|
||
53: {"name": "攻击等级", "values": [10, 20, 30, 40, 50], "weight": 7},
|
||
}
|
||
|
||
const ARMOR_ATTR_POOL := {
|
||
1: {"name": "最大生命", "values": [500, 1000, 1500, 2000, 2500], "weight": 12},
|
||
2: {"name": "最大法力", "values": [100, 200, 300, 500, 800], "weight": 10},
|
||
7: {"name": "攻击速度", "values": [2, 4, 6, 8, 10], "weight": 8},
|
||
8: {"name": "移动速度", "values": [2, 4, 6, 8, 10], "weight": 8},
|
||
9: {"name": "施法速度", "values": [4, 8, 12, 16, 20], "weight": 8},
|
||
23: {"name": "生命吸收", "values": [2, 3, 5, 8, 10], "weight": 7},
|
||
24: {"name": "法力吸收", "values": [2, 3, 5, 8, 10], "weight": 7},
|
||
27: {"name": "格挡", "values": [2, 4, 6, 8, 10], "weight": 7},
|
||
29: {"name": "剑防御", "values": [4, 6, 10, 12, 15], "weight": 6},
|
||
30: {"name": "双手防御", "values": [4, 6, 10, 12, 15], "weight": 6},
|
||
31: {"name": "匕首防御", "values": [4, 6, 10, 12, 15], "weight": 6},
|
||
34: {"name": "弓防御", "values": [4, 6, 10, 12, 15], "weight": 6},
|
||
35: {"name": "火抗性", "values": [4, 6, 10, 12, 15], "weight": 7},
|
||
37: {"name": "魔法抗性", "values": [4, 6, 10, 12, 15], "weight": 7},
|
||
38: {"name": "风抗性", "values": [4, 6, 10, 12, 15], "weight": 7},
|
||
54: {"name": "防御等级", "values": [10, 15, 20, 30, 40], "weight": 8},
|
||
}
|
||
|
||
## 判定物品是否是附魔相关卷轴
|
||
static func is_attr_scroll(vnum: int) -> bool:
|
||
return vnum in [VNUM_ATTR_ADD, VNUM_ATTR_CHANGE, VNUM_BLESSING_MARBLE]
|
||
|
||
## 判定目标物品是否可附魔/洗炼(武器或防具装备,对应 40250 GetAttributeSetIndex != -1)
|
||
static func can_have_attributes(target_proto: Dictionary) -> bool:
|
||
var item_type := int(target_proto.get("type", 0))
|
||
if item_type == ITEM_TYPE_WEAPON:
|
||
var sub_type := int(target_proto.get("sub_type", 0))
|
||
return sub_type != 5 # 箭矢 (WEAPON_ARROW) 不可附魔
|
||
elif item_type == ITEM_TYPE_ARMOR:
|
||
return true
|
||
return false
|
||
|
||
## 获取目标装备对应的属性池
|
||
static func get_attr_pool(target_proto: Dictionary) -> Dictionary:
|
||
var item_type := int(target_proto.get("type", 0))
|
||
if item_type == ITEM_TYPE_WEAPON:
|
||
return WEAPON_ATTR_POOL
|
||
return ARMOR_ATTR_POOL
|
||
|
||
## 执行附魔添加属性(USE_ADD_ATTRIBUTE 71085)
|
||
## 40250 原生逻辑:词条数 < 4;根据已有条数判定成功率;成功则随机一条不重复属性与档次
|
||
static func apply_add_attribute(target_item: Dictionary, target_proto: Dictionary, force_success := false) -> Dictionary:
|
||
if not can_have_attributes(target_proto):
|
||
return {"ok": false, "code": "CANNOT_ATTACH_ATTR", "msg": "该物品无法添加属性。"}
|
||
|
||
var attrs: Array = target_item.get("attrs", []).duplicate(true)
|
||
if attrs.size() >= 4:
|
||
return {"ok": false, "code": "MAX_ATTR_REACHED", "msg": "此物品已有 4 条或更多属性,无法再添加基础属性。"}
|
||
|
||
var cur_count := attrs.size()
|
||
var rate: int = int(ADD_PERCENT_TABLE[cur_count]) if cur_count < ADD_PERCENT_TABLE.size() else 0
|
||
var roll := 0 if force_success else (randi() % 100 + 1)
|
||
|
||
if not force_success and roll > rate:
|
||
return {
|
||
"ok": false,
|
||
"code": "ADD_ATTR_FAIL",
|
||
"msg": "属性添加失败。",
|
||
"consumed": true,
|
||
}
|
||
|
||
var pool := get_attr_pool(target_proto)
|
||
var existing_types := {}
|
||
for a in attrs:
|
||
existing_types[int(a.get("type", 0))] = true
|
||
|
||
var avail_keys: Array[int] = []
|
||
for k in pool.keys():
|
||
if not existing_types.has(int(k)):
|
||
avail_keys.append(int(k))
|
||
|
||
if avail_keys.is_empty():
|
||
return {"ok": false, "code": "NO_AVAILABLE_ATTR", "msg": "无可添加的属性。"}
|
||
|
||
# 按权重随机选取属性
|
||
var chosen_type: int = _pick_weighted_type(avail_keys, pool)
|
||
var grade := randi() % 5 # 0..4 (对应 1~5 档)
|
||
var value: int = int(pool[chosen_type]["values"][grade])
|
||
|
||
attrs.append({"type": chosen_type, "value": value})
|
||
target_item["attrs"] = attrs
|
||
|
||
return {
|
||
"ok": true,
|
||
"code": "SUCCESS",
|
||
"msg": "属性添加成功!",
|
||
"consumed": true,
|
||
"added_attr": {"type": chosen_type, "value": value, "name": pool[chosen_type]["name"]},
|
||
}
|
||
|
||
## 执行洗炼重置全部属性(USE_CHANGE_ATTRIBUTE 71084)
|
||
## 40250 原生逻辑:词条数 > 0;保留现有词条总数,全部重新随机生成类型与数值
|
||
static func apply_change_attribute(target_item: Dictionary, target_proto: Dictionary) -> Dictionary:
|
||
if not can_have_attributes(target_proto):
|
||
return {"ok": false, "code": "CANNOT_ATTACH_ATTR", "msg": "该物品无法重置属性。"}
|
||
|
||
var attrs: Array = target_item.get("attrs", []).duplicate(true)
|
||
var count := attrs.size()
|
||
if count == 0:
|
||
return {"ok": false, "code": "NO_ATTRIBUTES", "msg": "此物品没有任何额外属性,无法洗炼。"}
|
||
|
||
var pool := get_attr_pool(target_proto)
|
||
var new_attrs: Array = []
|
||
var used_types := {}
|
||
|
||
for _i in count:
|
||
var avail_keys: Array[int] = []
|
||
for k in pool.keys():
|
||
if not used_types.has(int(k)):
|
||
avail_keys.append(int(k))
|
||
if avail_keys.is_empty():
|
||
break
|
||
var chosen_type: int = _pick_weighted_type(avail_keys, pool)
|
||
used_types[chosen_type] = true
|
||
var grade := randi() % 5
|
||
var val: int = int(pool[chosen_type]["values"][grade])
|
||
new_attrs.append({"type": chosen_type, "value": val})
|
||
|
||
target_item["attrs"] = new_attrs
|
||
return {
|
||
"ok": true,
|
||
"code": "SUCCESS",
|
||
"msg": "属性重置成功!",
|
||
"consumed": true,
|
||
"attrs": new_attrs,
|
||
}
|
||
|
||
## 执行第五属性宝珠(USE_ADD_ATTRIBUTE2 / 祝福宝珠 70024)
|
||
## 40250 原生逻辑:严格要求现有词条数 == 4;成功率 30%(40250 ADD_PERCENT_TABLE[4])
|
||
static func apply_blessing_marble(target_item: Dictionary, target_proto: Dictionary, force_success := false) -> Dictionary:
|
||
if not can_have_attributes(target_proto):
|
||
return {"ok": false, "code": "CANNOT_ATTACH_ATTR", "msg": "该物品无法添加第五属性。"}
|
||
|
||
var attrs: Array = target_item.get("attrs", []).duplicate(true)
|
||
if attrs.size() < 4:
|
||
return {"ok": false, "code": "NEED_FOUR_ATTRS", "msg": "装备必须先拥有 4 条属性,才能使用祝福宝珠添加第 5 条属性。"}
|
||
if attrs.size() >= 5:
|
||
return {"ok": false, "code": "ALREADY_FIVE_ATTRS", "msg": "该物品已达 5 条属性上限。"}
|
||
|
||
var rate: int = int(ADD_PERCENT_TABLE[4]) # 30%
|
||
var roll := 0 if force_success else (randi() % 100 + 1)
|
||
if not force_success and roll > rate:
|
||
return {
|
||
"ok": false,
|
||
"code": "MARBLE_FAIL",
|
||
"msg": "祝福宝珠开启第 5 条属性失败。",
|
||
"consumed": true,
|
||
}
|
||
|
||
var pool := get_attr_pool(target_proto)
|
||
var existing_types := {}
|
||
for a in attrs:
|
||
existing_types[int(a.get("type", 0))] = true
|
||
|
||
var avail_keys: Array[int] = []
|
||
for k in pool.keys():
|
||
if not existing_types.has(int(k)):
|
||
avail_keys.append(int(k))
|
||
|
||
var chosen_type: int = _pick_weighted_type(avail_keys, pool)
|
||
var grade := randi() % 5
|
||
var val: int = int(pool[chosen_type]["values"][grade])
|
||
|
||
attrs.append({"type": chosen_type, "value": val})
|
||
target_item["attrs"] = attrs
|
||
|
||
return {
|
||
"ok": true,
|
||
"code": "SUCCESS",
|
||
"msg": "祝福宝珠成功开启第 5 条属性!",
|
||
"consumed": true,
|
||
"added_attr": {"type": chosen_type, "value": val, "name": pool[chosen_type]["name"]},
|
||
}
|
||
|
||
static func _pick_weighted_type(avail_keys: Array[int], pool: Dictionary) -> int:
|
||
var total_weight := 0
|
||
for k in avail_keys:
|
||
total_weight += int(pool[k].get("weight", 1))
|
||
var r := randi() % maxi(1, total_weight)
|
||
var accum := 0
|
||
for k in avail_keys:
|
||
accum += int(pool[k].get("weight", 1))
|
||
if r < accum:
|
||
return k
|
||
return avail_keys[0]
|