Files
mtgodot-poc/project/costume_attr_transfer_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

245 lines
9.3 KiB
GDScript

# costume_attr_transfer_system.gd —— Metin2 40250 官方时装属性转移与增益附魔系统 1:1
# 100% 对照 40250 服务端 char_item.cpp (ITEM_COSTUME, 70063, 70064), item_proto.txt
class_name CostumeAttrTransferSystem
extends RefCounted
signal costume_transformed(costume_vnum: int, new_attrs: Array)
signal costume_enchanted(costume_vnum: int, new_attrs: Array)
signal costume_attributes_transferred(src_vnum: int, dst_vnum: int, attrs: Array)
signal costume_error(reason: String)
# 40250 官方时装重铸与附魔道具 Vnum
const VNUM_COSTUME_TRANSFORM := 70063 # 时装重铸卷轴 (Kostümü dönüştür - 随机 1~3 条新属性)
const VNUM_COSTUME_ENCHANT := 70064 # 时装附魔石 (Kostümü efsunla - 保留条数重洗属性)
const VNUM_ATTR_TRANSFER := 72325 # 时装属性转移符
# 时装最大词条数
const MAX_COSTUME_ATTRS := 3
# 40250 官方时装属性词条候选池
const COSTUME_ATTR_POOL: Array[Dictionary] = [
{"type": "max_hp", "name": "生命上限", "min": 500, "max": 2000},
{"type": "max_sp", "name": "法力上限", "min": 200, "max": 1000},
{"type": "crit_pct", "name": "暴击几率 (%)", "min": 5, "max": 10},
{"type": "penetrate_pct", "name": "穿刺伤害几率 (%)", "min": 5, "max": 10},
{"type": "att_speed", "name": "攻击速度", "min": 5, "max": 10},
{"type": "mov_speed", "name": "移动速度", "min": 5, "max": 10},
{"type": "cast_speed", "name": "施法速度", "min": 5, "max": 10},
{"type": "hp_regen", "name": "生命自然恢复 (%)", "min": 10, "max": 30}
]
# 校验是否为时装物品
func is_costume_item(item: Dictionary) -> bool:
if item == null:
return false
var subtype = str(item.get("costume_subtype", ""))
var is_costume = bool(item.get("is_costume", false))
var vnum = int(item.get("vnum", 0))
# 40250 官方区间:时装铠甲 (41001~44000), 时装发型 (45001~46000), 时装武器 (40101~)
if is_costume or subtype != "":
return true
if (vnum >= 41001 and vnum <= 46000) or (vnum >= 40101 and vnum <= 40999):
return true
return false
# ==========================================
# 1. 时装重铸 (70063 Kostümü dönüştür)
# ==========================================
# 移除所有既有属性,随机赋予 1~3 条全新的时装属性
func transform_costume(
inventory: Array,
scroll_slot: int,
costume_slot: int,
forced_count: int = -1,
forced_attrs: Array = []
) -> Dictionary:
if not _validate_slots(inventory, scroll_slot, costume_slot):
costume_error.emit("INVALID_SLOT")
return {"ok": false, "reason": "INVALID_SLOT", "msg": "格位无效或道具为空!"}
var scroll = inventory[scroll_slot]
var costume = inventory[costume_slot]
if int(scroll.get("vnum", 0)) != VNUM_COSTUME_TRANSFORM:
costume_error.emit("NOT_TRANSFORM_SCROLL")
return {"ok": false, "reason": "NOT_TRANSFORM_SCROLL", "msg": "该道具不是时装重铸卷轴!"}
if not is_costume_item(costume):
costume_error.emit("NOT_COSTUME")
return {"ok": false, "reason": "NOT_COSTUME", "msg": "只有时装物品可以使用时装重铸卷轴!"}
if bool(costume.get("is_equipped", false)):
costume_error.emit("EQUIPPED_ITEM")
return {"ok": false, "reason": "EQUIPPED_ITEM", "msg": "无法对穿戴中的时装进行重铸,请先卸下!"}
# 消耗 1 个重铸卷轴
_consume_item_one(inventory, scroll_slot)
# 决定新属性条数 (1~3 条)
var count = forced_count if forced_count in [1, 2, 3] else randi_range(1, 3)
var new_attrs: Array = []
if not forced_attrs.is_empty():
new_attrs = forced_attrs.duplicate(true)
else:
new_attrs = _roll_random_attributes(count)
costume["attrs"] = new_attrs
costume_transformed.emit(int(costume.get("vnum", 0)), new_attrs)
return {
"ok": true,
"attrs_count": new_attrs.size(),
"attrs": new_attrs,
"msg": "时装周身流光溢彩!重铸成功,获得了 %d 条全新的时装属性。" % new_attrs.size()
}
# ==========================================
# 2. 时装附魔 (70064 Kostümü efsunla)
# ==========================================
# 保留当前属性条数,重新随机洗练词条类型与数值
func enchant_costume(
inventory: Array,
scroll_slot: int,
costume_slot: int,
forced_attrs: Array = []
) -> Dictionary:
if not _validate_slots(inventory, scroll_slot, costume_slot):
costume_error.emit("INVALID_SLOT")
return {"ok": false, "reason": "INVALID_SLOT", "msg": "格位无效或道具为空!"}
var scroll = inventory[scroll_slot]
var costume = inventory[costume_slot]
if int(scroll.get("vnum", 0)) != VNUM_COSTUME_ENCHANT:
costume_error.emit("NOT_ENCHANT_SCROLL")
return {"ok": false, "reason": "NOT_ENCHANT_SCROLL", "msg": "该道具不是时装附魔石!"}
if not is_costume_item(costume):
costume_error.emit("NOT_COSTUME")
return {"ok": false, "reason": "NOT_COSTUME", "msg": "只有时装物品可以使用时装附魔石!"}
if bool(costume.get("is_equipped", false)):
costume_error.emit("EQUIPPED_ITEM")
return {"ok": false, "reason": "EQUIPPED_ITEM", "msg": "无法对穿戴中的时装进行附魔,请先卸下!"}
var existing_attrs: Array = costume.get("attrs", [])
if existing_attrs.is_empty():
costume_error.emit("NO_ATTRS_TO_ENCHANT")
return {"ok": false, "reason": "NO_ATTRS_TO_ENCHANT", "msg": "此时装没有任何附加属性!请先使用时装重铸卷轴赋予属性。"}
# 消耗 1 个附魔石
_consume_item_one(inventory, scroll_slot)
var count = existing_attrs.size()
var new_attrs: Array = []
if not forced_attrs.is_empty():
new_attrs = forced_attrs.duplicate(true)
else:
new_attrs = _roll_random_attributes(count)
costume["attrs"] = new_attrs
costume_enchanted.emit(int(costume.get("vnum", 0)), new_attrs)
return {
"ok": true,
"attrs_count": new_attrs.size(),
"attrs": new_attrs,
"msg": "附魔灵石融入织物!保留原有 %d 条属性槽位,洗练出了全新的强力词条。" % count
}
# ==========================================
# 3. 时装属性转移 (Costume Attribute Transfer)
# ==========================================
func transfer_costume_attributes(
inventory: Array,
scroll_slot: int,
src_slot: int,
dst_slot: int
) -> Dictionary:
if scroll_slot < 0 or scroll_slot >= inventory.size() or inventory[scroll_slot] == null:
costume_error.emit("INVALID_SLOT")
return {"ok": false, "reason": "INVALID_SLOT", "msg": "转移卷轴格位无效!"}
if src_slot < 0 or src_slot >= inventory.size() or inventory[src_slot] == null:
costume_error.emit("INVALID_SLOT")
return {"ok": false, "reason": "INVALID_SLOT", "msg": "源时装格位无效!"}
if dst_slot < 0 or dst_slot >= inventory.size() or inventory[dst_slot] == null:
costume_error.emit("INVALID_SLOT")
return {"ok": false, "reason": "INVALID_SLOT", "msg": "目标时装格位无效!"}
var scroll = inventory[scroll_slot]
var src_item = inventory[src_slot]
var dst_item = inventory[dst_slot]
if int(scroll.get("vnum", 0)) != VNUM_ATTR_TRANSFER:
costume_error.emit("NOT_TRANSFER_SCROLL")
return {"ok": false, "reason": "NOT_TRANSFER_SCROLL", "msg": "该物品不是时装属性转移符!"}
if not is_costume_item(src_item) or not is_costume_item(dst_item):
costume_error.emit("NOT_COSTUME")
return {"ok": false, "reason": "NOT_COSTUME", "msg": "源道具与目标道具都必须是时装!"}
var src_type = str(src_item.get("costume_subtype", "body"))
var dst_type = str(dst_item.get("costume_subtype", "body"))
if src_type != dst_type:
costume_error.emit("TYPE_MISMATCH")
return {"ok": false, "reason": "TYPE_MISMATCH", "msg": "时装类型不匹配!时装铠甲只能转移至时装铠甲,发型只能转移至发型。"}
var src_attrs: Array = src_item.get("attrs", [])
if src_attrs.is_empty():
costume_error.emit("NO_ATTRS_TO_TRANSFER")
return {"ok": false, "reason": "NO_ATTRS_TO_TRANSFER", "msg": "源时装没有任何可转移的属性!"}
# 消耗 1 张转移符
_consume_item_one(inventory, scroll_slot)
# 执行无损属性迁移
var transferred = src_attrs.duplicate(true)
dst_item["attrs"] = transferred
src_item["attrs"] = [] # 源时装词条清空
costume_attributes_transferred.emit(int(src_item.get("vnum", 0)), int(dst_item.get("vnum", 0)), transferred)
return {
"ok": true,
"transferred_attrs": transferred,
"msg": "乾坤倒转!成功将 %d 条极品属性从旧时装完整迁移至新时装。" % transferred.size()
}
# 内部随机属性抽取工具
func _roll_random_attributes(count: int) -> Array:
var result: Array = []
var pool_indices: Array[int] = []
for i in range(COSTUME_ATTR_POOL.size()):
pool_indices.append(i)
pool_indices.shuffle()
var actual_count = min(count, pool_indices.size())
for i in range(actual_count):
var def = COSTUME_ATTR_POOL[pool_indices[i]]
var val = randi_range(int(def["min"]), int(def["max"]))
result.append({
"type": def["type"],
"name": def["name"],
"value": val
})
return result
func _validate_slots(inventory: Array, s1: int, s2: int) -> bool:
if s1 < 0 or s1 >= inventory.size() or inventory[s1] == null:
return false
if s2 < 0 or s2 >= inventory.size() or inventory[s2] == null:
return false
return true
func _consume_item_one(inventory: Array, slot: int) -> void:
if slot < 0 or slot >= inventory.size() or inventory[slot] == null:
return
var item = inventory[slot]
var count = int(item.get("count", 1))
if count > 1:
item["count"] = count - 1
else:
inventory[slot] = null