- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
299 lines
8.8 KiB
GDScript
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
|
|
}
|