- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
162 lines
4.7 KiB
GDScript
162 lines
4.7 KiB
GDScript
# belt_system.gd —— Metin2 40250 腰带系统与专属药水背包 1:1
|
|
# 对照 40250 服务端 belt.cpp, char_item.cpp, uiinventory.py
|
|
class_name BeltSystem
|
|
extends RefCounted
|
|
|
|
signal belt_equipped(belt_name: String, slots_unlocked: int)
|
|
signal belt_unequipped()
|
|
signal belt_item_stored(slot: int, item_name: String)
|
|
signal belt_item_removed(slot: int)
|
|
|
|
const MAX_BELT_SLOTS := 16 # 最大 4x4 空间
|
|
|
|
# 允许存入腰带的消耗品/药水类型
|
|
const ALLOWED_CONSUMABLE_TYPES: Array = ["potion", "consumable", "dew", "elixir"]
|
|
|
|
# 已装备的腰带
|
|
var equipped_belt: Dictionary = {}
|
|
var belt_inventory: Array = [] # 16 个格子
|
|
|
|
func _init() -> void:
|
|
belt_inventory.resize(MAX_BELT_SLOTS)
|
|
for i in range(MAX_BELT_SLOTS):
|
|
belt_inventory[i] = null
|
|
|
|
# 根据腰带强化等级计算开放的药水槽位数量
|
|
static func calculate_unlocked_slots(refine_level: int) -> int:
|
|
if refine_level >= 9:
|
|
return 16 # 4x4
|
|
elif refine_level >= 7:
|
|
return 12 # 3x4
|
|
elif refine_level >= 5:
|
|
return 10
|
|
elif refine_level >= 3:
|
|
return 8 # 2x4
|
|
elif refine_level >= 1:
|
|
return 6
|
|
else:
|
|
return 4 # 1x4
|
|
|
|
# 获取当前开放的药水格子数
|
|
func get_unlocked_slot_count() -> int:
|
|
if equipped_belt.is_empty():
|
|
return 0
|
|
var refine: int = int(equipped_belt.get("refine_level", 0))
|
|
return calculate_unlocked_slots(refine)
|
|
|
|
# 佩戴腰带
|
|
func equip_belt(belt_item: Dictionary) -> Dictionary:
|
|
if belt_item.is_empty():
|
|
return {"ok": false, "reason": "EMPTY_BELT", "msg": "腰带数据为空!"}
|
|
|
|
equipped_belt = belt_item.duplicate(true)
|
|
var unlocked := get_unlocked_slot_count()
|
|
var bname: String = equipped_belt.get("name", "腰带")
|
|
|
|
belt_equipped.emit(bname, unlocked)
|
|
|
|
return {
|
|
"ok": true,
|
|
"belt": equipped_belt,
|
|
"unlocked_slots": unlocked,
|
|
"msg": "已佩戴【%s】,解锁了 %d 格专属药水空间!" % [bname, unlocked]
|
|
}
|
|
|
|
# 卸下腰带 (需确保腰带内物品能够全部安全移出或为空)
|
|
func unequip_belt(main_inventory: Array) -> Dictionary:
|
|
if equipped_belt.is_empty():
|
|
return {"ok": false, "reason": "NO_BELT_EQUIPPED", "msg": "当前未佩戴腰带!"}
|
|
|
|
# 检查腰带内是否有物品
|
|
var items_in_belt: Array = []
|
|
for i in range(MAX_BELT_SLOTS):
|
|
if belt_inventory[i] != null:
|
|
items_in_belt.append({"belt_slot": i, "item": belt_inventory[i]})
|
|
|
|
if not items_in_belt.is_empty():
|
|
# 统计主背包空位
|
|
var empty_slots: Array = []
|
|
for j in range(main_inventory.size()):
|
|
if main_inventory[j] == null:
|
|
empty_slots.append(j)
|
|
|
|
if empty_slots.size() < items_in_belt.size():
|
|
return {
|
|
"ok": false,
|
|
"reason": "BELT_NOT_EMPTY",
|
|
"items_count": items_in_belt.size(),
|
|
"empty_slots": empty_slots.size(),
|
|
"msg": "主背包空间不足以容纳腰带内的药水,无法卸下腰带!"
|
|
}
|
|
|
|
# 移入主背包
|
|
for k in range(items_in_belt.size()):
|
|
var m_slot: int = empty_slots[k]
|
|
var it = items_in_belt[k]["item"]
|
|
var b_slot: int = items_in_belt[k]["belt_slot"]
|
|
main_inventory[m_slot] = it
|
|
belt_inventory[b_slot] = null
|
|
|
|
var old_belt := equipped_belt.duplicate(true)
|
|
equipped_belt.clear()
|
|
belt_unequipped.emit()
|
|
|
|
return {
|
|
"ok": true,
|
|
"belt": old_belt,
|
|
"msg": "腰带已卸下!内部药水已全部归还至主背包。"
|
|
}
|
|
|
|
# 存放药水至腰带专属背包
|
|
func store_item_in_belt(slot_idx: int, item: Dictionary) -> Dictionary:
|
|
if equipped_belt.is_empty():
|
|
return {"ok": false, "reason": "NO_BELT_EQUIPPED", "msg": "未佩戴腰带,无法使用专属药水栏!"}
|
|
|
|
var unlocked := get_unlocked_slot_count()
|
|
if slot_idx < 0 or slot_idx >= unlocked:
|
|
return {
|
|
"ok": false,
|
|
"reason": "SLOT_LOCKED_OR_INVALID",
|
|
"unlocked_slots": unlocked,
|
|
"msg": "该腰带槽位尚未解锁或无效!当前腰带只开放了前 %d 格。" % unlocked
|
|
}
|
|
|
|
# 药品类型校验
|
|
var it_type: String = item.get("item_type", "")
|
|
var is_potion: bool = ALLOWED_CONSUMABLE_TYPES.has(it_type) or item.get("is_potion", false)
|
|
if not is_potion:
|
|
return {
|
|
"ok": false,
|
|
"reason": "NOT_A_POTION",
|
|
"msg": "腰带栏位专用于补给品,只能存放药水、灵药与神圣露水!"
|
|
}
|
|
|
|
belt_inventory[slot_idx] = item.duplicate(true)
|
|
var iname: String = item.get("name", "药水")
|
|
belt_item_stored.emit(slot_idx, iname)
|
|
|
|
return {
|
|
"ok": true,
|
|
"slot_idx": slot_idx,
|
|
"item": belt_inventory[slot_idx],
|
|
"msg": "已将【%s】置入腰带快捷栏!" % iname
|
|
}
|
|
|
|
# 从腰带栏取出药水
|
|
func take_item_from_belt(slot_idx: int) -> Dictionary:
|
|
if slot_idx < 0 or slot_idx >= MAX_BELT_SLOTS:
|
|
return {"ok": false, "reason": "INVALID_SLOT"}
|
|
|
|
var item = belt_inventory[slot_idx]
|
|
if item == null:
|
|
return {"ok": false, "reason": "EMPTY_SLOT"}
|
|
|
|
belt_inventory[slot_idx] = null
|
|
belt_item_removed.emit(slot_idx)
|
|
|
|
return {
|
|
"ok": true,
|
|
"slot_idx": slot_idx,
|
|
"item": item
|
|
}
|