Files
mtgodot-poc/project/item_attr_system.gd
T
shenandshen 66d217b313 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 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

232 lines
9.8 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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]