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,298 @@
|
||||
# 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
|
||||
}
|
||||
Reference in New Issue
Block a user