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

299 lines
8.8 KiB
GDScript

# world_boss_ai_system.gd —— Metin2 40250 野外世界首领动态巡逻与仇恨反击 AI 1:1
# 对照 40250 服务端 char_battle.cpp, battle.cpp, mob_manager.cpp, char_state.cpp
class_name WorldBossAISystem
extends RefCounted
signal state_changed(old_state: String, new_state: String)
signal target_acquired(target_id: String, distance: float)
signal skill_cast(skill_name: String, damage: int, aoe_radius: float)
signal boss_enraged(atk_multiplier: float)
signal leash_reset_started()
signal leash_reset_completed()
const STATE_IDLE := "idle"
const STATE_PATROL := "patrol"
const STATE_CHASE := "chase"
const STATE_ATTACK := "attack"
const STATE_SKILL := "skill"
const STATE_ENRAGED := "enraged"
const STATE_LEASH_RETURN := "leash_return"
const STATE_DEAD := "dead"
const AGGRO_RADIUS: float = 18.0 # 索敌警戒范围 (米)
const ATTACK_RANGE: float = 3.5 # 普通近战攻击距离 (米)
const SKILL_RANGE: float = 9.0 # 首领大招攻击距离 (米)
const LEASH_DISTANCE: float = 35.0 # 脱战最远距离 (米)
const PATROL_RADIUS: float = 12.0 # 巡逻游荡半径 (米)
const SKILL_COOLDOWN: float = 8.0 # 技能冷却时间 (秒)
const ENRAGE_HP_RATIO: float = 0.30 # 30% 残血狂暴触发线
const BOSS_CONFIG: Dictionary = {
1901: {
"name": "九尾妖狐",
"level": 72,
"max_hp": 280000,
"atk": 850,
"def": 450,
"skill_name": "暴风雪呼啸",
"skill_mult": 1.8,
"skill_aoe": 8.0
},
1304: {
"name": "黄虎领主",
"level": 75,
"max_hp": 320000,
"atk": 920,
"def": 480,
"skill_name": "神虎震地啸",
"skill_mult": 2.0,
"skill_aoe": 7.5
},
2191: {
"name": "巨龟仙人",
"level": 67,
"max_hp": 350000,
"atk": 780,
"def": 600,
"skill_name": "地刺岩崩",
"skill_mult": 1.6,
"skill_aoe": 9.0
},
2206: {
"name": "烈焰之王",
"level": 73,
"max_hp": 300000,
"atk": 980,
"def": 420,
"skill_name": "地狱火海",
"skill_mult": 2.2,
"skill_aoe": 8.5
}
}
var vnum: int = 0
var boss_name: String = ""
var level: int = 1
var max_hp: int = 100000
var current_hp: int = 100000
var base_atk: int = 500
var base_def: int = 300
var skill_name: String = ""
var skill_mult: float = 1.8
var skill_aoe: float = 8.0
var spawn_position: Vector3 = Vector3.ZERO
var current_position: Vector3 = Vector3.ZERO
var patrol_destination: Vector3 = Vector3.ZERO
var current_state: String = STATE_IDLE
var is_enraged: bool = false
var skill_timer: float = 0.0
var patrol_wait_timer: float = 0.0
var current_target_id: String = ""
var aggro_table: Dictionary = {} # target_id -> threat_points
# 初始化世界首领
func init_boss(boss_vnum: int, pos: Vector3 = Vector3.ZERO) -> Dictionary:
if not BOSS_CONFIG.has(boss_vnum):
return {"ok": false, "reason": "INVALID_BOSS_VNUM", "msg": "未知的世界 Boss 代码!"}
var cfg = BOSS_CONFIG[boss_vnum]
vnum = boss_vnum
boss_name = cfg["name"]
level = cfg["level"]
max_hp = cfg["max_hp"]
current_hp = cfg["max_hp"]
base_atk = cfg["atk"]
base_def = cfg["def"]
skill_name = cfg["skill_name"]
skill_mult = cfg["skill_mult"]
skill_aoe = cfg["skill_aoe"]
spawn_position = pos
current_position = pos
patrol_destination = pos
current_state = STATE_IDLE
is_enraged = false
skill_timer = 0.0
patrol_wait_timer = 2.0
current_target_id = ""
aggro_table.clear()
return {
"ok": true,
"vnum": vnum,
"name": boss_name,
"level": level,
"hp": current_hp,
"pos": current_position
}
# 改变当前状态
func _change_state(new_state: String) -> void:
if current_state == new_state:
return
var old = current_state
current_state = new_state
state_changed.emit(old, new_state)
# 核心 AI 帧心跳更新驱动 (对照 40250 char_state.cpp)
func update_ai(delta: float, target_pos: Vector3, target_id: String = "player", is_target_alive: bool = true) -> Dictionary:
if current_state == STATE_DEAD:
return {"state": STATE_DEAD, "action": "none"}
# 技能冷却计时
if skill_timer > 0.0:
skill_timer = max(0.0, skill_timer - delta)
# 1. 脱战判定:如果距离出生点超出 LEASH_DISTANCE,强制触发回拉
var dist_from_spawn = current_position.distance_to(spawn_position)
if dist_from_spawn > LEASH_DISTANCE and current_state != STATE_LEASH_RETURN:
_start_leash_return()
return {"state": STATE_LEASH_RETURN, "action": "leashing", "msg": "超出脱战范围,首领无敌回满血并返回出生点!"}
# 2. 回拉返回出生点过程
if current_state == STATE_LEASH_RETURN:
var step = 12.0 * delta # 高速回跑
current_position = current_position.move_toward(spawn_position, step)
if current_position.distance_to(spawn_position) < 0.5:
current_position = spawn_position
current_hp = max_hp
aggro_table.clear()
current_target_id = ""
is_enraged = false
_change_state(STATE_IDLE)
leash_reset_completed.emit()
return {"state": STATE_IDLE, "action": "reset_done"}
return {"state": STATE_LEASH_RETURN, "action": "moving_home"}
# 3. 仇恨目标存活与索敌检测
var dist_to_target = current_position.distance_to(target_pos)
var has_aggro = is_target_alive and (dist_to_target <= AGGRO_RADIUS or aggro_table.has(target_id))
if not has_aggro or not is_target_alive:
# 失去仇恨或目标死亡 -> 回归巡逻/待机
current_target_id = ""
_handle_patrol_idle(delta)
return {"state": current_state, "action": "wandering"}
# 确认锁定仇恨目标
if current_target_id != target_id:
current_target_id = target_id
target_acquired.emit(target_id, dist_to_target)
# 4. 处于战斗追击 / 攻击决策
if dist_to_target > SKILL_RANGE:
# 距离过远,快速追赶
_change_state(STATE_CHASE)
var move_speed = 6.0 if not is_enraged else 7.8
current_position = current_position.move_toward(target_pos, move_speed * delta)
return {"state": STATE_CHASE, "action": "chasing", "dist": dist_to_target}
# 5. 技能大招释放判定
if skill_timer <= 0.0 and dist_to_target <= SKILL_RANGE:
skill_timer = SKILL_COOLDOWN
_change_state(STATE_SKILL)
var dmg_mult = skill_mult * (1.5 if is_enraged else 1.0)
var skill_dmg = int(float(base_atk) * dmg_mult)
skill_cast.emit(skill_name, skill_dmg, skill_aoe)
return {
"state": STATE_SKILL,
"action": "skill",
"skill_name": skill_name,
"damage": skill_dmg,
"aoe": skill_aoe
}
# 6. 普通攻击判定
if dist_to_target <= ATTACK_RANGE:
_change_state(STATE_ATTACK)
var atk_dmg = int(float(base_atk) * (1.5 if is_enraged else 1.0))
return {
"state": STATE_ATTACK,
"action": "basic_attack",
"damage": atk_dmg
}
else:
# 在技能范围内但未到普攻范围 -> 继续靠近
_change_state(STATE_CHASE)
var move_speed = 5.0 if not is_enraged else 6.5
current_position = current_position.move_toward(target_pos, move_speed * delta)
return {"state": STATE_CHASE, "action": "closing_in", "dist": dist_to_target}
# 待机与路点漫步巡逻处理
func _handle_patrol_idle(delta: float) -> void:
if current_state != STATE_PATROL:
_change_state(STATE_PATROL)
patrol_wait_timer -= delta
if patrol_wait_timer <= 0.0:
# 选取出生点周围随机路点
var angle = randf_range(0, TAU)
var r = randf_range(2.0, PATROL_RADIUS)
patrol_destination = spawn_position + Vector3(cos(angle) * r, 0.0, sin(angle) * r)
patrol_wait_timer = randf_range(4.0, 7.0)
if current_position.distance_to(patrol_destination) > 0.5:
current_position = current_position.move_toward(patrol_destination, 2.5 * delta)
# 开始拉回重置
func _start_leash_return() -> void:
_change_state(STATE_LEASH_RETURN)
leash_reset_started.emit()
# 受到攻击与伤害结算
func receive_damage(raw_dmg: int, attacker_id: String = "player") -> Dictionary:
if current_state == STATE_DEAD:
return {"ok": false, "reason": "ALREADY_DEAD"}
if current_state == STATE_LEASH_RETURN:
# 脱战回跑期间无敌
return {"ok": true, "immune": true, "net_dmg": 0, "msg": "首领脱战回归中,处于神圣免疫状态!"}
var net_dmg = max(1, raw_dmg - base_def)
current_hp = max(0, current_hp - net_dmg)
# 累计仇恨度
aggro_table[attacker_id] = aggro_table.get(attacker_id, 0) + net_dmg
# 狂暴判定:HP <= 30%
var hp_ratio = float(current_hp) / float(max_hp)
if hp_ratio <= ENRAGE_HP_RATIO and not is_enraged:
is_enraged = true
boss_enraged.emit(1.5)
# 死亡判定
if current_hp <= 0:
_change_state(STATE_DEAD)
return {
"ok": true,
"dead": true,
"net_dmg": net_dmg,
"remaining_hp": 0,
"msg": "【%s】发出震天哀鸣,轰然倒地!" % boss_name
}
return {
"ok": true,
"dead": false,
"net_dmg": net_dmg,
"remaining_hp": current_hp,
"hp_ratio": hp_ratio,
"is_enraged": is_enraged
}
# 获取当前首领状态摘要
func get_boss_status() -> Dictionary:
return {
"vnum": vnum,
"name": boss_name,
"level": level,
"state": current_state,
"current_hp": current_hp,
"max_hp": max_hp,
"is_enraged": is_enraged,
"pos": current_position,
"spawn_pos": spawn_position
}