- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
127 lines
4.2 KiB
GDScript
127 lines
4.2 KiB
GDScript
# costume_system.gd —— Metin2 40250 独立时装发型与武器衣柜系统 1:1
|
|
# 对照 40250 服务端 costume.cpp, char_item.cpp, uiinventory.py, item_data.h
|
|
class_name CostumeSystem
|
|
extends RefCounted
|
|
|
|
signal costume_equipped(slot_type: int, item: Dictionary)
|
|
signal costume_unequipped(slot_type: int)
|
|
signal costume_expired(slot_type: int, item_name: String)
|
|
|
|
enum CostumeSlot {
|
|
COSTUME_BODY = 0, # 时装衣服
|
|
COSTUME_HAIR = 1, # 时装发型
|
|
COSTUME_WEAPON = 2 # 时装武器外观
|
|
}
|
|
|
|
const DEFAULT_COSTUME_DURATION := 604800.0 # 默认 7 天 (7 * 86400)
|
|
|
|
var equipped_costumes: Dictionary = {
|
|
CostumeSlot.COSTUME_BODY: null,
|
|
CostumeSlot.COSTUME_HAIR: null,
|
|
CostumeSlot.COSTUME_WEAPON: null
|
|
}
|
|
|
|
# 穿戴时装
|
|
func equip_costume(slot_type: int, costume_item: Dictionary) -> Dictionary:
|
|
if not equipped_costumes.has(slot_type):
|
|
return {"ok": false, "reason": "INVALID_SLOT", "msg": "无效的时装栏位!"}
|
|
|
|
if costume_item.is_empty():
|
|
return {"ok": false, "reason": "EMPTY_ITEM", "msg": "时装物品数据为空!"}
|
|
|
|
# 校验时装部位匹配
|
|
var c_type: String = costume_item.get("costume_type", "")
|
|
match slot_type:
|
|
CostumeSlot.COSTUME_BODY:
|
|
if c_type != "body":
|
|
return {"ok": false, "reason": "SLOT_TYPE_MISMATCH", "msg": "此物品不是时装衣服!"}
|
|
CostumeSlot.COSTUME_HAIR:
|
|
if c_type != "hair":
|
|
return {"ok": false, "reason": "SLOT_TYPE_MISMATCH", "msg": "此物品不是时装发型!"}
|
|
CostumeSlot.COSTUME_WEAPON:
|
|
if c_type != "weapon":
|
|
return {"ok": false, "reason": "SLOT_TYPE_MISMATCH", "msg": "此物品不是时装武器外观!"}
|
|
|
|
var old_item = equipped_costumes[slot_type]
|
|
var item_to_equip = costume_item.duplicate(true)
|
|
|
|
if not item_to_equip.has("remaining_time"):
|
|
item_to_equip["remaining_time"] = DEFAULT_COSTUME_DURATION
|
|
|
|
equipped_costumes[slot_type] = item_to_equip
|
|
|
|
costume_equipped.emit(slot_type, item_to_equip)
|
|
|
|
return {
|
|
"ok": true,
|
|
"slot_type": slot_type,
|
|
"item": item_to_equip,
|
|
"old_item": old_item,
|
|
"msg": "时装【%s】已成功穿戴!" % item_to_equip.get("name", "时装")
|
|
}
|
|
|
|
# 卸下时装
|
|
func unequip_costume(slot_type: int) -> Dictionary:
|
|
if not equipped_costumes.has(slot_type):
|
|
return {"ok": false, "reason": "INVALID_SLOT", "msg": "无效的时装栏位!"}
|
|
|
|
var item = equipped_costumes[slot_type]
|
|
if item == null:
|
|
return {"ok": false, "reason": "SLOT_EMPTY", "msg": "该时装栏位未穿戴时装!"}
|
|
|
|
equipped_costumes[slot_type] = null
|
|
costume_unequipped.emit(slot_type)
|
|
|
|
return {
|
|
"ok": true,
|
|
"slot_type": slot_type,
|
|
"item": item,
|
|
"msg": "已卸下时装【%s】。" % item.get("name", "时装")
|
|
}
|
|
|
|
# 汇总所有已穿戴时装的战力属性
|
|
func get_costume_bonuses() -> Dictionary:
|
|
var total_bonuses: Dictionary = {}
|
|
for slot in equipped_costumes.keys():
|
|
var it = equipped_costumes[slot]
|
|
if it == null:
|
|
continue
|
|
var bonuses: Dictionary = it.get("bonuses", {})
|
|
for b_key in bonuses.keys():
|
|
var val: int = int(bonuses[b_key])
|
|
total_bonuses[b_key] = total_bonuses.get(b_key, 0) + val
|
|
return total_bonuses
|
|
|
|
# 获取 3D 外观覆盖模型 ID
|
|
func get_appearance_overrides() -> Dictionary:
|
|
var overrides: Dictionary = {
|
|
"body_model": 0,
|
|
"hair_model": 0,
|
|
"weapon_model": 0
|
|
}
|
|
if equipped_costumes[CostumeSlot.COSTUME_BODY] != null:
|
|
overrides["body_model"] = int(equipped_costumes[CostumeSlot.COSTUME_BODY].get("model_id", 0))
|
|
if equipped_costumes[CostumeSlot.COSTUME_HAIR] != null:
|
|
overrides["hair_model"] = int(equipped_costumes[CostumeSlot.COSTUME_HAIR].get("model_id", 0))
|
|
if equipped_costumes[CostumeSlot.COSTUME_WEAPON] != null:
|
|
overrides["weapon_model"] = int(equipped_costumes[CostumeSlot.COSTUME_WEAPON].get("model_id", 0))
|
|
return overrides
|
|
|
|
# 逐帧时效衰减更新
|
|
func update(dt: float) -> Array:
|
|
var expired_list: Array = []
|
|
for slot in equipped_costumes.keys():
|
|
var it = equipped_costumes[slot]
|
|
if it == null:
|
|
continue
|
|
|
|
it["remaining_time"] = maxf(0.0, float(it.get("remaining_time", 0.0)) - dt)
|
|
if it["remaining_time"] <= 0.0:
|
|
var iname: String = it.get("name", "时装")
|
|
expired_list.append({"slot": slot, "item": it})
|
|
equipped_costumes[slot] = null
|
|
costume_expired.emit(slot, iname)
|
|
costume_unequipped.emit(slot)
|
|
|
|
return expired_list
|