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,326 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user