Files
mtgodot-poc/project/devils_catacomb_system.gd
T
shenandshen 66d217b313 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 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

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