- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
205 lines
8.2 KiB
GDScript
205 lines
8.2 KiB
GDScript
# test_world_boss_parity.gd —— 40250 野外四大世界 Boss 1:1 纯净回归测试套件
|
|
extends SceneTree
|
|
|
|
const WorldBossSystem = preload("res://world_boss_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_parity (Direction 3: World Bosses & Chests 1:1) ===")
|
|
_test_boss_table_and_spawn()
|
|
_test_damage_mitigation_and_phase_minions()
|
|
_test_enrage_mechanic()
|
|
_test_boss_kill_and_drops()
|
|
_test_boss_chest_opening()
|
|
_finish()
|
|
|
|
func _test_boss_table_and_spawn() -> void:
|
|
print("\n--- Test 1: Boss Config Table & Spawning ---")
|
|
var wbs := WorldBossSystem.new()
|
|
|
|
# Invalid boss vnum
|
|
var invalid_res = wbs.spawn_boss(99999)
|
|
_assert(invalid_res["ok"] == false and invalid_res["reason"] == "INVALID_BOSS_VNUM", "Invalid boss vnum rejected")
|
|
|
|
# Check table contains the 4 iconic world bosses
|
|
_assert(WorldBossSystem.BOSS_TABLE.has(1901), "Nine Tails (1901) defined")
|
|
_assert(WorldBossSystem.BOSS_TABLE.has(2191), "Desert Tortoise (2191) defined")
|
|
_assert(WorldBossSystem.BOSS_TABLE.has(1304), "Yellow Tiger (1304) defined")
|
|
_assert(WorldBossSystem.BOSS_TABLE.has(2206), "Flame King (2206) defined")
|
|
|
|
# Spawn Nine Tails (1901)
|
|
var spawn_data := {"fired": false, "name": "", "vnum": 0}
|
|
wbs.boss_spawned.connect(func(vnum: int, bname: String, _pos: Vector3):
|
|
spawn_data["fired"] = true
|
|
spawn_data["name"] = bname
|
|
spawn_data["vnum"] = vnum
|
|
)
|
|
|
|
var spawn_pos := Vector3(50.0, 0.0, -120.0)
|
|
var res = wbs.spawn_boss(1901, spawn_pos)
|
|
_assert(res["ok"] == true, "Nine Tails spawned successfully")
|
|
_assert(wbs.is_boss_alive == true, "Boss is alive")
|
|
_assert(wbs.active_boss["max_hp"] == 280000, "Nine Tails max HP is 280,000")
|
|
_assert(wbs.active_boss["base_atk"] == 850, "Nine Tails base ATK is 850")
|
|
_assert(wbs.active_boss["base_def"] == 450, "Nine Tails base DEF is 450")
|
|
_assert(spawn_data["fired"] == true, "boss_spawned signal emitted")
|
|
_assert(spawn_data["name"] == "九尾妖狐", "Boss name matches")
|
|
|
|
func _test_damage_mitigation_and_phase_minions() -> void:
|
|
print("\n--- Test 2: DEF Mitigation & Phase Minion Spawns (75%, 50%, 25%) ---")
|
|
var wbs := WorldBossSystem.new()
|
|
wbs.spawn_boss(1901) # HP: 280000, DEF: 450
|
|
|
|
var minion_events := {"count": 0, "last_minion_vnum": 0, "last_count": 0}
|
|
wbs.minions_summoned.connect(func(_bvnum: int, mvnum: int, count: int):
|
|
minion_events["count"] += 1
|
|
minion_events["last_minion_vnum"] = mvnum
|
|
minion_events["last_count"] = count
|
|
)
|
|
|
|
# 1. Normal damage without crossing threshold: Raw 1450 - DEF 450 = 1000 dmg
|
|
var res1 = wbs.apply_damage(1450)
|
|
_assert(res1["ok"] == true, "Damage applied successfully")
|
|
_assert(res1["net_dmg"] == 1000, "1450 raw dmg - 450 def = 1000 net dmg")
|
|
_assert(wbs.active_boss["current_hp"] == 279000, "HP updated to 279,000")
|
|
_assert(minion_events["count"] == 0, "No minions summoned yet")
|
|
|
|
# 2. Damage boss to trigger 75% threshold (280000 * 0.75 = 210000 HP threshold)
|
|
# Need 69,000 net damage -> raw damage 69,450
|
|
var res2 = wbs.apply_damage(69450)
|
|
_assert(wbs.active_boss["current_hp"] == 210000, "Boss at exactly 75% HP (210,000)")
|
|
_assert(res2["triggered_events"].has("PHASE_75_MINIONS"), "PHASE_75_MINIONS triggered")
|
|
_assert(minion_events["count"] == 1, "Minion summon signal fired 1 time")
|
|
_assert(minion_events["last_minion_vnum"] == 2204, "Summoned Ice Yeti minions (vnum 2204)")
|
|
_assert(minion_events["last_count"] == 4, "Summoned 4 minions")
|
|
|
|
# Additional attack should not re-trigger 75%
|
|
var res3 = wbs.apply_damage(1450)
|
|
_assert(not res3["triggered_events"].has("PHASE_75_MINIONS"), "75% phase does not retrigger")
|
|
|
|
# 3. Damage boss to cross 50% threshold (140,000 HP)
|
|
# Current HP is 209,000. Need 69,000 net damage -> raw 69,450
|
|
var res4 = wbs.apply_damage(69450)
|
|
_assert(wbs.active_boss["current_hp"] == 140000, "Boss at 50% HP (140,000)")
|
|
_assert(res4["triggered_events"].has("PHASE_50_MINIONS"), "PHASE_50_MINIONS triggered")
|
|
_assert(minion_events["count"] == 2, "Minion summon count now 2")
|
|
|
|
func _test_enrage_mechanic() -> void:
|
|
print("\n--- Test 3: Enrage Mechanic at <= 30% HP (+50% ATK) ---")
|
|
var wbs := WorldBossSystem.new()
|
|
wbs.spawn_boss(2206) # Flame King: HP 400000, Base ATK 1200, Base DEF 500
|
|
|
|
var enrage_data := {"fired": false, "boss_vnum": 0}
|
|
wbs.boss_enraged.connect(func(bvnum: int):
|
|
enrage_data["fired"] = true
|
|
enrage_data["boss_vnum"] = bvnum
|
|
)
|
|
|
|
_assert(wbs.active_boss["is_enraged"] == false, "Boss initially not enraged")
|
|
_assert(wbs.active_boss["current_atk"] == 1200, "Base ATK is 1200")
|
|
|
|
# 30% of 400,000 = 120,000 HP.
|
|
# Deal 280,000 net damage -> raw damage 280,500
|
|
var res = wbs.apply_damage(280500)
|
|
_assert(wbs.active_boss["current_hp"] == 120000, "Boss HP reached 120,000 (30%)")
|
|
_assert(res["triggered_events"].has("BOSS_ENRAGED"), "BOSS_ENRAGED triggered in events")
|
|
_assert(enrage_data["fired"] == true, "boss_enraged signal fired")
|
|
_assert(enrage_data["boss_vnum"] == 2206, "Enraged boss vnum matches Flame King (2206)")
|
|
_assert(wbs.active_boss["is_enraged"] == true, "is_enraged is now true")
|
|
_assert(wbs.active_boss["current_atk"] == 1800, "ATK increased by 50% to 1800")
|
|
|
|
func _test_boss_kill_and_drops() -> void:
|
|
print("\n--- Test 4: Boss Kill & Guaranteed Chest Drops ---")
|
|
var wbs := WorldBossSystem.new()
|
|
wbs.spawn_boss(1304) # Yellow Tiger: HP 350000, DEF 480, Chest: 50008
|
|
|
|
var kill_data := {"fired": false, "drops": []}
|
|
wbs.boss_killed.connect(func(_bvnum: int, _pos: Vector3, drops: Array):
|
|
kill_data["fired"] = true
|
|
kill_data["drops"] = drops
|
|
)
|
|
|
|
# Deal lethal damage
|
|
var kill_res = wbs.apply_damage(500000)
|
|
_assert(kill_res["ok"] == true and kill_res["killed"] == true, "Boss defeated")
|
|
_assert(wbs.is_boss_alive == false, "Boss is dead")
|
|
_assert(kill_data["fired"] == true, "boss_killed signal fired")
|
|
|
|
var drops: Array = kill_res["drops"]
|
|
_assert(drops.size() >= 4, "Drops include chest, soul stone, scroll, and gold")
|
|
|
|
# Verify guaranteed chest (50008)
|
|
var has_chest := false
|
|
var has_soul_stone := false
|
|
var has_scroll := false
|
|
var has_gold := false
|
|
|
|
for d in drops:
|
|
if d["vnum"] == 50008:
|
|
has_chest = true
|
|
elif d["vnum"] == 50513:
|
|
has_soul_stone = true
|
|
elif d["vnum"] == 25040:
|
|
has_scroll = true
|
|
elif d["vnum"] == 1 and d["count"] >= 70000:
|
|
has_gold = true
|
|
|
|
_assert(has_chest, "Guaranteed Yellow Tiger Chest (50008) dropped")
|
|
_assert(has_soul_stone, "Guaranteed Soul Stone (50513) dropped")
|
|
_assert(has_scroll, "Guaranteed Blessing Scroll (25040) dropped")
|
|
_assert(has_gold, "Gold dropped within range (>= 70,000)")
|
|
|
|
func _test_boss_chest_opening() -> void:
|
|
print("\n--- Test 5: World Boss Chest Opening (40250 Loot Pool) ---")
|
|
var wbs := WorldBossSystem.new()
|
|
var inv: Array = [
|
|
{"vnum": 50006, "name": "九尾妖狐宝箱", "count": 1},
|
|
null,
|
|
null
|
|
]
|
|
|
|
# Invalid slot
|
|
var invalid_res = wbs.open_boss_chest(99, inv)
|
|
_assert(invalid_res["ok"] == false and invalid_res["reason"] == "INVALID_SLOT", "Invalid slot rejected")
|
|
|
|
# Empty slot
|
|
var empty_res = wbs.open_boss_chest(1, inv)
|
|
_assert(empty_res["ok"] == false and empty_res["reason"] == "EMPTY_SLOT", "Empty slot rejected")
|
|
|
|
# Non-chest item
|
|
inv[1] = {"vnum": 10, "name": "木剑", "count": 1}
|
|
var non_chest_res = wbs.open_boss_chest(1, inv)
|
|
_assert(non_chest_res["ok"] == false and non_chest_res["reason"] == "NOT_A_BOSS_CHEST", "Non-chest item rejected")
|
|
|
|
# Open Nine Tails Chest (50006) at slot 0
|
|
var res = wbs.open_boss_chest(0, inv)
|
|
_assert(res["ok"] == true, "Chest opened successfully")
|
|
|
|
var reward = res["reward"]
|
|
_assert(reward.has("vnum") and reward.has("name"), "Valid reward generated")
|
|
print(" Received chest reward: %s (vnum: %d, count: %d)" % [reward["name"], reward["vnum"], reward.get("count", 1)])
|
|
|
|
var stored_slot = res["stored_slot"]
|
|
_assert(stored_slot >= 0 and inv[stored_slot] != null, "Reward saved in inventory")
|
|
_assert(inv[stored_slot]["vnum"] == reward["vnum"], "Inventory item matches rewarded vnum")
|
|
_assert(inv[stored_slot]["vnum"] != 50006, "Chest (50006) was consumed and replaced")
|
|
|
|
func _finish() -> void:
|
|
print("\n==========================================")
|
|
print("World Boss Parity Test Results: %d Passed, %d Failed" % [_passed, _failed])
|
|
print("==========================================")
|
|
if _failed > 0:
|
|
quit(1)
|
|
else:
|
|
quit(0)
|