- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
249 lines
8.2 KiB
GDScript
249 lines
8.2 KiB
GDScript
# treasure_box_key_system.gd —— Metin2 40250 官方金银宝箱与专属钥匙拖拽开启系统 1:1
|
||
# 100% 对照 40250 服务端 char_item.cpp (ITEM_TREASURE_BOX, ITEM_TREASURE_KEY), special_item_group.txt, item_proto.txt
|
||
class_name TreasureBoxKeySystem
|
||
extends RefCounted
|
||
|
||
signal box_unlocked(box_vnum: int, key_vnum: int, reward: Dictionary)
|
||
signal unlock_failed(reason: String)
|
||
|
||
# 宝箱类型分类常数 (对照 item->GetValue(0))
|
||
const BOX_TYPE_SILVER := 1 # 银系列 (Silver)
|
||
const BOX_TYPE_GOLD := 2 # 金系列 (Gold)
|
||
|
||
# 40250 官方宝箱资料表 (ITEM_TREASURE_BOX)
|
||
const BOXES: Dictionary = {
|
||
50006: {
|
||
"name": "银宝箱 (Silver Treasure Box)",
|
||
"type_id": BOX_TYPE_SILVER,
|
||
"tr_name": "Gümüş Define Sandığı"
|
||
},
|
||
50007: {
|
||
"name": "金宝箱 (Gold Treasure Box)",
|
||
"type_id": BOX_TYPE_GOLD,
|
||
"tr_name": "Altın Define Sandığı"
|
||
},
|
||
50012: {
|
||
"name": "银宝箱+ (Silver Treasure Box+)",
|
||
"type_id": BOX_TYPE_SILVER,
|
||
"tr_name": "Gümüş Define Sandığı+"
|
||
},
|
||
50013: {
|
||
"name": "金宝箱+ (Gold Treasure Box+)",
|
||
"type_id": BOX_TYPE_GOLD,
|
||
"tr_name": "Altın Define Sandığı+"
|
||
}
|
||
}
|
||
|
||
# 40250 官方钥匙资料表 (ITEM_TREASURE_KEY)
|
||
const KEYS: Dictionary = {
|
||
50008: {
|
||
"name": "银钥匙 (Silver Key)",
|
||
"type_id": BOX_TYPE_SILVER,
|
||
"tr_name": "Gümüş Anahtar"
|
||
},
|
||
50009: {
|
||
"name": "金钥匙 (Gold Key)",
|
||
"type_id": BOX_TYPE_GOLD,
|
||
"tr_name": "Altın Anahtar"
|
||
},
|
||
50014: {
|
||
"name": "银钥匙+ (Silver Key+)",
|
||
"type_id": BOX_TYPE_SILVER,
|
||
"tr_name": "Gümüş Anahtar+"
|
||
},
|
||
50015: {
|
||
"name": "金钥匙+ (Gold Key+)",
|
||
"type_id": BOX_TYPE_GOLD,
|
||
"tr_name": "Altın Anahtar+"
|
||
}
|
||
}
|
||
|
||
# 40250 special_item_group.txt 官方掉落池
|
||
const DROP_TABLES: Dictionary = {
|
||
50006: [ # 银宝箱
|
||
{"type": "item", "vnum": 50300, "count": 1, "name": "技能书 (Skill Book)", "prob": 100},
|
||
{"type": "item", "vnum": 25040, "count": 1, "name": "祝福卷轴 (Blessing Scroll)", "prob": 40},
|
||
{"type": "gold", "amount": 80000, "name": "80,000 金币", "prob": 30},
|
||
{"type": "exp", "amount": 30000, "name": "30,000 经验值", "prob": 30},
|
||
{"type": "item", "vnum": 30047, "count": 1, "name": "诅咒之书 (Curse Book)", "prob": 20},
|
||
{"type": "item", "vnum": 30050, "count": 1, "name": "冰之碎片 (Ice Piece)", "prob": 20},
|
||
{"type": "item", "vnum": 70008, "count": 1, "name": "温暖葡萄酒", "prob": 15}
|
||
],
|
||
50007: [ # 金宝箱
|
||
{"type": "item", "vnum": 25040, "count": 1, "name": "祝福卷轴 (Blessing Scroll)", "prob": 60},
|
||
{"type": "item", "vnum": 70024, "count": 1, "name": "经验指环 (Ring of Experience)", "prob": 30},
|
||
{"type": "item", "vnum": 70003, "count": 1, "name": "小偷手套 (Thief Glove)", "prob": 30},
|
||
{"type": "gold", "amount": 150000, "name": "150,000 金币", "prob": 40},
|
||
{"type": "exp", "amount": 60000, "name": "60,000 经验值", "prob": 40},
|
||
{"type": "item", "vnum": 27003, "count": 50, "name": "红色大生命药水 x50", "prob": 50},
|
||
{"type": "item", "vnum": 27006, "count": 50, "name": "蓝色大魔法药水 x50", "prob": 50}
|
||
],
|
||
50012: [ # 银宝箱+
|
||
{"type": "item", "vnum": 50300, "count": 2, "name": "技能书 x2", "prob": 100},
|
||
{"type": "item", "vnum": 25040, "count": 1, "name": "祝福卷轴", "prob": 50},
|
||
{"type": "gold", "amount": 120000, "name": "120,000 金币", "prob": 40},
|
||
{"type": "item", "vnum": 70048, "count": 1, "name": "变装斗篷", "prob": 25}
|
||
],
|
||
50013: [ # 金宝箱+
|
||
{"type": "item", "vnum": 25040, "count": 2, "name": "祝福卷轴 x2", "prob": 80},
|
||
{"type": "item", "vnum": 70024, "count": 1, "name": "经验指环", "prob": 45},
|
||
{"type": "gold", "amount": 250000, "name": "250,000 金币", "prob": 50},
|
||
{"type": "exp", "amount": 100000, "name": "100,000 经验值", "prob": 50}
|
||
]
|
||
}
|
||
|
||
# 开启历史记录
|
||
var _open_history: Array[Dictionary] = []
|
||
|
||
# 判断是否是宝箱
|
||
func is_treasure_box(vnum: int) -> bool:
|
||
return BOXES.has(vnum)
|
||
|
||
# 判断是否是钥匙
|
||
func is_treasure_key(vnum: int) -> bool:
|
||
return KEYS.has(vnum)
|
||
|
||
# 校验钥匙与宝箱是否能够配对开启 (item->GetValue(0) == item2->GetValue(0))
|
||
func can_unlock(key_vnum: int, box_vnum: int) -> bool:
|
||
if not KEYS.has(key_vnum) or not BOXES.has(box_vnum):
|
||
return false
|
||
return int(KEYS[key_vnum]["type_id"]) == int(BOXES[box_vnum]["type_id"])
|
||
|
||
# 拖拽钥匙至宝箱进行解锁
|
||
func unlock_box_with_key(
|
||
inventory: Array,
|
||
key_slot: int,
|
||
box_slot: int,
|
||
forced_drop_idx: int = -1
|
||
) -> Dictionary:
|
||
if key_slot < 0 or key_slot >= inventory.size() or inventory[key_slot] == null:
|
||
unlock_failed.emit("INVALID_KEY_SLOT")
|
||
return {"ok": false, "reason": "INVALID_KEY_SLOT", "msg": "未找到指定的钥匙!"}
|
||
|
||
if box_slot < 0 or box_slot >= inventory.size() or inventory[box_slot] == null:
|
||
unlock_failed.emit("INVALID_BOX_SLOT")
|
||
return {"ok": false, "reason": "INVALID_BOX_SLOT", "msg": "未找到指定的宝箱!"}
|
||
|
||
var key_item = inventory[key_slot]
|
||
var box_item = inventory[box_slot]
|
||
|
||
var key_vnum = int(key_item.get("vnum", 0))
|
||
var box_vnum = int(box_item.get("vnum", 0))
|
||
|
||
# 检查物品分类是否为钥匙与宝箱
|
||
if not is_treasure_key(key_vnum):
|
||
unlock_failed.emit("NOT_A_KEY")
|
||
return {"ok": false, "reason": "NOT_A_KEY", "msg": "被拖拽的物品并不是一把宝箱钥匙!"}
|
||
|
||
if not is_treasure_box(box_vnum):
|
||
unlock_failed.emit("NOT_A_TREASURE_BOX")
|
||
# 40250 char_item.cpp:1948 "그 열쇠로는 열 수 없는 것 같군."
|
||
return {"ok": false, "reason": "NOT_A_TREASURE_BOX", "msg": "这把钥匙似乎无法用来开启这个物品。"}
|
||
|
||
# 检查金银分类匹配
|
||
if not can_unlock(key_vnum, box_vnum):
|
||
unlock_failed.emit("KEY_MISMATCH")
|
||
return {
|
||
"ok": false,
|
||
"reason": "KEY_MISMATCH",
|
||
"msg": "钥匙型号不吻合!%s 无法打开 %s。" % [KEYS[key_vnum]["name"], BOXES[box_vnum]["name"]]
|
||
}
|
||
|
||
# 配对成功:双双扣除 1 个数量
|
||
_consume_item_one(inventory, key_slot)
|
||
_consume_item_one(inventory, box_slot)
|
||
|
||
# 计算奖励
|
||
var drops: Array = DROP_TABLES.get(box_vnum, [])
|
||
if drops.is_empty():
|
||
drops = DROP_TABLES[50006]
|
||
|
||
var picked_reward: Dictionary = {}
|
||
if forced_drop_idx >= 0 and forced_drop_idx < drops.size():
|
||
picked_reward = drops[forced_drop_idx]
|
||
else:
|
||
# 加权随机选取
|
||
var total_weight := 0
|
||
for d in drops:
|
||
total_weight += int(d.get("prob", 10))
|
||
var r = randi_range(1, total_weight)
|
||
var current := 0
|
||
for d in drops:
|
||
current += int(d.get("prob", 10))
|
||
if r <= current:
|
||
picked_reward = d
|
||
break
|
||
|
||
# 发放奖励
|
||
var reward_type = picked_reward.get("type", "item")
|
||
match reward_type:
|
||
"item":
|
||
_give_or_stack_item(inventory, int(picked_reward["vnum"]), int(picked_reward.get("count", 1)))
|
||
"gold":
|
||
# 金币奖励
|
||
pass
|
||
"exp":
|
||
# 经验奖励
|
||
pass
|
||
|
||
var record = {
|
||
"box_vnum": box_vnum,
|
||
"key_vnum": key_vnum,
|
||
"reward": picked_reward
|
||
}
|
||
_open_history.append(record)
|
||
box_unlocked.emit(box_vnum, key_vnum, picked_reward)
|
||
|
||
return {
|
||
"ok": true,
|
||
"box_name": BOXES[box_vnum]["name"],
|
||
"key_name": KEYS[key_vnum]["name"],
|
||
"reward": picked_reward,
|
||
"msg": "咔哒一声脆响!宝箱锁扣开启,你从箱中获得了【%s】!" % picked_reward.get("name", "宝物")
|
||
}
|
||
|
||
# 获取宝箱完整掉落表
|
||
func get_box_drop_table(box_vnum: int) -> Array:
|
||
return DROP_TABLES.get(box_vnum, [])
|
||
|
||
# 开启历史记录
|
||
func get_open_history() -> Array[Dictionary]:
|
||
return _open_history
|
||
|
||
func clear_history() -> void:
|
||
_open_history.clear()
|
||
|
||
# 内部消耗与堆叠工具函数
|
||
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
|
||
|
||
func _give_or_stack_item(inventory: Array, vnum: int, count: int) -> void:
|
||
for it in inventory:
|
||
if it != null and int(it.get("vnum", 0)) == vnum:
|
||
it["count"] = int(it.get("count", 1)) + count
|
||
return
|
||
for i in range(inventory.size()):
|
||
if inventory[i] == null:
|
||
inventory[i] = {"vnum": vnum, "count": count}
|
||
return
|
||
inventory.append({"vnum": vnum, "count": count})
|
||
|
||
# 序列化与反序列化
|
||
func serialize() -> Dictionary:
|
||
return {
|
||
"open_history": _open_history.duplicate(true)
|
||
}
|
||
|
||
func deserialize(data: Dictionary) -> void:
|
||
_open_history.clear()
|
||
var raw = data.get("open_history", [])
|
||
for item in raw:
|
||
_open_history.append(item.duplicate(true))
|