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:
@@ -0,0 +1,194 @@
|
||||
# endless_spire_dungeon_system.gd —— Metin2 40250 单人单机迷宫地牢【无尽天关】爬塔挑战 1:1
|
||||
# 对照 40250 服务端 dungeon.cpp, dungeon_info.cpp, questmanager.cpp
|
||||
class_name EndlessSpireDungeonSystem
|
||||
extends RefCounted
|
||||
|
||||
signal floor_started(floor_num: int, is_boss_floor: bool, mob_count: int)
|
||||
signal floor_cleared(floor_num: int, points_earned: int, time_used: float)
|
||||
signal checkpoint_saved(checkpoint_floor: int)
|
||||
signal spire_trial_completed(highest_floor: int, total_points: int)
|
||||
signal spire_trial_failed(floor_num: int, reason: String)
|
||||
|
||||
const MAX_FLOORS := 50
|
||||
const FLOOR_TIME_LIMIT := 120.0 # 每层 120 秒限时
|
||||
|
||||
const MILESTONE_BOSSES: Dictionary = {
|
||||
5: {"name": "半兽人首领", "vnum": 691, "hp": 50000},
|
||||
10: {"name": "蜘蛛女王", "vnum": 2091, "hp": 80000},
|
||||
15: {"name": "沙漠巨怪", "vnum": 2191, "hp": 120000},
|
||||
20: {"name": "恶魔死神", "vnum": 1093, "hp": 180000},
|
||||
25: {"name": "烈焰领主", "vnum": 2206, "hp": 220000},
|
||||
30: {"name": "九尾妖狐", "vnum": 1901, "hp": 280000},
|
||||
35: {"name": "黄虎之魂", "vnum": 1304, "hp": 320000},
|
||||
40: {"name": "冥王阿兹瑞尔", "vnum": 2598, "hp": 400000},
|
||||
45: {"name": "冰龙贝兰-塞陶", "vnum": 2493, "hp": 500000},
|
||||
50: {"name": "混沌魔神·阿萨泽尔", "vnum": 8027, "hp": 800000}
|
||||
}
|
||||
|
||||
var is_in_trial: bool = false
|
||||
var current_floor: int = 1
|
||||
var highest_checkpoint: int = 1
|
||||
var total_points: int = 0
|
||||
var floor_time_remaining: float = 0.0
|
||||
|
||||
var active_mobs: Dictionary = {} # mob_id -> { "vnum", "alive" }
|
||||
var next_mob_id: int = 1
|
||||
var personal_best_floor: int = 0
|
||||
|
||||
# 开启爬塔挑战
|
||||
func start_trial(from_checkpoint: bool = false) -> Dictionary:
|
||||
if is_in_trial:
|
||||
return {"ok": false, "reason": "ALREADY_IN_TRIAL"}
|
||||
|
||||
is_in_trial = true
|
||||
current_floor = highest_checkpoint if from_checkpoint else 1
|
||||
total_points = 0
|
||||
next_mob_id = 1
|
||||
|
||||
_start_floor(current_floor)
|
||||
|
||||
return {
|
||||
"ok": true,
|
||||
"starting_floor": current_floor,
|
||||
"checkpoint": highest_checkpoint,
|
||||
"msg": "踏入【无尽天关】第 %d 层!" % current_floor
|
||||
}
|
||||
|
||||
# 开启指定层数
|
||||
func _start_floor(floor_num: int) -> void:
|
||||
current_floor = floor_num
|
||||
floor_time_remaining = FLOOR_TIME_LIMIT
|
||||
active_mobs.clear()
|
||||
|
||||
var is_boss = MILESTONE_BOSSES.has(floor_num)
|
||||
if is_boss:
|
||||
var b_cfg = MILESTONE_BOSSES[floor_num]
|
||||
var mid = next_mob_id
|
||||
next_mob_id += 1
|
||||
active_mobs[mid] = {
|
||||
"id": mid,
|
||||
"vnum": b_cfg["vnum"],
|
||||
"name": b_cfg["name"],
|
||||
"is_boss": true,
|
||||
"alive": true
|
||||
}
|
||||
floor_started.emit(floor_num, true, 1)
|
||||
else:
|
||||
# 普通层小怪
|
||||
var mob_count = 4 + (floor_num % 5) * 2
|
||||
for i in range(mob_count):
|
||||
var mid = next_mob_id
|
||||
next_mob_id += 1
|
||||
active_mobs[mid] = {
|
||||
"id": mid,
|
||||
"vnum": 101 + (floor_num % 10),
|
||||
"name": "天关守卫",
|
||||
"is_boss": false,
|
||||
"alive": true
|
||||
}
|
||||
floor_started.emit(floor_num, false, mob_count)
|
||||
|
||||
# 击杀当前层怪物
|
||||
func kill_mob(mob_id: int) -> Dictionary:
|
||||
if not is_in_trial:
|
||||
return {"ok": false, "reason": "NOT_IN_TRIAL"}
|
||||
|
||||
if not active_mobs.has(mob_id) or not active_mobs[mob_id]["alive"]:
|
||||
return {"ok": false, "reason": "MOB_NOT_FOUND"}
|
||||
|
||||
active_mobs[mob_id]["alive"] = false
|
||||
active_mobs.erase(mob_id)
|
||||
|
||||
# 检查本层是否清空
|
||||
if active_mobs.is_empty():
|
||||
return _on_floor_cleared()
|
||||
|
||||
return {
|
||||
"ok": true,
|
||||
"remaining_mobs": active_mobs.size()
|
||||
}
|
||||
|
||||
# 本层通关结算
|
||||
func _on_floor_cleared() -> Dictionary:
|
||||
var time_used = FLOOR_TIME_LIMIT - floor_time_remaining
|
||||
var points_awarded = current_floor * 10
|
||||
total_points += points_awarded
|
||||
|
||||
if current_floor > personal_best_floor:
|
||||
personal_best_floor = current_floor
|
||||
|
||||
floor_cleared.emit(current_floor, points_awarded, time_used)
|
||||
|
||||
# 检查点保存判定 (逢 5 层通关)
|
||||
if current_floor % 5 == 0:
|
||||
var next_cp = current_floor + 1
|
||||
if next_cp > highest_checkpoint and next_cp <= MAX_FLOORS:
|
||||
highest_checkpoint = next_cp
|
||||
checkpoint_saved.emit(highest_checkpoint)
|
||||
|
||||
# 终极 50 层通关判定
|
||||
if current_floor >= MAX_FLOORS:
|
||||
is_in_trial = false
|
||||
spire_trial_completed.emit(MAX_FLOORS, total_points)
|
||||
return {
|
||||
"ok": true,
|
||||
"trial_completed": true,
|
||||
"highest_floor": MAX_FLOORS,
|
||||
"total_points": total_points,
|
||||
"msg": "登峰造极!你已彻底通关【无尽天关】第 50 层巅峰!"
|
||||
}
|
||||
|
||||
# 进阶下一层
|
||||
var next_f = current_floor + 1
|
||||
_start_floor(next_f)
|
||||
|
||||
return {
|
||||
"ok": true,
|
||||
"floor_cleared": current_floor - 1,
|
||||
"next_floor": next_f,
|
||||
"points": total_points,
|
||||
"msg": "第 %d 层顺利通关!进入第 %d 层!" % [current_floor - 1, next_f]
|
||||
}
|
||||
|
||||
# 帧心跳更新限时
|
||||
func update(delta: float) -> Dictionary:
|
||||
if not is_in_trial:
|
||||
return {"status": "inactive"}
|
||||
|
||||
floor_time_remaining -= delta
|
||||
if floor_time_remaining <= 0.0:
|
||||
floor_time_remaining = 0.0
|
||||
is_in_trial = false
|
||||
spire_trial_failed.emit(current_floor, "TIME_EXPIRED")
|
||||
return {
|
||||
"status": "failed",
|
||||
"reason": "TIME_EXPIRED",
|
||||
"floor": current_floor,
|
||||
"msg": "时间耗尽!被传送出天关。"
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "active",
|
||||
"floor": current_floor,
|
||||
"remaining_time": floor_time_remaining,
|
||||
"remaining_mobs": active_mobs.size()
|
||||
}
|
||||
|
||||
# 中途离开挑战
|
||||
func exit_trial() -> Dictionary:
|
||||
if not is_in_trial:
|
||||
return {"ok": false, "reason": "NOT_IN_TRIAL"}
|
||||
is_in_trial = false
|
||||
active_mobs.clear()
|
||||
return {"ok": true, "msg": "已主动退出天关,检查点进度已保存。"}
|
||||
|
||||
# 获取挑战概览
|
||||
func get_spire_status() -> Dictionary:
|
||||
return {
|
||||
"is_in_trial": is_in_trial,
|
||||
"current_floor": current_floor,
|
||||
"highest_checkpoint": highest_checkpoint,
|
||||
"personal_best_floor": personal_best_floor,
|
||||
"total_points": total_points,
|
||||
"remaining_time": floor_time_remaining
|
||||
}
|
||||
Reference in New Issue
Block a user