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
+133
View File
@@ -0,0 +1,133 @@
# dungeon_ranking_system.gd —— Metin2 40250 地牢信息面板与最速通关记录系统 1:1
# 对照 40250 服务端与客户端 dungeon_info.cpp, dungeon.cpp, uidungeoninfo.py
class_name DungeonRankingSystem
extends RefCounted
signal record_updated(dungeon_id: String, fastest_time: float, total_clears: int)
signal new_fastest_record(dungeon_id: String, new_time: float, old_time: float)
# 40250 官方经典地牢登记表
const DUNGEON_REGISTRY: Dictionary = {
"demon_tower": {
"name": "恶魔之塔",
"min_level": 40,
"ticket_vnum": 0,
"ticket_count": 0,
"ticket_name": "",
"cooldown_seconds": 0.0
},
"spider_dungeon_3": {
"name": "蜘蛛洞穴三层",
"min_level": 50,
"ticket_vnum": 30327,
"ticket_count": 1,
"ticket_name": "蜘蛛钥匙",
"cooldown_seconds": 1800.0 # 30 分钟
},
"dragon_lair": {
"name": "冰龙贝兰巢穴",
"min_level": 75,
"ticket_vnum": 30179,
"ticket_count": 3,
"ticket_name": "扭曲钥匙",
"cooldown_seconds": 3600.0 # 60 分钟
},
"devils_catacomb": {
"name": "地下墓穴",
"min_level": 75,
"ticket_vnum": 30311,
"ticket_count": 1,
"ticket_name": "灵魂水晶钥匙",
"cooldown_seconds": 3600.0 # 60 分钟
}
}
# 个人通关数据字典: dungeon_id -> Dictionary
var dungeon_records: Dictionary = {}
func _init() -> void:
for d_id in DUNGEON_REGISTRY.keys():
dungeon_records[d_id] = {
"total_clears": 0,
"fastest_clear_time": 999999.0, # 初始极大值
"highest_peak_damage": 0,
"last_clear_timestamp": 0.0
}
# 记录通关结算
func record_clear(
dungeon_id: String,
clear_time_sec: float,
peak_damage: int,
current_timestamp: float = 0.0
) -> Dictionary:
if not DUNGEON_REGISTRY.has(dungeon_id):
return {"ok": false, "reason": "INVALID_DUNGEON_ID", "msg": "未知的地牢!"}
var rec = dungeon_records[dungeon_id]
rec["total_clears"] += 1
rec["last_clear_timestamp"] = current_timestamp
# 检查并更新最高单次输出伤害
if peak_damage > int(rec["highest_peak_damage"]):
rec["highest_peak_damage"] = peak_damage
# 检查是否刷新最速通关记录
var is_new_record := false
var old_time: float = float(rec["fastest_clear_time"])
if clear_time_sec < old_time:
is_new_record = true
rec["fastest_clear_time"] = clear_time_sec
new_fastest_record.emit(dungeon_id, clear_time_sec, old_time)
record_updated.emit(dungeon_id, rec["fastest_clear_time"], rec["total_clears"])
var dname: String = DUNGEON_REGISTRY[dungeon_id]["name"]
return {
"ok": true,
"dungeon_id": dungeon_id,
"dungeon_name": dname,
"clear_time": clear_time_sec,
"is_new_record": is_new_record,
"fastest_time": rec["fastest_clear_time"],
"total_clears": rec["total_clears"],
"highest_peak_damage": rec["highest_peak_damage"],
"msg": "%s】通关成功!耗时 %.1f%s" % [dname, clear_time_sec, " (刷新最快纪录!)" if is_new_record else ""]
}
# 获取地牢详情与入场/冷却状态
func get_dungeon_info(dungeon_id: String, current_timestamp: float = 0.0) -> Dictionary:
if not DUNGEON_REGISTRY.has(dungeon_id):
return {"ok": false, "reason": "INVALID_DUNGEON_ID"}
var cfg = DUNGEON_REGISTRY[dungeon_id]
var rec = dungeon_records[dungeon_id]
# 判定冷却时间
var cd_sec: float = float(cfg["cooldown_seconds"])
var last_ts: float = float(rec["last_clear_timestamp"])
var elapsed := current_timestamp - last_ts
var is_on_cooldown := (cd_sec > 0.0 and last_ts > 0.0 and elapsed < cd_sec)
var remaining_cd := maxf(0.0, cd_sec - elapsed) if is_on_cooldown else 0.0
return {
"ok": true,
"dungeon_id": dungeon_id,
"name": cfg["name"],
"min_level": cfg["min_level"],
"ticket_vnum": cfg["ticket_vnum"],
"ticket_count": cfg["ticket_count"],
"ticket_name": cfg["ticket_name"],
"is_on_cooldown": is_on_cooldown,
"remaining_cooldown": remaining_cd,
"total_clears": rec["total_clears"],
"fastest_clear_time": rec["fastest_clear_time"] if rec["fastest_clear_time"] < 999999.0 else 0.0,
"highest_peak_damage": rec["highest_peak_damage"]
}
# 获取全部地牢摘要速览列表
func get_all_dungeon_summaries(current_timestamp: float = 0.0) -> Array:
var list: Array = []
for d_id in DUNGEON_REGISTRY.keys():
list.append(get_dungeon_info(d_id, current_timestamp))
return list