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,257 @@
|
||||
# dragon_lair_system.gd —— Metin2 40250 冰龙贝兰-塞陶挑战巢穴 1:1
|
||||
# 对照 40250 服务端 dragon_lair.quest, dungeon.cpp, char_battle.cpp, mob 2493, NPC 30121
|
||||
class_name DragonLairSystem
|
||||
extends RefCounted
|
||||
|
||||
signal dungeon_started(time_limit: float)
|
||||
signal metin_destroyed(metin_vnum: int, metin_name: String)
|
||||
signal minions_spawned(count: int, mob_vnum: int)
|
||||
signal dragon_hp_changed(current_hp: int, max_hp: int)
|
||||
signal dragon_defeated(drops: Array)
|
||||
signal dungeon_failed(reason: String)
|
||||
|
||||
const NPC_GHOST_OF_SURA := 30121 # 修罗鬼魂
|
||||
const ITEM_TWISTED_KEY := 30179 # 扭曲钥匙
|
||||
const KEY_REQUIRED_COUNT := 3
|
||||
const MIN_LEVEL := 75
|
||||
const TIME_LIMIT := 3600.0 # 60 分钟限时
|
||||
|
||||
const BOSS_VNUM := 2493 # 贝兰-塞陶 (Beran-Setaou)
|
||||
const BOSS_MAX_HP := 800000
|
||||
const BOSS_BASE_ATK := 1800
|
||||
const BOSS_BASE_DEF := 650
|
||||
|
||||
const METIN_TABLE: Dictionary = {
|
||||
8031: {"name": "傲慢之石", "desc": "提供 30% 物理抗性减免"},
|
||||
8032: {"name": "寂寞之石", "desc": "提供生命自然恢复"},
|
||||
8033: {"name": "平静之石", "desc": "提供 30% 魔法抗性减免"},
|
||||
8034: {"name": "复仇之石", "desc": "提供 40% 攻击力增幅"}
|
||||
}
|
||||
|
||||
var is_active: bool = false
|
||||
var remaining_time: float = 0.0
|
||||
var dragon_hp: int = 0
|
||||
var dragon_max_hp: int = BOSS_MAX_HP
|
||||
var is_dragon_alive: bool = false
|
||||
var active_metins: Dictionary = {} # vnum -> bool (true = alive)
|
||||
var active_minions: Array = []
|
||||
var regen_timer: float = 0.0
|
||||
|
||||
# 进入冰龙巢穴 (通过 NPC 30121 验证)
|
||||
func enter_lair(player_level: int, inventory: Array) -> Dictionary:
|
||||
if player_level < MIN_LEVEL:
|
||||
return {
|
||||
"ok": false,
|
||||
"reason": "LEVEL_TOO_LOW",
|
||||
"msg": "你的等级不足 75 级,无法承受冰龙巢穴的严寒!"
|
||||
}
|
||||
|
||||
# 检查背包中的 3 把扭曲钥匙 (30179)
|
||||
var key_slots: Array = []
|
||||
var total_keys: int = 0
|
||||
for i in range(inventory.size()):
|
||||
var item = inventory[i]
|
||||
if item != null and int(item.get("vnum", 0)) == ITEM_TWISTED_KEY:
|
||||
var cnt: int = int(item.get("count", 1))
|
||||
total_keys += cnt
|
||||
key_slots.append({"slot": i, "count": cnt})
|
||||
|
||||
if total_keys < KEY_REQUIRED_COUNT:
|
||||
return {
|
||||
"ok": false,
|
||||
"reason": "MISSING_KEYS",
|
||||
"current_keys": total_keys,
|
||||
"required_keys": KEY_REQUIRED_COUNT,
|
||||
"msg": "进入冰龙巢穴需要 3 把扭曲钥匙!你当前拥有 %d 把。" % total_keys
|
||||
}
|
||||
|
||||
# 扣除 3 把扭曲钥匙
|
||||
var needed: int = KEY_REQUIRED_COUNT
|
||||
for ks in key_slots:
|
||||
var slot_idx: int = ks["slot"]
|
||||
var available: int = ks["count"]
|
||||
if available <= needed:
|
||||
needed -= available
|
||||
inventory[slot_idx] = null
|
||||
else:
|
||||
inventory[slot_idx]["count"] = available - needed
|
||||
needed = 0
|
||||
if needed <= 0:
|
||||
break
|
||||
|
||||
# 初始化巢穴状态
|
||||
is_active = true
|
||||
remaining_time = TIME_LIMIT
|
||||
dragon_hp = BOSS_MAX_HP
|
||||
dragon_max_hp = BOSS_MAX_HP
|
||||
is_dragon_alive = true
|
||||
active_minions.clear()
|
||||
regen_timer = 0.0
|
||||
|
||||
# 刷新 4 块龙神魔石
|
||||
active_metins.clear()
|
||||
for vnum in METIN_TABLE.keys():
|
||||
active_metins[vnum] = true
|
||||
|
||||
dungeon_started.emit(remaining_time)
|
||||
|
||||
return {
|
||||
"ok": true,
|
||||
"time_limit": remaining_time,
|
||||
"dragon_hp": dragon_hp,
|
||||
"metins": active_metins.duplicate(),
|
||||
"msg": "已使用 3 把扭曲钥匙,成功开启冰龙巢穴!限时 60 分钟!"
|
||||
}
|
||||
|
||||
# 获取当前冰龙实际攻击力 (受 8034 复仇之石影响)
|
||||
func get_dragon_atk() -> int:
|
||||
var atk := BOSS_BASE_ATK
|
||||
if active_metins.get(8034, false):
|
||||
atk = int(atk * 1.4) # +40% ATK
|
||||
return atk
|
||||
|
||||
# 获取当前冰龙防御力
|
||||
func get_dragon_def() -> int:
|
||||
return BOSS_BASE_DEF
|
||||
|
||||
# 击破魔石机制
|
||||
func destroy_metin(metin_vnum: int) -> Dictionary:
|
||||
if not is_active:
|
||||
return {"ok": false, "reason": "DUNGEON_NOT_ACTIVE"}
|
||||
|
||||
if not active_metins.get(metin_vnum, false):
|
||||
return {"ok": false, "reason": "METIN_ALREADY_DESTROYED"}
|
||||
|
||||
active_metins[metin_vnum] = false
|
||||
var mname: String = METIN_TABLE[metin_vnum]["name"]
|
||||
|
||||
# 击破魔石唤醒 4 只塞陶龙宫护卫 (vnum: 2401)
|
||||
var spawn_count := 4
|
||||
for i in range(spawn_count):
|
||||
active_minions.append({"vnum": 2401, "hp": 30000})
|
||||
|
||||
metin_destroyed.emit(metin_vnum, mname)
|
||||
minions_spawned.emit(spawn_count, 2401)
|
||||
|
||||
return {
|
||||
"ok": true,
|
||||
"metin_vnum": metin_vnum,
|
||||
"metin_name": mname,
|
||||
"remaining_metins": get_alive_metin_count(),
|
||||
"spawned_minions": spawn_count,
|
||||
"msg": "【%s】已被击破!对应龙神加成已破除,但唤醒了龙宫护卫!" % mname
|
||||
}
|
||||
|
||||
# 获取存活魔石数量
|
||||
func get_alive_metin_count() -> int:
|
||||
var count := 0
|
||||
for v in active_metins.values():
|
||||
if v:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
# 对冰龙造成伤害
|
||||
func apply_damage(raw_dmg: int, is_magic: bool = false) -> Dictionary:
|
||||
if not is_active or not is_dragon_alive:
|
||||
return {"ok": false, "reason": "DRAGON_NOT_ALIVE"}
|
||||
|
||||
var def := get_dragon_def()
|
||||
var net_dmg: float = max(1, raw_dmg - def)
|
||||
|
||||
# 8031 傲慢之石减免 30% 物理伤害
|
||||
if not is_magic and active_metins.get(8031, false):
|
||||
net_dmg *= 0.70
|
||||
|
||||
# 8033 平静之石减免 30% 魔法伤害
|
||||
if is_magic and active_metins.get(8033, false):
|
||||
net_dmg *= 0.70
|
||||
|
||||
var final_dmg := int(round(net_dmg))
|
||||
dragon_hp = max(0, dragon_hp - final_dmg)
|
||||
|
||||
dragon_hp_changed.emit(dragon_hp, dragon_max_hp)
|
||||
|
||||
# 击杀冰龙判定
|
||||
if dragon_hp <= 0:
|
||||
is_dragon_alive = false
|
||||
is_active = false
|
||||
var drops := _generate_dragon_drops()
|
||||
dragon_defeated.emit(drops)
|
||||
return {
|
||||
"ok": true,
|
||||
"killed": true,
|
||||
"net_dmg": final_dmg,
|
||||
"remaining_hp": 0,
|
||||
"drops": drops,
|
||||
"msg": "【冰龙贝兰-塞陶】已被彻底击败!龙鳞与龙爪散落一地!"
|
||||
}
|
||||
|
||||
return {
|
||||
"ok": true,
|
||||
"killed": false,
|
||||
"net_dmg": final_dmg,
|
||||
"remaining_hp": dragon_hp,
|
||||
"hp_ratio": float(dragon_hp) / float(dragon_max_hp)
|
||||
}
|
||||
|
||||
# 逐帧更新 (计时与 8032 寂寞之石生命回复)
|
||||
func update(dt: float) -> void:
|
||||
if not is_active:
|
||||
return
|
||||
|
||||
remaining_time -= dt
|
||||
if remaining_time <= 0.0:
|
||||
is_active = false
|
||||
is_dragon_alive = false
|
||||
dungeon_failed.emit("TIME_OUT")
|
||||
return
|
||||
|
||||
# 8032 寂寞之石:每 5 秒为冰龙回复 2% 最大生命 (16000 HP)
|
||||
if is_dragon_alive and active_metins.get(8032, false):
|
||||
regen_timer += dt
|
||||
if regen_timer >= 5.0:
|
||||
regen_timer = 0.0
|
||||
var heal_amount := int(BOSS_MAX_HP * 0.02)
|
||||
dragon_hp = mini(BOSS_MAX_HP, dragon_hp + heal_amount)
|
||||
dragon_hp_changed.emit(dragon_hp, dragon_max_hp)
|
||||
|
||||
# 生成冰龙掉落物 (71123 龙鳞, 71129 龙爪, 50186 水龙宝箱)
|
||||
func _generate_dragon_drops() -> Array:
|
||||
var drops: Array = []
|
||||
|
||||
# 1. 必掉史诗升阶材料:龙鳞 (71123)
|
||||
drops.append({
|
||||
"vnum": 71123,
|
||||
"name": "龙鳞",
|
||||
"count": randi_range(1, 2)
|
||||
})
|
||||
|
||||
# 2. 必掉史诗升阶材料:龙爪 (71129)
|
||||
drops.append({
|
||||
"vnum": 71129,
|
||||
"name": "龙爪",
|
||||
"count": randi_range(1, 2)
|
||||
})
|
||||
|
||||
# 3. 必掉水龙专属宝箱 (50186)
|
||||
drops.append({
|
||||
"vnum": 50186,
|
||||
"name": "贝兰-塞陶宝箱",
|
||||
"count": 1
|
||||
})
|
||||
|
||||
# 4. 必掉灵魂之石 (50513)
|
||||
drops.append({
|
||||
"vnum": 50513,
|
||||
"name": "灵魂之石",
|
||||
"count": 1
|
||||
})
|
||||
|
||||
# 5. 大额金币掉落
|
||||
drops.append({
|
||||
"vnum": 1,
|
||||
"name": "金币",
|
||||
"count": randi_range(150000, 300000)
|
||||
})
|
||||
|
||||
return drops
|
||||
Reference in New Issue
Block a user