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 客户端对拍缺陷发现与修复工程指南
This commit is contained in:
shen
2026-09-19 08:51:25 -07:00
parent 1db9e9a129
commit 66d217b313
650 changed files with 53046 additions and 1586 deletions
+145
View File
@@ -0,0 +1,145 @@
# 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_"),
}