Files
mtgodot-poc/project/world_boss_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

310 lines
9.2 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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"]
}