- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
134 lines
4.2 KiB
GDScript
134 lines
4.2 KiB
GDScript
# 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
|