- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
310 lines
9.2 KiB
GDScript
310 lines
9.2 KiB
GDScript
# world_boss_system.gd —— Metin2 40250 野外四大世界 Boss 巡回战与专属宝箱 1:1
|
||
# 对照 40250 服务端 char_battle.cpp, regen.cpp, mob_drop_item.txt, special_item_group.txt
|
||
class_name WorldBossSystem
|
||
extends RefCounted
|
||
|
||
signal boss_spawned(boss_vnum: int, boss_name: String, position: Vector3)
|
||
signal boss_hp_changed(current_hp: int, max_hp: int)
|
||
signal minions_summoned(boss_vnum: int, minion_vnum: int, count: int)
|
||
signal boss_enraged(boss_vnum: int)
|
||
signal boss_killed(boss_vnum: int, killer_pos: Vector3, drops: Array)
|
||
|
||
const BOSS_TABLE: Dictionary = {
|
||
1901: {
|
||
"name": "九尾妖狐",
|
||
"zone": "冰山",
|
||
"max_hp": 280000,
|
||
"base_atk": 850,
|
||
"base_def": 450,
|
||
"chest_vnum": 50006,
|
||
"chest_name": "九尾妖狐宝箱",
|
||
"minion_vnum": 2204, # 冰山雪怪
|
||
"minion_count": 4,
|
||
"gold_min": 50000,
|
||
"gold_max": 100000
|
||
},
|
||
2191: {
|
||
"name": "巨型沙漠龟",
|
||
"zone": "永壁沙漠",
|
||
"max_hp": 320000,
|
||
"base_atk": 920,
|
||
"base_def": 520,
|
||
"chest_vnum": 50007,
|
||
"chest_name": "沙漠龟宝箱",
|
||
"minion_vnum": 2108, # 沙漠强盗
|
||
"minion_count": 4,
|
||
"gold_min": 60000,
|
||
"gold_max": 120000
|
||
},
|
||
1304: {
|
||
"name": "黄虎之魂",
|
||
"zone": "黄龙神殿",
|
||
"max_hp": 350000,
|
||
"base_atk": 1050,
|
||
"base_def": 480,
|
||
"chest_vnum": 50008,
|
||
"chest_name": "黄虎宝箱",
|
||
"minion_vnum": 1302, # 狂暴神殿狂热信徒
|
||
"minion_count": 4,
|
||
"gold_min": 70000,
|
||
"gold_max": 130000
|
||
},
|
||
2206: {
|
||
"name": "烈焰领主",
|
||
"zone": "烈焰原野",
|
||
"max_hp": 400000,
|
||
"base_atk": 1200,
|
||
"base_def": 500,
|
||
"chest_vnum": 50009,
|
||
"chest_name": "烈焰宝箱",
|
||
"minion_vnum": 2202, # 烈焰幽灵
|
||
"minion_count": 4,
|
||
"gold_min": 80000,
|
||
"gold_max": 150000
|
||
}
|
||
}
|
||
|
||
# 宝箱开启奖池 (40250 special_item_group.txt 1:1)
|
||
const CHEST_LOOT_TABLE: Dictionary = {
|
||
50006: [
|
||
{"vnum": 50513, "name": "灵魂之石", "count": 1, "weight": 30},
|
||
{"vnum": 25040, "name": "祝福卷轴", "count": 1, "weight": 25},
|
||
{"vnum": 169, "name": "战神之剑+9", "count": 1, "weight": 15},
|
||
{"vnum": 11299, "name": "黑钢甲+9", "count": 1, "weight": 10},
|
||
{"vnum": 71084, "name": "高级附魔石", "count": 3, "weight": 20}
|
||
],
|
||
50007: [
|
||
{"vnum": 50513, "name": "灵魂之石", "count": 1, "weight": 30},
|
||
{"vnum": 25040, "name": "祝福卷轴", "count": 1, "weight": 25},
|
||
{"vnum": 3159, "name": "鬼牙刃+9", "count": 1, "weight": 15},
|
||
{"vnum": 11499, "name": "黑风甲+9", "count": 1, "weight": 10},
|
||
{"vnum": 71084, "name": "高级附魔石", "count": 3, "weight": 20}
|
||
],
|
||
50008: [
|
||
{"vnum": 50513, "name": "灵魂之石", "count": 1, "weight": 30},
|
||
{"vnum": 25040, "name": "祝福卷轴", "count": 1, "weight": 25},
|
||
{"vnum": 1109, "name": "党人斧+9", "count": 1, "weight": 15},
|
||
{"vnum": 11699, "name": "黑仙袍+9", "count": 1, "weight": 10},
|
||
{"vnum": 71084, "name": "高级附魔石", "count": 3, "weight": 20}
|
||
],
|
||
50009: [
|
||
{"vnum": 50513, "name": "灵魂之石", "count": 1, "weight": 30},
|
||
{"vnum": 25040, "name": "祝福卷轴", "count": 1, "weight": 25},
|
||
{"vnum": 189, "name": "天狼剑+9", "count": 1, "weight": 15},
|
||
{"vnum": 11899, "name": "黑龙袍+9", "count": 1, "weight": 10},
|
||
{"vnum": 71084, "name": "高级附魔石", "count": 3, "weight": 20}
|
||
]
|
||
}
|
||
|
||
# 当前活跃的世界 Boss
|
||
var active_boss: Dictionary = {}
|
||
var is_boss_alive: bool = false
|
||
var respawn_timers: Dictionary = {} # mob_vnum -> remaining_cooldown
|
||
|
||
func spawn_boss(mob_vnum: int, pos: Vector3 = Vector3.ZERO) -> Dictionary:
|
||
if not BOSS_TABLE.has(mob_vnum):
|
||
return {"ok": false, "reason": "INVALID_BOSS_VNUM", "msg": "未知的世界Boss!"}
|
||
|
||
var cfg: Dictionary = BOSS_TABLE[mob_vnum]
|
||
active_boss = {
|
||
"vnum": mob_vnum,
|
||
"name": cfg["name"],
|
||
"zone": cfg["zone"],
|
||
"max_hp": cfg["max_hp"],
|
||
"current_hp": cfg["max_hp"],
|
||
"base_atk": cfg["base_atk"],
|
||
"current_atk": cfg["base_atk"],
|
||
"base_def": cfg["base_def"],
|
||
"current_def": cfg["base_def"],
|
||
"chest_vnum": cfg["chest_vnum"],
|
||
"chest_name": cfg["chest_name"],
|
||
"minion_vnum": cfg["minion_vnum"],
|
||
"minion_count": cfg["minion_count"],
|
||
"position": pos,
|
||
"is_enraged": false,
|
||
"phase_75_triggered": false,
|
||
"phase_50_triggered": false,
|
||
"phase_25_triggered": false,
|
||
"summoned_minions": []
|
||
}
|
||
is_boss_alive = true
|
||
|
||
boss_spawned.emit(mob_vnum, cfg["name"], pos)
|
||
|
||
return {
|
||
"ok": true,
|
||
"boss": active_boss.duplicate(),
|
||
"msg": "【世界Boss】%s 已在 %s 现身!" % [cfg["name"], cfg["zone"]]
|
||
}
|
||
|
||
# 承受伤害与阶段机制判定
|
||
func apply_damage(raw_dmg: int) -> Dictionary:
|
||
if not is_boss_alive or active_boss.is_empty():
|
||
return {"ok": false, "reason": "NO_ACTIVE_BOSS", "msg": "当前没有存活的世界Boss!"}
|
||
|
||
var def: int = active_boss["current_def"]
|
||
var net_dmg: int = max(1, raw_dmg - def)
|
||
active_boss["current_hp"] = max(0, active_boss["current_hp"] - net_dmg)
|
||
|
||
var cur_hp: int = active_boss["current_hp"]
|
||
var max_hp: int = active_boss["max_hp"]
|
||
var hp_ratio: float = float(cur_hp) / float(max_hp)
|
||
|
||
boss_hp_changed.emit(cur_hp, max_hp)
|
||
|
||
var triggered_events: Array = []
|
||
|
||
# 1. 阶段小怪召唤机制 (75%, 50%, 25%)
|
||
if hp_ratio <= 0.75 and not active_boss["phase_75_triggered"]:
|
||
active_boss["phase_75_triggered"] = true
|
||
_summon_minions(active_boss["minion_vnum"], active_boss["minion_count"])
|
||
triggered_events.append("PHASE_75_MINIONS")
|
||
|
||
if hp_ratio <= 0.50 and not active_boss["phase_50_triggered"]:
|
||
active_boss["phase_50_triggered"] = true
|
||
_summon_minions(active_boss["minion_vnum"], active_boss["minion_count"])
|
||
triggered_events.append("PHASE_50_MINIONS")
|
||
|
||
if hp_ratio <= 0.25 and not active_boss["phase_25_triggered"]:
|
||
active_boss["phase_25_triggered"] = true
|
||
_summon_minions(active_boss["minion_vnum"], active_boss["minion_count"])
|
||
triggered_events.append("PHASE_25_MINIONS")
|
||
|
||
# 2. 残血狂暴机制 (<= 30% HP)
|
||
if hp_ratio <= 0.30 and not active_boss["is_enraged"]:
|
||
active_boss["is_enraged"] = true
|
||
active_boss["current_atk"] = int(active_boss["base_atk"] * 1.5)
|
||
boss_enraged.emit(active_boss["vnum"])
|
||
triggered_events.append("BOSS_ENRAGED")
|
||
|
||
# 3. 击杀判定
|
||
if cur_hp <= 0:
|
||
is_boss_alive = false
|
||
var drops: Array = _generate_boss_drops(active_boss["vnum"])
|
||
var death_pos: Vector3 = active_boss["position"]
|
||
var boss_name: String = active_boss["name"]
|
||
var boss_vnum: int = active_boss["vnum"]
|
||
|
||
boss_killed.emit(boss_vnum, death_pos, drops)
|
||
|
||
return {
|
||
"ok": true,
|
||
"killed": true,
|
||
"net_dmg": net_dmg,
|
||
"remaining_hp": 0,
|
||
"drops": drops,
|
||
"triggered_events": triggered_events,
|
||
"msg": "【世界Boss】%s 已被击杀!掉落专属宝箱!" % boss_name
|
||
}
|
||
|
||
return {
|
||
"ok": true,
|
||
"killed": false,
|
||
"net_dmg": net_dmg,
|
||
"remaining_hp": cur_hp,
|
||
"hp_ratio": hp_ratio,
|
||
"is_enraged": active_boss["is_enraged"],
|
||
"current_atk": active_boss["current_atk"],
|
||
"triggered_events": triggered_events
|
||
}
|
||
|
||
# 召唤护卫小怪
|
||
func _summon_minions(minion_vnum: int, count: int) -> void:
|
||
for i in range(count):
|
||
var offset := Vector3(randf_range(-3.0, 3.0), 0.0, randf_range(-3.0, 3.0))
|
||
active_boss["summoned_minions"].append({
|
||
"vnum": minion_vnum,
|
||
"pos": active_boss["position"] + offset
|
||
})
|
||
minions_summoned.emit(active_boss["vnum"], minion_vnum, count)
|
||
|
||
# 生成 Boss 击杀掉落物
|
||
func _generate_boss_drops(mob_vnum: int) -> Array:
|
||
var cfg: Dictionary = BOSS_TABLE[mob_vnum]
|
||
var drops: Array = []
|
||
|
||
# 1. 必掉专属宝箱
|
||
drops.append({
|
||
"vnum": cfg["chest_vnum"],
|
||
"name": cfg["chest_name"],
|
||
"count": 1
|
||
})
|
||
|
||
# 2. 必掉灵魂之石 (50513)
|
||
drops.append({
|
||
"vnum": 50513,
|
||
"name": "灵魂之石",
|
||
"count": 1
|
||
})
|
||
|
||
# 3. 必掉祝福卷轴 (25040)
|
||
drops.append({
|
||
"vnum": 25040,
|
||
"name": "祝福卷轴",
|
||
"count": 1
|
||
})
|
||
|
||
# 4. 大额金币掉落 (散落地上)
|
||
var gold_amount: int = randi_range(cfg["gold_min"], cfg["gold_max"])
|
||
drops.append({
|
||
"vnum": 1, # 金币
|
||
"name": "金币",
|
||
"count": gold_amount
|
||
})
|
||
|
||
return drops
|
||
|
||
# 开启世界 Boss 宝箱
|
||
func open_boss_chest(chest_slot: int, inventory: Array) -> Dictionary:
|
||
if chest_slot < 0 or chest_slot >= inventory.size():
|
||
return {"ok": false, "reason": "INVALID_SLOT", "msg": "无效的背包格子!"}
|
||
|
||
var item = inventory[chest_slot]
|
||
if item == null:
|
||
return {"ok": false, "reason": "EMPTY_SLOT", "msg": "该格子上没有物品!"}
|
||
|
||
var chest_vnum: int = int(item.get("vnum", 0))
|
||
if not CHEST_LOOT_TABLE.has(chest_vnum):
|
||
return {"ok": false, "reason": "NOT_A_BOSS_CHEST", "msg": "这不是世界 Boss 宝箱!"}
|
||
|
||
var pool: Array = CHEST_LOOT_TABLE[chest_vnum]
|
||
|
||
# 权重抽取奖励
|
||
var total_weight: int = 0
|
||
for entry in pool:
|
||
total_weight += int(entry.get("weight", 1))
|
||
|
||
var roll := randi_range(1, total_weight)
|
||
var accumulated := 0
|
||
var chosen_reward: Dictionary = {}
|
||
for entry in pool:
|
||
accumulated += int(entry.get("weight", 1))
|
||
if roll <= accumulated:
|
||
chosen_reward = entry.duplicate()
|
||
break
|
||
|
||
if chosen_reward.is_empty():
|
||
chosen_reward = pool[0].duplicate()
|
||
|
||
# 扣除宝箱
|
||
inventory[chest_slot] = null
|
||
|
||
# 放入抽中的奖励
|
||
var stored_slot := -1
|
||
for i in range(inventory.size()):
|
||
if inventory[i] == null:
|
||
inventory[i] = {
|
||
"vnum": chosen_reward["vnum"],
|
||
"count": chosen_reward.get("count", 1),
|
||
"name": chosen_reward["name"]
|
||
}
|
||
stored_slot = i
|
||
break
|
||
|
||
return {
|
||
"ok": true,
|
||
"reward": chosen_reward,
|
||
"stored_slot": stored_slot,
|
||
"msg": "打开宝箱获得了【%s】!" % chosen_reward["name"]
|
||
}
|