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