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

178 lines
7.4 KiB
GDScript

# test_world_boss_ai_behavior_parity.gd —— 40250 世界首领动态 AI 行为状态机 1:1 回归测试套件
extends SceneTree
const WorldBossAISystem = preload("res://world_boss_ai_system.gd")
var _passed := 0
var _failed := 0
func _assert(cond: bool, msg: String) -> void:
if cond:
_passed += 1
print(" [PASS] %s" % msg)
else:
_failed += 1
printerr(" [FAIL] %s" % msg)
func _init() -> void:
print("\n=== Running test_world_boss_ai_behavior_parity (Direction 1: World Boss AI 1:1) ===")
_test_boss_initialization()
_test_aggro_and_chase()
_test_skills_and_attacks()
_test_enrage_mechanic()
_test_leash_and_reset()
_test_death()
_finish()
func _test_boss_initialization() -> void:
print("\n--- Test 1: Boss Initialization & Table Config ---")
var ai := WorldBossAISystem.new()
var fail = ai.init_boss(99999)
_assert(fail["ok"] == false and fail["reason"] == "INVALID_BOSS_VNUM", "Invalid boss vnum rejected")
var vnums = [1901, 1304, 2191, 2206]
for v in vnums:
var init_res = ai.init_boss(v, Vector3(100, 0, 100))
_assert(init_res["ok"] == true, "Boss %d initialized" % v)
_assert(ai.current_state == WorldBossAISystem.STATE_IDLE, "Starts in IDLE state")
_assert(ai.current_hp == ai.max_hp, "HP at max")
func _test_aggro_and_chase() -> void:
print("\n--- Test 2: Aggro Acquisition & Chase Mechanics ---")
var ai := WorldBossAISystem.new()
ai.init_boss(1901, Vector3.ZERO)
var aggro_sig := {"fired": false, "id": "", "dist": 0.0}
ai.target_acquired.connect(func(tid: String, d: float):
aggro_sig["fired"] = true
aggro_sig["id"] = tid
aggro_sig["dist"] = d
)
# Target 25m away (beyond 18m AGGRO_RADIUS)
var step1 = ai.update_ai(0.1, Vector3(25, 0, 0), "player", true)
_assert(aggro_sig["fired"] == false, "No aggro at 25m")
_assert(step1["action"] == "wandering", "Wandering when target is far")
# Target enters 15m (within 18m AGGRO_RADIUS)
var step2 = ai.update_ai(0.1, Vector3(15, 0, 0), "player", true)
_assert(aggro_sig["fired"] == true and aggro_sig["id"] == "player", "Target acquired signal fired at 15m")
_assert(step2["state"] == WorldBossAISystem.STATE_CHASE, "Transitions to CHASE state")
_assert(ai.current_position.x > 0.0, "Boss stepped forward towards player")
func _test_skills_and_attacks() -> void:
print("\n--- Test 3: Skill Casting & Basic Attacks ---")
var ai := WorldBossAISystem.new()
ai.init_boss(1901, Vector3.ZERO) # Nine tails: Atk 850, Def 450, Skill: 暴风雪呼啸, mult 1.8
var skill_sig := {"fired": false, "name": "", "dmg": 0}
ai.skill_cast.connect(func(sname: String, dmg: int, _aoe: float):
skill_sig["fired"] = true
skill_sig["name"] = sname
skill_sig["dmg"] = dmg
)
# Target at 5.0m (within 9m SKILL_RANGE, but beyond 3.5m ATTACK_RANGE)
var skill_act = ai.update_ai(0.1, Vector3(5, 0, 0), "player", true)
_assert(skill_act["state"] == WorldBossAISystem.STATE_SKILL, "Casts skill when in skill range")
_assert(skill_sig["fired"] == true and skill_sig["name"] == "暴风雪呼啸", "Skill signal emitted with correct name")
_assert(skill_sig["dmg"] == int(850 * 1.8), "Skill damage is base_atk * 1.8")
# Target closes to 2.0m (within 3.5m ATTACK_RANGE)
# Skill is now on cooldown, should perform basic attack
var atk_act = ai.update_ai(0.1, Vector3(2, 0, 0), "player", true)
_assert(atk_act["state"] == WorldBossAISystem.STATE_ATTACK, "Executes basic attack within melee range")
_assert(atk_act["damage"] == 850, "Basic attack damage matches base_atk")
func _test_enrage_mechanic() -> void:
print("\n--- Test 4: Enrage Mechanic (< 30% HP) ---")
var ai := WorldBossAISystem.new()
ai.init_boss(1304, Vector3.ZERO) # 1304: HP 320000, Def 480, Atk 920
var enrage_sig := {"fired": false, "mult": 0.0}
ai.boss_enraged.connect(func(m: float):
enrage_sig["fired"] = true
enrage_sig["mult"] = m
)
# Deal damage down to ~40% HP (no enrage)
# Max HP = 320000. 40% = 128000. Damage needed = 192000 net dmg.
# Raw dmg = 192000 + 480 = 192480.
var hit1 = ai.receive_damage(192480, "player")
_assert(hit1["is_enraged"] == false, "Not enraged at 40% HP")
_assert(enrage_sig["fired"] == false, "boss_enraged signal not yet fired")
# Deal damage down to 25% HP (< 30% HP trigger)
# Need 50000 more net dmg.
var hit2 = ai.receive_damage(50000 + 480, "player")
_assert(hit2["is_enraged"] == true, "Enraged triggered at <= 30% HP")
_assert(enrage_sig["fired"] == true and enrage_sig["mult"] == 1.5, "boss_enraged signal emitted with 1.5x")
# Attack while enraged has 1.5x damage
# 1. Enraged skill cast
var enraged_skill = ai.update_ai(0.1, Vector3(5, 0, 0), "player", true)
_assert(enraged_skill["state"] == WorldBossAISystem.STATE_SKILL, "Enraged skill cast triggered")
_assert(enraged_skill["damage"] == int(920 * 2.0 * 1.5), "Enraged skill gets 1.5x damage multiplier")
# 2. Enraged basic attack (skill is now on cooldown)
var enraged_atk = ai.update_ai(0.1, Vector3(2, 0, 0), "player", true)
_assert(enraged_atk["state"] == WorldBossAISystem.STATE_ATTACK, "Enraged basic attack triggered")
_assert(enraged_atk["damage"] == int(920 * 1.5), "Enraged basic attack gets 1.5x damage")
func _test_leash_and_reset() -> void:
print("\n--- Test 5: Leash Distance & Full Reset ---")
var ai := WorldBossAISystem.new()
ai.init_boss(2191, Vector3.ZERO) # Spawn at (0, 0, 0)
var leash_start := {"fired": false}
var leash_done := {"fired": false}
ai.leash_reset_started.connect(func(): leash_start["fired"] = true)
ai.leash_reset_completed.connect(func(): leash_done["fired"] = true)
# Boss takes damage
ai.receive_damage(100000, "player")
_assert(ai.current_hp < ai.max_hp, "Boss damaged")
# Move boss far beyond 35m LEASH_DISTANCE
ai.current_position = Vector3(40, 0, 0)
var leash_act = ai.update_ai(0.1, Vector3(45, 0, 0), "player", true)
_assert(leash_act["state"] == WorldBossAISystem.STATE_LEASH_RETURN, "Enters LEASH_RETURN state")
_assert(leash_start["fired"] == true, "leash_reset_started signal fired")
# In leash return, attacks are immune
var immune_hit = ai.receive_damage(50000, "player")
_assert(immune_hit.get("immune", false) == true, "Attacks during leash return are IMMUNE")
# Simulate returning to spawn point
while ai.current_state == WorldBossAISystem.STATE_LEASH_RETURN:
ai.update_ai(0.5, Vector3(45, 0, 0), "player", true)
_assert(leash_done["fired"] == true, "leash_reset_completed signal fired")
_assert(ai.current_state == WorldBossAISystem.STATE_IDLE, "Boss back in IDLE")
_assert(ai.current_hp == ai.max_hp, "Boss HP fully restored to 100%")
_assert(ai.current_position.distance_to(Vector3.ZERO) < 0.1, "Boss back at spawn point")
func _test_death() -> void:
print("\n--- Test 6: Death State ---")
var ai := WorldBossAISystem.new()
ai.init_boss(2206, Vector3.ZERO)
var fatal = ai.receive_damage(10000000, "player")
_assert(fatal["dead"] == true, "Fatal hit flags dead == true")
_assert(ai.current_state == WorldBossAISystem.STATE_DEAD, "State becomes DEAD")
var after_death = ai.update_ai(0.1, Vector3.ZERO, "player", true)
_assert(after_death["state"] == WorldBossAISystem.STATE_DEAD and after_death["action"] == "none", "Dead boss does nothing")
func _finish() -> void:
print("\n==========================================")
print("World Boss AI Parity Tests Finished:")
print("Passed: %d, Failed: %d" % [_passed, _failed])
print("==========================================")
if _failed > 0:
printerr("FAILED: Some tests did not pass.")
quit(1)
else:
print("SUCCESS: All World Boss AI parity tests passed!")
quit(0)