- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
146 lines
5.8 KiB
GDScript
146 lines
5.8 KiB
GDScript
# chest_system.gd —— 40250 首领宝箱与钥匙系统 1:1 规则引擎
|
||
# 严格对照:
|
||
# metin2/src/server/game/src/char_item.cpp:2024-2080 (ITEM_GIFTBOX / 50070 / 50011)
|
||
# metin2/src/server/game/src/char_item.cpp:1936-1980 (ITEM_TREASURE_BOX / ITEM_TREASURE_KEY)
|
||
# metin2/src/server/game/src/special_item_group.txt
|
||
extends RefCounted
|
||
|
||
# 宝箱与钥匙 VNUM 定义
|
||
const VNUM_SILVER_BOX := 50001 # 银宝箱
|
||
const VNUM_SILVER_KEY := 50002 # 银钥匙
|
||
const VNUM_GOLD_BOX := 50003 # 金宝箱
|
||
const VNUM_GOLD_KEY := 50004 # 金钥匙
|
||
|
||
const VNUM_MOONLIGHT_BOX := 50011 # 月光宝盒
|
||
const VNUM_CHIEF_ORC_CHEST := 50070 # 兽人首领箱
|
||
const VNUM_DARK_LEADER_CHEST := 50071# 黑魔首领箱
|
||
const VNUM_SPIDER_QUEEN_CHEST := 50073# 蜘蛛女王箱
|
||
const VNUM_NINE_TAILS_CHEST := 50078 # 九尾妖狐宝箱
|
||
const VNUM_FLAME_KING_CHEST := 50079 # 火炎领主宝箱
|
||
const VNUM_REAPER_CHEST := 50082 # 死神宝箱
|
||
|
||
# 特殊掉落类型标识(CSpecialItemGroup)
|
||
const SPECIAL_GOLD := 1000000001
|
||
const SPECIAL_EXP := 1000000002
|
||
const SPECIAL_MOB_TRAP := 1000000003
|
||
const SPECIAL_SLOW_TRAP := 1000000004
|
||
const SPECIAL_POISON_TRAP := 1000000005
|
||
|
||
# 钥匙与宝箱配对映射表
|
||
const KEY_BOX_PAIRS := {
|
||
VNUM_SILVER_KEY: VNUM_SILVER_BOX,
|
||
VNUM_GOLD_KEY: VNUM_GOLD_BOX,
|
||
}
|
||
|
||
# 官方 40250 经典掉落表数据结构 (vnum, count, prob_weight)
|
||
const DROP_TABLES := {
|
||
VNUM_MOONLIGHT_BOX: [
|
||
{"type": "item", "vnum": 50300, "count": 1, "weight": 20, "name": "技能书"},
|
||
{"type": "item", "vnum": 71001, "count": 1, "weight": 15, "name": "驱魔卷轴"},
|
||
{"type": "item", "vnum": 71094, "count": 1, "weight": 10, "name": "隐士教训"},
|
||
{"type": "item", "vnum": 25040, "count": 1, "weight": 15, "name": "祝福卷轴"},
|
||
{"type": "gold", "vnum": SPECIAL_GOLD, "count": 50000, "weight": 25, "name": "金币 50,000"},
|
||
{"type": "exp", "vnum": SPECIAL_EXP, "count": 30000, "weight": 15, "name": "经验值 30,000"},
|
||
],
|
||
VNUM_CHIEF_ORC_CHEST: [
|
||
{"type": "item", "vnum": 50300, "count": 1, "weight": 20, "name": "技能书"},
|
||
{"type": "item", "vnum": 50513, "count": 1, "weight": 10, "name": "魂石"},
|
||
{"type": "item", "vnum": 25040, "count": 1, "weight": 15, "name": "祝福卷轴"},
|
||
{"type": "item", "vnum": 11249, "count": 1, "weight": 10, "name": "幽冥战甲+9"},
|
||
{"type": "trap_poison", "vnum": SPECIAL_POISON_TRAP, "count": 1, "weight": 10, "name": "毒气陷阱"},
|
||
{"type": "gold", "vnum": SPECIAL_GOLD, "count": 100000, "weight": 20, "name": "金币 100,000"},
|
||
{"type": "exp", "vnum": SPECIAL_EXP, "count": 50000, "weight": 15, "name": "经验值 50,000"},
|
||
],
|
||
VNUM_SILVER_BOX: [
|
||
{"type": "item", "vnum": 25040, "count": 1, "weight": 25, "name": "祝福卷轴"},
|
||
{"type": "item", "vnum": 50300, "count": 1, "weight": 25, "name": "技能书"},
|
||
{"type": "gold", "vnum": SPECIAL_GOLD, "count": 30000, "weight": 30, "name": "金币 30,000"},
|
||
{"type": "trap_slow", "vnum": SPECIAL_SLOW_TRAP, "count": 1, "weight": 20, "name": "减速毒雾"},
|
||
],
|
||
VNUM_GOLD_BOX: [
|
||
{"type": "item", "vnum": 25040, "count": 2, "weight": 25, "name": "祝福卷轴 x2"},
|
||
{"type": "item", "vnum": 50513, "count": 1, "weight": 20, "name": "魂石"},
|
||
{"type": "item", "vnum": 71001, "count": 2, "weight": 20, "name": "驱魔卷轴 x2"},
|
||
{"type": "gold", "vnum": SPECIAL_GOLD, "count": 80000, "weight": 35, "name": "金币 80,000"},
|
||
]
|
||
}
|
||
|
||
static func is_gift_box(vnum: int) -> bool:
|
||
return vnum in [
|
||
VNUM_MOONLIGHT_BOX, VNUM_CHIEF_ORC_CHEST, VNUM_DARK_LEADER_CHEST,
|
||
VNUM_SPIDER_QUEEN_CHEST, VNUM_NINE_TAILS_CHEST, VNUM_FLAME_KING_CHEST,
|
||
VNUM_REAPER_CHEST
|
||
]
|
||
|
||
static func is_treasure_box(vnum: int) -> bool:
|
||
return vnum in [VNUM_SILVER_BOX, VNUM_GOLD_BOX]
|
||
|
||
static func is_treasure_key(vnum: int) -> bool:
|
||
return vnum in [VNUM_SILVER_KEY, VNUM_GOLD_KEY]
|
||
|
||
static func is_matching_key(key_vnum: int, box_vnum: int) -> bool:
|
||
return KEY_BOX_PAIRS.get(key_vnum, 0) == box_vnum
|
||
|
||
# 开启普通礼盒(无需钥匙,直接右键)
|
||
static func open_gift_box(
|
||
box_vnum: int,
|
||
empty_inventory_slots: int,
|
||
roll_val := -1
|
||
) -> Dictionary:
|
||
if not is_gift_box(box_vnum):
|
||
return {"success": false, "err": "not_gift_box", "msg": "该物品无法直接开启。"}
|
||
|
||
if empty_inventory_slots < 1:
|
||
return {"success": false, "err": "inventory_full", "msg": "背包已满,无法开启宝箱。"}
|
||
|
||
return _roll_chest_reward(box_vnum, roll_val)
|
||
|
||
# 使用钥匙开启上锁宝箱(拖拽钥匙至宝箱)
|
||
static func open_treasure_box_with_key(
|
||
key_vnum: int,
|
||
box_vnum: int,
|
||
empty_inventory_slots: int,
|
||
roll_val := -1
|
||
) -> Dictionary:
|
||
if not is_treasure_box(box_vnum):
|
||
return {"success": false, "err": "not_treasure_box", "msg": "这不是一个需要钥匙的宝箱。"}
|
||
|
||
if not is_matching_key(key_vnum, box_vnum):
|
||
return {"success": false, "err": "key_mismatch", "msg": "这把钥匙无法打开该宝箱。"}
|
||
|
||
if empty_inventory_slots < 1:
|
||
return {"success": false, "err": "inventory_full", "msg": "背包已满,无法开启宝箱。"}
|
||
|
||
var res := _roll_chest_reward(box_vnum, roll_val)
|
||
res["consumed_key_vnum"] = key_vnum
|
||
res["consumed_box_vnum"] = box_vnum
|
||
return res
|
||
|
||
# 抽选掉落物
|
||
static func _roll_chest_reward(box_vnum: int, roll_val: int) -> Dictionary:
|
||
var drops: Array = DROP_TABLES.get(box_vnum, DROP_TABLES[VNUM_MOONLIGHT_BOX])
|
||
var total_weight := 0
|
||
for d in drops:
|
||
total_weight += int(d.get("weight", 10))
|
||
|
||
var dice: int = roll_val if roll_val > 0 else (randi() % total_weight)
|
||
dice = dice % total_weight # 约束范围
|
||
|
||
var accum := 0
|
||
var chosen: Dictionary = drops[0]
|
||
for d in drops:
|
||
accum += int(d.get("weight", 10))
|
||
if dice < accum:
|
||
chosen = d
|
||
break
|
||
|
||
return {
|
||
"success": true,
|
||
"box_vnum": box_vnum,
|
||
"reward_type": chosen.get("type", "item"),
|
||
"reward_vnum": chosen.get("vnum", 0),
|
||
"reward_count": chosen.get("count", 1),
|
||
"reward_name": chosen.get("name", "神秘道具"),
|
||
"is_trap": String(chosen.get("type", "")).begins_with("trap_"),
|
||
}
|