- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
327 lines
8.5 KiB
GDScript
327 lines
8.5 KiB
GDScript
# devils_catacomb_system.gd —— Metin2 40250 地下墓穴 7 层与冥王阿兹瑞尔挑战副本 1:1
|
|
# 对照 40250 服务端 devils_catacomb.quest, dungeon.cpp, char_battle.cpp, mob 2598, 2597, 2596
|
|
class_name DevilsCatacombSystem
|
|
extends RefCounted
|
|
|
|
signal floor_advanced(floor: int)
|
|
signal metin_destroyed(is_correct: bool)
|
|
signal tartaros_killed(drop_item: Dictionary)
|
|
signal obelisk_activated()
|
|
signal charon_killed()
|
|
signal azrael_hp_changed(current_hp: int, max_hp: int)
|
|
signal catacomb_cleared(drops: Array)
|
|
signal catacomb_failed(reason: String)
|
|
|
|
const NPC_CATACOMB_GUARD := 20367
|
|
const ITEM_SOUL_CRYSTAL_KEY := 30311 # 灵魂水晶钥匙 (进入与 1 层解锁)
|
|
const ITEM_GRIM_SOUL_STONE := 30312 # 冷酷之石 (5 层塔耳塔洛斯掉落)
|
|
const MIN_LEVEL := 75
|
|
const TIME_LIMIT := 3600.0 # 60 分钟限时
|
|
|
|
# 首领属性
|
|
const MOB_TARTAROS := 2597 # 5 层守卫 塔耳塔洛斯
|
|
const MOB_CHARON := 2596 # 6 层将军 卡隆
|
|
const MOB_AZRAEL := 2598 # 7 层冥界之王 阿兹瑞尔
|
|
|
|
const AZRAEL_MAX_HP := 900000
|
|
const AZRAEL_BASE_ATK := 2200
|
|
const AZRAEL_BASE_DEF := 680
|
|
|
|
var is_active: bool = false
|
|
var current_floor: int = 0
|
|
var remaining_time: float = 0.0
|
|
|
|
# 3 层复仇魔石状态
|
|
var correct_metin_id: int = 3
|
|
var metins_broken: int = 0
|
|
|
|
# 5 层方尖碑状态
|
|
var has_grim_stone: bool = false
|
|
var is_obelisk_unlocked: bool = false
|
|
|
|
# 7 层阿兹瑞尔状态
|
|
var azrael_hp: int = 0
|
|
var azrael_max_hp: int = AZRAEL_MAX_HP
|
|
var is_azrael_alive: bool = false
|
|
var azrael_minions_summoned: bool = false
|
|
|
|
# 进入地下墓穴 (通过 NPC 20367)
|
|
func enter_catacomb(player_level: int, inventory: Array) -> Dictionary:
|
|
if player_level < MIN_LEVEL:
|
|
return {
|
|
"ok": false,
|
|
"reason": "LEVEL_TOO_LOW",
|
|
"msg": "你的等级不足 75 级,无法承受地下墓穴的死灵死气!"
|
|
}
|
|
|
|
# 检查 1 层必需的灵魂水晶钥匙 (30311)
|
|
var key_slot := -1
|
|
for i in range(inventory.size()):
|
|
var item = inventory[i]
|
|
if item != null and int(item.get("vnum", 0)) == ITEM_SOUL_CRYSTAL_KEY:
|
|
key_slot = i
|
|
break
|
|
|
|
if key_slot == -1:
|
|
return {
|
|
"ok": false,
|
|
"reason": "MISSING_SOUL_KEY",
|
|
"msg": "进入地下墓穴需要持有【灵魂水晶钥匙】!"
|
|
}
|
|
|
|
# 初始化副本状态进入 1 层
|
|
is_active = true
|
|
current_floor = 1
|
|
remaining_time = TIME_LIMIT
|
|
correct_metin_id = randi_range(1, 5)
|
|
metins_broken = 0
|
|
has_grim_stone = false
|
|
is_obelisk_unlocked = false
|
|
is_azrael_alive = false
|
|
|
|
floor_advanced.emit(1)
|
|
|
|
return {
|
|
"ok": true,
|
|
"floor": 1,
|
|
"time_limit": remaining_time,
|
|
"msg": "成功踏入地下墓穴一层!限时 60 分钟!"
|
|
}
|
|
|
|
# 第 1 层:使用灵魂水晶钥匙开启石门封印进入 2 层
|
|
func unlock_floor_1(inventory: Array) -> Dictionary:
|
|
if not is_active or current_floor != 1:
|
|
return {"ok": false, "reason": "WRONG_FLOOR"}
|
|
|
|
var key_slot := -1
|
|
for i in range(inventory.size()):
|
|
var item = inventory[i]
|
|
if item != null and int(item.get("vnum", 0)) == ITEM_SOUL_CRYSTAL_KEY:
|
|
key_slot = i
|
|
break
|
|
|
|
if key_slot == -1:
|
|
return {"ok": false, "reason": "MISSING_KEY", "msg": "背包中缺少灵魂水晶钥匙!"}
|
|
|
|
# 消耗钥匙
|
|
inventory[key_slot] = null
|
|
current_floor = 2
|
|
floor_advanced.emit(2)
|
|
|
|
return {
|
|
"ok": true,
|
|
"floor": 2,
|
|
"msg": "灵魂水晶钥匙已插入封印,石门轰然开启!进入地下墓穴二层!"
|
|
}
|
|
|
|
# 第 2 层:突破恶灵迷宫进入 3 层
|
|
func complete_floor_2() -> Dictionary:
|
|
if not is_active or current_floor != 2:
|
|
return {"ok": false, "reason": "WRONG_FLOOR"}
|
|
|
|
current_floor = 3
|
|
floor_advanced.emit(3)
|
|
return {
|
|
"ok": true,
|
|
"floor": 3,
|
|
"msg": "已突破恶灵迷宫!进入地下墓穴三层!"
|
|
}
|
|
|
|
# 第 3 层:击碎复仇魔石 (8035)
|
|
func break_floor_3_metin(metin_idx: int) -> Dictionary:
|
|
if not is_active or current_floor != 3:
|
|
return {"ok": false, "reason": "WRONG_FLOOR"}
|
|
|
|
metins_broken += 1
|
|
if metin_idx == correct_metin_id:
|
|
current_floor = 4
|
|
metin_destroyed.emit(true)
|
|
floor_advanced.emit(4)
|
|
return {
|
|
"ok": true,
|
|
"correct": true,
|
|
"floor": 4,
|
|
"msg": "击碎了真正的复仇魔石!封印瓦解,传送至地下墓穴四层!"
|
|
}
|
|
else:
|
|
metin_destroyed.emit(false)
|
|
return {
|
|
"ok": true,
|
|
"correct": false,
|
|
"floor": 3,
|
|
"msg": "这是虚假的伪装魔石!周围唤醒了冥界诅咒之魂!"
|
|
}
|
|
|
|
# 第 4 层:玄武岩石柱迷宫寻路进入 5 层
|
|
func complete_floor_4() -> Dictionary:
|
|
if not is_active or current_floor != 4:
|
|
return {"ok": false, "reason": "WRONG_FLOOR"}
|
|
|
|
current_floor = 5
|
|
floor_advanced.emit(5)
|
|
return {
|
|
"ok": true,
|
|
"floor": 5,
|
|
"msg": "成功找到玄武岩迷宫真出口!进入地下墓穴五层!"
|
|
}
|
|
|
|
# 第 5 层:击败塔耳塔洛斯 (2597) 并获得冷酷之石 (30312)
|
|
func defeat_tartaros(inventory: Array) -> Dictionary:
|
|
if not is_active or current_floor != 5:
|
|
return {"ok": false, "reason": "WRONG_FLOOR"}
|
|
|
|
# 放入背包
|
|
var empty_slot := -1
|
|
for i in range(inventory.size()):
|
|
if inventory[i] == null:
|
|
empty_slot = i
|
|
break
|
|
|
|
if empty_slot == -1:
|
|
return {"ok": false, "reason": "INVENTORY_FULL", "msg": "背包已满,无法拾取冷酷之石!"}
|
|
|
|
var grim_stone = {"vnum": ITEM_GRIM_SOUL_STONE, "name": "冷酷之石", "count": 1}
|
|
inventory[empty_slot] = grim_stone
|
|
has_grim_stone = true
|
|
|
|
tartaros_killed.emit(grim_stone)
|
|
|
|
return {
|
|
"ok": true,
|
|
"drop": grim_stone,
|
|
"msg": "击败了守卫【塔耳塔洛斯】,获得了【冷酷之石】!请前往方尖碑镶嵌!"
|
|
}
|
|
|
|
# 第 5 层:将冷酷之石插入玄武岩方尖碑,开启 6 层通道
|
|
func insert_grim_stone_to_obelisk(inventory: Array) -> Dictionary:
|
|
if not is_active or current_floor != 5:
|
|
return {"ok": false, "reason": "WRONG_FLOOR"}
|
|
|
|
var stone_slot := -1
|
|
for i in range(inventory.size()):
|
|
var item = inventory[i]
|
|
if item != null and int(item.get("vnum", 0)) == ITEM_GRIM_SOUL_STONE:
|
|
stone_slot = i
|
|
break
|
|
|
|
if stone_slot == -1:
|
|
return {"ok": false, "reason": "MISSING_STONE", "msg": "背包中缺少冷酷之石!"}
|
|
|
|
inventory[stone_slot] = null
|
|
is_obelisk_unlocked = true
|
|
current_floor = 6
|
|
obelisk_activated.emit()
|
|
floor_advanced.emit(6)
|
|
|
|
return {
|
|
"ok": true,
|
|
"floor": 6,
|
|
"msg": "冷酷之石完美嵌入方尖碑!冥界封印开启,进入地下墓穴六层!"
|
|
}
|
|
|
|
# 第 6 层:击败冥界将军卡隆 (2596) 进入 7 层阿兹瑞尔王座
|
|
func defeat_charon() -> Dictionary:
|
|
if not is_active or current_floor != 6:
|
|
return {"ok": false, "reason": "WRONG_FLOOR"}
|
|
|
|
current_floor = 7
|
|
# 初始化 7 层 Boss 阿兹瑞尔
|
|
azrael_hp = AZRAEL_MAX_HP
|
|
azrael_max_hp = AZRAEL_MAX_HP
|
|
is_azrael_alive = true
|
|
azrael_minions_summoned = false
|
|
|
|
charon_killed.emit()
|
|
floor_advanced.emit(7)
|
|
|
|
return {
|
|
"ok": true,
|
|
"floor": 7,
|
|
"boss_hp": azrael_hp,
|
|
"msg": "冥界将军【卡隆】已被斩灭!传送至七层【冥王阿兹瑞尔王座】!"
|
|
}
|
|
|
|
# 第 7 层:对冥王阿兹瑞尔造成伤害
|
|
func attack_azrael(raw_dmg: int) -> Dictionary:
|
|
if not is_active or current_floor != 7 or not is_azrael_alive:
|
|
return {"ok": false, "reason": "BOSS_NOT_AVAILABLE"}
|
|
|
|
var net_dmg: int = max(1, raw_dmg - AZRAEL_BASE_DEF)
|
|
azrael_hp = max(0, azrael_hp - net_dmg)
|
|
|
|
azrael_hp_changed.emit(azrael_hp, azrael_max_hp)
|
|
|
|
# 半血召唤死灵护卫
|
|
var triggered_minions := false
|
|
if azrael_hp <= int(AZRAEL_MAX_HP * 0.5) and not azrael_minions_summoned:
|
|
azrael_minions_summoned = true
|
|
triggered_minions = true
|
|
|
|
# 击败阿兹瑞尔
|
|
if azrael_hp <= 0:
|
|
is_azrael_alive = false
|
|
is_active = false
|
|
var drops: Array = _generate_azrael_drops()
|
|
catacomb_cleared.emit(drops)
|
|
return {
|
|
"ok": true,
|
|
"killed": true,
|
|
"net_dmg": net_dmg,
|
|
"remaining_hp": 0,
|
|
"drops": drops,
|
|
"msg": "【冥王阿兹瑞尔】轰然倒下!地下墓穴已被彻底征服!"
|
|
}
|
|
|
|
return {
|
|
"ok": true,
|
|
"killed": false,
|
|
"net_dmg": net_dmg,
|
|
"remaining_hp": azrael_hp,
|
|
"summoned_minions": triggered_minions
|
|
}
|
|
|
|
# 逐帧倒计时
|
|
func update(dt: float) -> void:
|
|
if not is_active:
|
|
return
|
|
|
|
remaining_time -= dt
|
|
if remaining_time <= 0.0:
|
|
is_active = false
|
|
is_azrael_alive = false
|
|
catacomb_failed.emit("TIME_OUT")
|
|
|
|
# 生成阿兹瑞尔掉落物
|
|
func _generate_azrael_drops() -> Array:
|
|
var drops: Array = []
|
|
|
|
# 1. 必掉阿兹瑞尔专属宝箱 (50187)
|
|
drops.append({
|
|
"vnum": 50187,
|
|
"name": "阿兹瑞尔宝箱",
|
|
"count": 1
|
|
})
|
|
|
|
# 2. 必掉灵魂之石 (50513)
|
|
drops.append({
|
|
"vnum": 50513,
|
|
"name": "灵魂之石",
|
|
"count": 1
|
|
})
|
|
|
|
# 3. 终极泰坦之盾 (13149) 概率掉落
|
|
drops.append({
|
|
"vnum": 13149,
|
|
"name": "泰坦之盾+0",
|
|
"count": 1
|
|
})
|
|
|
|
# 4. 大额冥界金币
|
|
drops.append({
|
|
"vnum": 1,
|
|
"name": "金币",
|
|
"count": randi_range(200000, 400000)
|
|
})
|
|
|
|
return drops
|