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
+146
View File
@@ -0,0 +1,146 @@
# wardrobe_transmog_system.gd —— Metin2 40250 装备外观幻化重塑与试衣间系统 1:1
# 对照 40250 服务端 item_transmutation.cpp, char_item.cpp
class_name WardrobeTransmogSystem
extends RefCounted
signal item_transmuted(target_slot: int, target_name: String, new_look_vnum: int, new_look_name: String)
signal transmutation_reverted(target_slot: int, item_name: String)
const VNUM_TRANSMOG_SCROLL := 72301 # 幻化秘籍
const VNUM_REVERT_SCROLL := 72302 # 幻化还原秘籍
const TRANSMOG_COST_GOLD := 10000000 # 1000 万金币
# 装备大类
const TYPE_WEAPON := 1
const TYPE_ARMOR := 2
const TYPE_COSTUME := 28
# 试衣间预览外观效果 (不消耗物品)
func preview_transmog(target_item: Dictionary, skin_item: Dictionary) -> Dictionary:
var val = _validate_compatibility(target_item, skin_item)
if not val["ok"]:
return val
return {
"ok": true,
"base_vnum": target_item.get("vnum", 0),
"base_name": target_item.get("name", "神装"),
"preview_vnum": skin_item.get("vnum", 0),
"preview_name": skin_item.get("name", "幻化外观"),
"msg": "试衣间预览生成完毕!"
}
# 执行幻化外观重塑
func apply_transmutation(scroll_slot: int, target_slot: int, skin_slot: int, inventory: Array, player_data: Dictionary) -> Dictionary:
if scroll_slot < 0 or scroll_slot >= inventory.size() or inventory[scroll_slot] == null:
return {"ok": false, "reason": "INVALID_SCROLL_SLOT"}
if target_slot < 0 or target_slot >= inventory.size() or inventory[target_slot] == null:
return {"ok": false, "reason": "INVALID_TARGET_SLOT"}
if skin_slot < 0 or skin_slot >= inventory.size() or inventory[skin_slot] == null:
return {"ok": false, "reason": "INVALID_SKIN_SLOT"}
if target_slot == skin_slot:
return {"ok": false, "reason": "SAME_SLOT", "msg": "目标装备与外观装备不能为同一格子!"}
var scroll = inventory[scroll_slot]
if int(scroll.get("vnum", 0)) != VNUM_TRANSMOG_SCROLL:
return {"ok": false, "reason": "NOT_A_TRANSMOG_SCROLL", "msg": "需要使用【幻化秘籍】(72301)"}
var target = inventory[target_slot]
var skin = inventory[skin_slot]
var comp_check = _validate_compatibility(target, skin)
if not comp_check["ok"]:
return comp_check
var gold = int(player_data.get("gold", 0))
if gold < TRANSMOG_COST_GOLD:
return {"ok": false, "reason": "NOT_ENOUGH_GOLD", "msg": "幻化所需金币不足!需要 10,000,000 金币。"}
# 扣除金币与卷轴
player_data["gold"] = gold - TRANSMOG_COST_GOLD
var sc_cnt = int(scroll.get("count", 1))
if sc_cnt > 1:
scroll["count"] = sc_cnt - 1
else:
inventory[scroll_slot] = null
# 消耗外观材料装备
var skin_vnum = int(skin.get("vnum", 0))
var skin_name = skin.get("name", "外观")
var sk_cnt = int(skin.get("count", 1))
if sk_cnt > 1:
skin["count"] = sk_cnt - 1
else:
inventory[skin_slot] = null
# 赋予目标装备幻化外观 (所有基础属性、强化与灵石 100% 保留)
target["transmog_vnum"] = skin_vnum
target["transmog_name"] = skin_name
var tname: String = target.get("name", "装备")
item_transmuted.emit(target_slot, tname, skin_vnum, skin_name)
return {
"ok": true,
"target_slot": target_slot,
"target_name": tname,
"transmog_vnum": skin_vnum,
"transmog_name": skin_name,
"msg": "幻化成功!【%s】披上了【%s】的华丽外观!" % [tname, skin_name]
}
# 还原装备原生外观
func revert_transmutation(scroll_slot: int, target_slot: int, inventory: Array) -> Dictionary:
if scroll_slot < 0 or scroll_slot >= inventory.size() or inventory[scroll_slot] == null:
return {"ok": false, "reason": "INVALID_SCROLL_SLOT"}
if target_slot < 0 or target_slot >= inventory.size() or inventory[target_slot] == null:
return {"ok": false, "reason": "INVALID_TARGET_SLOT"}
var scroll = inventory[scroll_slot]
if int(scroll.get("vnum", 0)) != VNUM_REVERT_SCROLL:
return {"ok": false, "reason": "NOT_A_REVERT_SCROLL", "msg": "需要使用【幻化还原秘籍】(72302)!"}
var target = inventory[target_slot]
if not target.has("transmog_vnum") or int(target.get("transmog_vnum", 0)) == 0:
return {"ok": false, "reason": "NOT_TRANSMUTED", "msg": "该装备未曾幻化,无需还原!"}
# 消耗还原卷轴
var cnt = int(scroll.get("count", 1))
if cnt > 1:
scroll["count"] = cnt - 1
else:
inventory[scroll_slot] = null
target.erase("transmog_vnum")
target.erase("transmog_name")
var tname: String = target.get("name", "装备")
transmutation_reverted.emit(target_slot, tname)
return {
"ok": true,
"target_slot": target_slot,
"item_name": tname,
"msg": "幻化已抹除!【%s】恢复为原生经典外观。" % tname
}
# 校验外观类型兼容性
func _validate_compatibility(target: Dictionary, skin: Dictionary) -> Dictionary:
if target.is_empty() or skin.is_empty():
return {"ok": false, "reason": "EMPTY_DATA"}
var t_type = int(target.get("type", 1))
var s_type = int(skin.get("type", 1))
# 武器只能幻化武器或时装武器外观;防具只能幻化防具或时装衣服
if t_type == TYPE_WEAPON:
if s_type != TYPE_WEAPON and s_type != TYPE_COSTUME:
return {"ok": false, "reason": "TYPE_MISMATCH", "msg": "武器只能幻化武器或时装外观!"}
elif t_type == TYPE_ARMOR:
if s_type != TYPE_ARMOR and s_type != TYPE_COSTUME:
return {"ok": false, "reason": "TYPE_MISMATCH", "msg": "防具衣服只能幻化衣服防具外观!"}
else:
return {"ok": false, "reason": "UNSUPPORTED_TYPE", "msg": "该装备部位暂不支持幻化外观。"}
return {"ok": true}