- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
197 lines
5.0 KiB
GDScript
197 lines
5.0 KiB
GDScript
# cursed_monolith_spire_system.gd —— Metin2 40250 野外魔石破除之【封印魔塔】限时防守挑战 1:1
|
|
# 对照 40250 服务端 regen.cpp, dungeon.cpp, char_battle.cpp, item_manager.cpp
|
|
class_name CursedMonolithSpireSystem
|
|
extends RefCounted
|
|
|
|
signal spire_emerged(pos: Vector3, time_limit: float)
|
|
signal wave_spawned(wave_index: int, wave_name: String, mob_count: int)
|
|
signal trial_completed(success: bool, time_used: float)
|
|
signal reward_chest_spawned(chest_vnum: int, pos: Vector3)
|
|
|
|
const TRIAL_TIME_LIMIT: float = 180.0 # 3 分钟倒计时
|
|
const CHEST_VNUM: int = 50130 # 上古魔塔宝箱 (Ancient Spire Chest)
|
|
|
|
const WAVE_CONFIG: Dictionary = {
|
|
1: {
|
|
"name": "第 1 波:狂暴魔兵突袭",
|
|
"mob_vnum": 701, # 恶魔士兵
|
|
"count": 6
|
|
},
|
|
2: {
|
|
"name": "第 2 波:双生魔将合围",
|
|
"mob_vnum": 1092, # 恶魔大将
|
|
"count": 2
|
|
},
|
|
3: {
|
|
"name": "第 3 波:上古守墓魔王",
|
|
"mob_vnum": 1093, # 恶魔死神幻影
|
|
"count": 1
|
|
}
|
|
}
|
|
|
|
var is_active: bool = false
|
|
var current_wave: int = 0
|
|
var time_remaining: float = 0.0
|
|
var spire_pos: Vector3 = Vector3.ZERO
|
|
var difficulty: int = 50
|
|
|
|
var active_mobs: Dictionary = {} # mob_id -> { "vnum", "alive" }
|
|
var next_mob_id: int = 1
|
|
var is_victory: bool = false
|
|
var chest_available: bool = false
|
|
|
|
# 启动封印魔塔挑战
|
|
func start_trial(pos: Vector3, diff_level: int = 50) -> Dictionary:
|
|
if is_active:
|
|
return {"ok": false, "reason": "TRIAL_ALREADY_ACTIVE", "msg": "封印魔塔挑战正在进行中!"}
|
|
|
|
is_active = true
|
|
is_victory = false
|
|
chest_available = false
|
|
spire_pos = pos
|
|
difficulty = diff_level
|
|
time_remaining = TRIAL_TIME_LIMIT
|
|
current_wave = 0
|
|
active_mobs.clear()
|
|
next_mob_id = 1
|
|
|
|
spire_emerged.emit(pos, TRIAL_TIME_LIMIT)
|
|
|
|
# 立即召唤第 1 波
|
|
_spawn_wave(1)
|
|
|
|
return {
|
|
"ok": true,
|
|
"pos": pos,
|
|
"time_limit": TRIAL_TIME_LIMIT,
|
|
"msg": "大地剧烈震颤!【诅咒的封印黑石魔塔】破土升起!请在 180 秒内清除所有魔潮!"
|
|
}
|
|
|
|
# 产生指定阶段怪潮
|
|
func _spawn_wave(wave_idx: int) -> void:
|
|
current_wave = wave_idx
|
|
var cfg = WAVE_CONFIG[wave_idx]
|
|
var count: int = cfg["count"]
|
|
var vnum: int = cfg["mob_vnum"]
|
|
|
|
active_mobs.clear()
|
|
for i in range(count):
|
|
var mid := next_mob_id
|
|
next_mob_id += 1
|
|
active_mobs[mid] = {
|
|
"id": mid,
|
|
"vnum": vnum,
|
|
"alive": true
|
|
}
|
|
|
|
wave_spawned.emit(wave_idx, cfg["name"], count)
|
|
|
|
# 击杀魔潮怪物
|
|
func kill_mob(mob_id: int) -> Dictionary:
|
|
if not is_active:
|
|
return {"ok": false, "reason": "TRIAL_NOT_ACTIVE"}
|
|
|
|
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():
|
|
if current_wave < 3:
|
|
# 进入下一波
|
|
var next_w = current_wave + 1
|
|
_spawn_wave(next_w)
|
|
return {
|
|
"ok": true,
|
|
"wave_cleared": current_wave - 1,
|
|
"next_wave": next_w,
|
|
"msg": "当前波次肃清!更加凶险的魔物正在凝聚!"
|
|
}
|
|
else:
|
|
# 第 3 波首领击破 -> 大获全胜!
|
|
return _finish_trial(true)
|
|
|
|
return {
|
|
"ok": true,
|
|
"remaining_in_wave": active_mobs.size()
|
|
}
|
|
|
|
# 帧心跳更新限时倒计时
|
|
func update(delta: float) -> Dictionary:
|
|
if not is_active:
|
|
return {"status": "inactive"}
|
|
|
|
time_remaining -= delta
|
|
if time_remaining <= 0.0:
|
|
time_remaining = 0.0
|
|
return _finish_trial(false)
|
|
|
|
return {
|
|
"status": "running",
|
|
"wave": current_wave,
|
|
"remaining_mobs": active_mobs.size(),
|
|
"time_remaining": time_remaining
|
|
}
|
|
|
|
# 结算挑战胜负
|
|
func _finish_trial(success: bool) -> Dictionary:
|
|
is_active = false
|
|
is_victory = success
|
|
var time_used = TRIAL_TIME_LIMIT - time_remaining
|
|
|
|
if success:
|
|
chest_available = true
|
|
reward_chest_spawned.emit(CHEST_VNUM, spire_pos)
|
|
trial_completed.emit(true, time_used)
|
|
return {
|
|
"ok": true,
|
|
"victory": true,
|
|
"time_used": time_used,
|
|
"msg": "【封印魔塔】封印彻底瓦解!降下【上古魔塔宝箱】!耗时: %.1f 秒" % time_used
|
|
}
|
|
else:
|
|
active_mobs.clear()
|
|
trial_completed.emit(false, TRIAL_TIME_LIMIT)
|
|
return {
|
|
"ok": true,
|
|
"victory": false,
|
|
"time_used": TRIAL_TIME_LIMIT,
|
|
"msg": "时间耗尽!封印黑石魔塔重新沉入深渊地底,挑战失败!"
|
|
}
|
|
|
|
# 开启上古宝箱获取掉落
|
|
func open_reward_chest() -> Array:
|
|
if not chest_available:
|
|
return []
|
|
|
|
chest_available = false
|
|
var drops: Array = [
|
|
{"vnum": 71003, "name": "祝福卷轴", "count": 2},
|
|
{"vnum": 50513, "name": "灵魂之石", "count": 1},
|
|
{"vnum": 70024, "name": "祝福宝珠", "count": 1},
|
|
{"vnum": 1, "name": "金币", "count": 3500000}
|
|
]
|
|
return drops
|
|
|
|
# 取消/中断挑战
|
|
func cancel_trial() -> Dictionary:
|
|
if not is_active:
|
|
return {"ok": false, "reason": "NOT_ACTIVE"}
|
|
|
|
is_active = false
|
|
active_mobs.clear()
|
|
return {"ok": true, "msg": "挑战已提前终止。"}
|
|
|
|
# 获取状态
|
|
func get_trial_state() -> Dictionary:
|
|
return {
|
|
"is_active": is_active,
|
|
"is_victory": is_victory,
|
|
"current_wave": current_wave,
|
|
"remaining_mobs": active_mobs.size(),
|
|
"time_remaining": time_remaining,
|
|
"chest_available": chest_available
|
|
}
|