- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
169 lines
6.7 KiB
GDScript
169 lines
6.7 KiB
GDScript
# test_metin_spawn_event_parity.gd —— 40250 野外 8 档魔石与怪潮暴动 1:1 回归测试套件
|
|
extends SceneTree
|
|
|
|
const MetinSpawnEventSystem = preload("res://metin_spawn_event_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_metin_spawn_event_parity (Direction 3: Field Metin Spawn & Waves 1:1) ===")
|
|
_test_table_configuration()
|
|
_test_spawn_metin()
|
|
_test_damage_and_phases()
|
|
_test_destruction_and_drops()
|
|
_test_invalid_inputs()
|
|
_finish()
|
|
|
|
func _test_table_configuration() -> void:
|
|
print("\n--- Test 1: Metin Table Configuration (8008~8014, 8027) ---")
|
|
var expected_vnums = [8008, 8009, 8010, 8011, 8012, 8013, 8014, 8027]
|
|
_assert(MetinSpawnEventSystem.METIN_TABLE.size() == 8, "Table has exactly 8 Metin stone types")
|
|
|
|
for vnum in expected_vnums:
|
|
_assert(MetinSpawnEventSystem.METIN_TABLE.has(vnum), "Has Metin stone %d" % vnum)
|
|
var entry = MetinSpawnEventSystem.METIN_TABLE[vnum]
|
|
_assert(entry.has("name") and entry.has("level") and entry.has("max_hp") and entry.has("def") and entry.has("wave_mob") and entry.has("wave_count"),
|
|
"Metin %d has all required fields" % vnum)
|
|
_assert(entry["max_hp"] > 0 and entry["def"] > 0 and entry["wave_count"] > 0,
|
|
"Metin %d has positive stats" % vnum)
|
|
|
|
func _test_spawn_metin() -> void:
|
|
print("\n--- Test 2: Spawning Metin Stones ---")
|
|
var sys := MetinSpawnEventSystem.new()
|
|
var spawn_sig := {"fired": false, "vnum": 0, "name": "", "pos": Vector3.ZERO}
|
|
sys.metin_spawned.connect(func(v: int, n: String, p: Vector3):
|
|
spawn_sig["fired"] = true
|
|
spawn_sig["vnum"] = v
|
|
spawn_sig["name"] = n
|
|
spawn_sig["pos"] = p
|
|
)
|
|
|
|
var spawn_pos := Vector3(100.5, 0.0, 200.5)
|
|
var res = sys.spawn_metin(8008, spawn_pos)
|
|
_assert(res["ok"] == true, "Spawned 8008 successfully")
|
|
_assert(res["id"] == 1, "First instance ID is 1")
|
|
_assert(sys.active_metins.has(1), "Instance 1 in active_metins")
|
|
_assert(spawn_sig["fired"] == true and spawn_sig["vnum"] == 8008 and spawn_sig["pos"] == spawn_pos,
|
|
"metin_spawned signal emitted correctly")
|
|
|
|
var res2 = sys.spawn_metin(8027, Vector3(50.0, 0.0, 50.0))
|
|
_assert(res2["ok"] == true and res2["id"] == 2, "Second stone instance ID is 2")
|
|
_assert(sys.active_metins.size() == 2, "2 stones active")
|
|
|
|
func _test_damage_and_phases() -> void:
|
|
print("\n--- Test 3: Damage Calculation & 75%, 50%, 25% Mob Waves ---")
|
|
var sys := MetinSpawnEventSystem.new()
|
|
var res = sys.spawn_metin(8008, Vector3.ZERO) # 8008: HP 70000, Def 120, Mob 391, count 4
|
|
var id: int = res["id"]
|
|
|
|
var waves := []
|
|
sys.mob_wave_erupted.connect(func(vnum: int, count: int, minion_vnum: int):
|
|
waves.append({"vnum": vnum, "count": count, "minion_vnum": minion_vnum})
|
|
)
|
|
|
|
# Raw damage 200 - 120 def = 80 net damage
|
|
var atk1 = sys.attack_metin(id, 200)
|
|
_assert(atk1["ok"] == true, "Attack successful")
|
|
_assert(atk1["net_dmg"] == 80, "Net damage = 200 - 120 = 80")
|
|
_assert(atk1["remaining_hp"] == 70000 - 80, "Remaining HP correct")
|
|
_assert(atk1["wave_triggered"] == false, "No wave at > 75% HP")
|
|
_assert(waves.size() == 0, "No wave signal fired")
|
|
|
|
# Damage to trigger <= 75% HP: 70000 * 0.75 = 52500. We deal raw dmg to bring it below 52500.
|
|
# Remaining is 69920. Need to deal at least 69920 - 52500 = 17420 net dmg.
|
|
# Raw dmg = 17420 + 120 = 17540
|
|
var atk_phase_75 = sys.attack_metin(id, 17540)
|
|
_assert(atk_phase_75["wave_triggered"] == true, "Phase 75% wave triggered")
|
|
_assert(atk_phase_75["spawned_count"] == 4, "Spawned 4 minions")
|
|
_assert(waves.size() == 1 and waves[0]["minion_vnum"] == 391, "Wave signal received for 391")
|
|
|
|
# Another hit while still in 50%~75% bracket should NOT trigger wave again
|
|
var atk_repeat = sys.attack_metin(id, 1000)
|
|
_assert(atk_repeat["wave_triggered"] == false, "Subsequent hit doesn't retrigger 75% wave")
|
|
_assert(waves.size() == 1, "Wave count still 1")
|
|
|
|
# Damage to trigger <= 50% HP (35000)
|
|
# Current HP is around 51620. Need 17000 net dmg.
|
|
var atk_phase_50 = sys.attack_metin(id, 20000)
|
|
_assert(atk_phase_50["wave_triggered"] == true, "Phase 50% wave triggered")
|
|
_assert(waves.size() == 2, "Wave count now 2")
|
|
|
|
# Damage to trigger <= 25% HP (17500)
|
|
var atk_phase_25 = sys.attack_metin(id, 20000)
|
|
_assert(atk_phase_25["wave_triggered"] == true, "Phase 25% wave triggered")
|
|
_assert(waves.size() == 3, "Wave count now 3")
|
|
|
|
func _test_destruction_and_drops() -> void:
|
|
print("\n--- Test 4: Metin Destruction & Drops ---")
|
|
var sys := MetinSpawnEventSystem.new()
|
|
var res = sys.spawn_metin(8014, Vector3(10, 0, 10)) # 8014: 死亡之石, lvl 65, HP 260000
|
|
var id: int = res["id"]
|
|
|
|
var destroy_sig := {"fired": false, "vnum": 0, "drops": []}
|
|
sys.metin_destroyed.connect(func(vnum: int, pos: Vector3, drops: Array):
|
|
destroy_sig["fired"] = true
|
|
destroy_sig["vnum"] = vnum
|
|
destroy_sig["drops"] = drops
|
|
)
|
|
|
|
# Overkill blow
|
|
var kill_atk = sys.attack_metin(id, 1000000)
|
|
_assert(kill_atk["ok"] == true and kill_atk["destroyed"] == true, "Metin destroyed")
|
|
_assert(sys.active_metins.has(id) == false, "Stone removed from active_metins")
|
|
_assert(destroy_sig["fired"] == true and destroy_sig["vnum"] == 8014, "metin_destroyed signal emitted")
|
|
|
|
var drops: Array = kill_atk["drops"]
|
|
_assert(drops.size() == 4, "4 drops generated")
|
|
|
|
var has_skill_book := false
|
|
var has_soul_stone := false
|
|
var has_spirit_stone := false
|
|
var has_gold := false
|
|
|
|
for d in drops:
|
|
if d["vnum"] == 50300:
|
|
has_skill_book = true
|
|
elif d["vnum"] == 50513:
|
|
has_soul_stone = true
|
|
elif d["vnum"] == 28430:
|
|
has_spirit_stone = true
|
|
elif d["vnum"] == 1 and d["count"] >= 65 * 500:
|
|
has_gold = true
|
|
|
|
_assert(has_skill_book, "Dropped Skill Book 50300")
|
|
_assert(has_soul_stone, "Dropped Soul Stone 50513")
|
|
_assert(has_spirit_stone, "Dropped Spirit Stone 28430")
|
|
_assert(has_gold, "Dropped Gold scaled with level 65")
|
|
|
|
func _test_invalid_inputs() -> void:
|
|
print("\n--- Test 5: Invalid Metin Inputs ---")
|
|
var sys := MetinSpawnEventSystem.new()
|
|
var spawn_fail = sys.spawn_metin(99999)
|
|
_assert(spawn_fail["ok"] == false and spawn_fail["reason"] == "INVALID_METIN_VNUM",
|
|
"Correctly rejected invalid vnum 99999")
|
|
|
|
var atk_fail = sys.attack_metin(8888, 100)
|
|
_assert(atk_fail["ok"] == false and atk_fail["reason"] == "METIN_NOT_FOUND",
|
|
"Correctly rejected attack on missing instance 8888")
|
|
|
|
func _finish() -> void:
|
|
print("\n==========================================")
|
|
print("Metin Spawn Event 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 Metin Spawn Event parity tests passed!")
|
|
quit(0)
|