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
+171
View File
@@ -0,0 +1,171 @@
# event_manager_system.gd —— Metin2 40250 单人模式全服掉落活动与倍率调度器 1:1
# 对照 40250 服务端 cmd_gm.cpp (do_priv_empire), priv_manager.cpp, event.cpp, Items 50011, 50037
class_name EventManagerSystem
extends RefCounted
signal rate_changed(rate_type: String, multiplier: float)
signal event_state_changed(event_name: String, is_active: bool)
signal event_box_dropped(box_vnum: int, box_name: String)
const ITEM_MOONLIGHT_BOX := 50011 # 月光宝盒
const ITEM_HEXAGONAL_BOX := 50037 # 六角宝盒
# 宝盒奖励奖池 (40250 官方 special_item_group.txt 1:1)
const EVENT_BOX_LOOT: Dictionary = {
50011: [
{"vnum": 25040, "name": "祝福卷轴", "count": 1, "weight": 30},
{"vnum": 71084, "name": "高级附魔石", "count": 1, "weight": 25},
{"vnum": 71085, "name": "高级洗练石", "count": 1, "weight": 25},
{"vnum": 70024, "name": "祝福大理石", "count": 1, "weight": 10},
{"vnum": 72723, "name": "月之灵药(特)", "count": 1, "weight": 10}
],
50037: [
{"vnum": 72001, "name": "经验之戒", "count": 1, "weight": 20},
{"vnum": 72002, "name": "盗贼手套", "count": 1, "weight": 20},
{"vnum": 27102, "name": "绿药水(大)", "count": 5, "weight": 30},
{"vnum": 27105, "name": "紫药水(大)", "count": 5, "weight": 30}
]
}
var exp_rate: float = 1.0
var drop_rate: float = 1.0
var gold_rate: float = 1.0
var is_moonlight_active: bool = false
var is_hexagonal_active: bool = false
# 设置倍率 (exp, drop, gold)
func set_rate(rate_type: String, multiplier: float) -> Dictionary:
if multiplier < 1.0 or multiplier > 5.0:
return {"ok": false, "reason": "INVALID_MULTIPLIER", "msg": "倍率必须在 1.0 到 5.0 之间!"}
match rate_type:
"exp":
exp_rate = multiplier
"drop":
drop_rate = multiplier
"gold":
gold_rate = multiplier
_:
return {"ok": false, "reason": "INVALID_RATE_TYPE", "msg": "未知的倍率类型!"}
rate_changed.emit(rate_type, multiplier)
return {
"ok": true,
"rate_type": rate_type,
"multiplier": multiplier,
"msg": "全服【%s】倍率已设置为 %.1fx" % [rate_type.to_upper(), multiplier]
}
# 开关经典限时掉落活动
func toggle_event(event_name: String, enable: bool) -> Dictionary:
match event_name:
"moonlight":
is_moonlight_active = enable
"hexagonal":
is_hexagonal_active = enable
_:
return {"ok": false, "reason": "INVALID_EVENT_NAME", "msg": "未知的活动名称!"}
event_state_changed.emit(event_name, enable)
var state_str := "开启" if enable else "关闭"
var ev_desc := "月光宝盒掉落狂欢" if event_name == "moonlight" else "六角宝盒冲级庆典"
return {
"ok": true,
"event_name": event_name,
"is_active": enable,
"msg": "%s】活动已%s" % [ev_desc, state_str]
}
# 怪物击杀时判定额外活动宝盒掉落
func check_event_drops(mob_level: int, player_level: int, forced_roll: float = -1.0) -> Array:
var drops: Array = []
# 等级差在 15 级以内才掉落活动宝盒
if absi(mob_level - player_level) > 15:
return drops
# 1. 月光宝盒判定 (基础概率 10% * drop_rate)
if is_moonlight_active:
var chance := 0.10 * drop_rate
var roll := forced_roll if forced_roll >= 0.0 else randf()
if roll <= chance:
drops.append({"vnum": ITEM_MOONLIGHT_BOX, "name": "月光宝盒", "count": 1})
event_box_dropped.emit(ITEM_MOONLIGHT_BOX, "月光宝盒")
# 2. 六角宝盒判定 (基础概率 15% * drop_rate)
if is_hexagonal_active:
var chance := 0.15 * drop_rate
var roll := forced_roll if forced_roll >= 0.0 else randf()
if roll <= chance:
drops.append({"vnum": ITEM_HEXAGONAL_BOX, "name": "六角宝盒", "count": 1})
event_box_dropped.emit(ITEM_HEXAGONAL_BOX, "六角宝盒")
return drops
# 计算经倍率加成后的经验与金币
func calculate_exp(base_exp: int) -> int:
return int(round(base_exp * exp_rate))
func calculate_gold(base_gold: int) -> int:
return int(round(base_gold * gold_rate))
# 打开活动宝盒
func open_event_box(slot_idx: int, inventory: Array) -> Dictionary:
if slot_idx < 0 or slot_idx >= inventory.size():
return {"ok": false, "reason": "INVALID_SLOT", "msg": "无效的背包栏位!"}
var box = inventory[slot_idx]
if box == null:
return {"ok": false, "reason": "EMPTY_SLOT", "msg": "该格子上没有物品!"}
var box_vnum: int = int(box.get("vnum", 0))
if not EVENT_BOX_LOOT.has(box_vnum):
return {"ok": false, "reason": "NOT_AN_EVENT_BOX", "msg": "这不是活动宝盒!"}
var pool: Array = EVENT_BOX_LOOT[box_vnum]
# 权重抽奖
var total_weight := 0
for item in pool:
total_weight += int(item.get("weight", 1))
var roll := randi_range(1, total_weight)
var accumulated := 0
var reward: Dictionary = {}
for item in pool:
accumulated += int(item.get("weight", 1))
if roll <= accumulated:
reward = item.duplicate()
break
if reward.is_empty():
reward = pool[0].duplicate()
# 扣除宝盒,存入奖励
var cnt: int = int(box.get("count", 1))
if cnt <= 1:
inventory[slot_idx] = null
else:
box["count"] = cnt - 1
var stored_slot := -1
for i in range(inventory.size()):
if inventory[i] == null:
inventory[i] = {
"vnum": reward["vnum"],
"name": reward["name"],
"count": reward.get("count", 1)
}
stored_slot = i
break
return {
"ok": true,
"reward": reward,
"stored_slot": stored_slot,
"msg": "打开活动宝盒,获得了【%s x%d】!" % [reward["name"], reward.get("count", 1)]
}