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