Files
mtgodot-poc/project/treasure_box_key_system.gd
T
shenandshen 66d217b313 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 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

249 lines
8.2 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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))