- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
168 lines
6.6 KiB
GDScript
168 lines
6.6 KiB
GDScript
# test_energy_system_parity.gd —— 40250 官方能量碎片与水晶熔炼系统 1:1 回归测试套件
|
|
extends SceneTree
|
|
|
|
const EnergySystem = preload("res://energy_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_energy_system_parity (Direction 1: Energy System 1:1) ===")
|
|
_test_fragment_extraction()
|
|
_test_crystal_fusion()
|
|
_test_crystal_usage_and_buff_lifecycle()
|
|
_test_serialization()
|
|
_finish()
|
|
|
|
func _test_fragment_extraction() -> void:
|
|
print("\n--- Test 1: Energy Fragment Extraction (40250 Quest Rules) ---")
|
|
var sys := EnergySystem.new()
|
|
var inv: Array = [
|
|
{"vnum": 10, "name": "长剑+0", "level_limit": 1},
|
|
{"vnum": 8000, "name": "木箭", "level_limit": 40, "type": 1, "subtype": 5},
|
|
{"vnum": 160, "name": "战斗剑+9", "level_limit": 65}
|
|
]
|
|
|
|
# 1. Player level too low
|
|
var res_low_player = sys.extract_energy_fragments(inv, 2, 30)
|
|
_assert(res_low_player["ok"] == false, "Extraction rejected: player lv 30 < 35")
|
|
_assert(res_low_player["reason"] == "PLAYER_LEVEL_TOO_LOW", "Reason is PLAYER_LEVEL_TOO_LOW")
|
|
|
|
# 2. Equipment level too low (< 35)
|
|
var res_low_equip = sys.extract_energy_fragments(inv, 0, 40)
|
|
_assert(res_low_equip["ok"] == false, "Extraction rejected: equip lv 1 < 35")
|
|
_assert(res_low_equip["reason"] == "EQUIP_LEVEL_TOO_LOW", "Reason is EQUIP_LEVEL_TOO_LOW")
|
|
|
|
# 3. Arrow weapon prohibited
|
|
var res_arrow = sys.extract_energy_fragments(inv, 1, 40)
|
|
_assert(res_arrow["ok"] == false, "Extraction rejected: arrow weapon not allowed")
|
|
_assert(res_arrow["reason"] == "ARROW_NOT_ALLOWED", "Reason is ARROW_NOT_ALLOWED")
|
|
|
|
var extract_sig := {"fired": false, "cnt": 0}
|
|
sys.fragments_extracted.connect(func(c: int, _n: String):
|
|
extract_sig["fired"] = true
|
|
extract_sig["cnt"] = c
|
|
)
|
|
|
|
# 4. Valid extraction of Lv 65 sword
|
|
var res_succ = sys.extract_energy_fragments(inv, 2, 70)
|
|
_assert(res_succ["ok"] == true, "Lv 65 sword extracted successfully")
|
|
_assert(inv[2] != null and inv[2]["vnum"] == EnergySystem.VNUM_ENERGY_STONE, "Slot 2 converted to Energy Stones")
|
|
_assert(inv[2]["count"] >= 3 and inv[2]["count"] <= 8, "Yield is between 3 and 8 fragments")
|
|
_assert(extract_sig["fired"] == true, "fragments_extracted signal emitted")
|
|
|
|
func _test_crystal_fusion() -> void:
|
|
print("\n--- Test 2: Energy Crystal (51002) Fusion (30 Stones + 1000 Gold) ---")
|
|
var sys := EnergySystem.new()
|
|
var player_gold := {"gold": 500} # Less than 1000
|
|
var inv: Array = [
|
|
{"vnum": EnergySystem.VNUM_ENERGY_STONE, "name": "能量碎片", "count": 25},
|
|
null
|
|
]
|
|
|
|
# 1. Gold not enough
|
|
var res_no_gold = sys.fuse_energy_crystal(inv, player_gold)
|
|
_assert(res_no_gold["ok"] == false, "Fusion rejected: gold 500 < 1000")
|
|
_assert(res_no_gold["reason"] == "NOT_ENOUGH_GOLD", "Reason is NOT_ENOUGH_GOLD")
|
|
|
|
# 2. Stones not enough (25 < 30)
|
|
player_gold["gold"] = 5000
|
|
var res_no_stone = sys.fuse_energy_crystal(inv, player_gold)
|
|
_assert(res_no_stone["ok"] == false, "Fusion rejected: stones 25 < 30")
|
|
_assert(res_no_stone["reason"] == "NOT_ENOUGH_STONES", "Reason is NOT_ENOUGH_STONES")
|
|
|
|
# 3. Valid fusion with 35 stones
|
|
inv[0]["count"] = 35
|
|
var fuse_sig := {"fired": false}
|
|
sys.crystal_fused.connect(func(_c: Dictionary):
|
|
fuse_sig["fired"] = true
|
|
)
|
|
|
|
var res_succ = sys.fuse_energy_crystal(inv, player_gold)
|
|
_assert(res_succ["ok"] == true, "Fusion succeeded")
|
|
_assert(player_gold["gold"] == 4000, "1,000 gold deducted (5000 -> 4000)")
|
|
_assert(inv[0]["count"] == 5, "Stones deducted (35 - 30 = 5 remaining)")
|
|
_assert(inv[1] != null and inv[1]["vnum"] == EnergySystem.VNUM_CHARGING_STONE, "Energy Crystal generated in slot 1")
|
|
_assert(fuse_sig["fired"] == true, "crystal_fused signal emitted")
|
|
|
|
func _test_crystal_usage_and_buff_lifecycle() -> void:
|
|
print("\n--- Test 3: Crystal Usage, 10% Attribute Buff & 2-Hour Lifecycle ---")
|
|
var sys := EnergySystem.new()
|
|
var inv: Array = [
|
|
{"vnum": EnergySystem.VNUM_CHARGING_STONE, "name": "能量水晶", "count": 1}
|
|
]
|
|
var equipped: Array = [
|
|
{"attack_power": 200, "defense": 0, "max_hp": 0},
|
|
{"attack_power": 0, "defense": 150, "max_hp": 0},
|
|
{"attack_power": 0, "defense": 0, "max_hp": 1000}
|
|
]
|
|
var player_stats := {"attack_power": 500, "defense": 300, "max_hp": 5000}
|
|
|
|
var act_sig := {"fired": false, "dur": 0.0, "pct": 0}
|
|
sys.energy_activated.connect(func(d: float, p: int):
|
|
act_sig["fired"] = true
|
|
act_sig["dur"] = d
|
|
act_sig["pct"] = p
|
|
)
|
|
|
|
var exp_sig := {"fired": false}
|
|
sys.energy_expired.connect(func():
|
|
exp_sig["fired"] = true
|
|
)
|
|
|
|
# Use crystal
|
|
var use_res = sys.use_energy_crystal(inv, 0, equipped, player_stats)
|
|
_assert(use_res["ok"] == true, "Crystal activated")
|
|
_assert(sys.is_energy_active == true, "is_energy_active is true")
|
|
_assert(inv[0] == null, "Energy crystal consumed from inventory")
|
|
_assert(act_sig["fired"] == true, "energy_activated signal emitted")
|
|
_assert(act_sig["dur"] == 7200.0, "Duration is 7200.0s (2 hours)")
|
|
|
|
# Check 10% stats:
|
|
# atk: 500 + 20 = 520
|
|
# def: 300 + 15 = 315
|
|
# hp: 5000 + 100 = 5100
|
|
_assert(player_stats["attack_power"] == 520, "Attack boosted by 10% of 200 = +20")
|
|
_assert(player_stats["defense"] == 315, "Defense boosted by 10% of 150 = +15")
|
|
_assert(player_stats["max_hp"] == 5100, "HP boosted by 10% of 1000 = +100")
|
|
|
|
# Advance time 7200s -> timeout
|
|
sys.update(7200.0, player_stats)
|
|
_assert(sys.is_energy_active == false, "Energy expired after 2 hours")
|
|
_assert(player_stats["attack_power"] == 500, "Attack cleanly reversed back to 500")
|
|
_assert(player_stats["defense"] == 300, "Defense cleanly reversed back to 300")
|
|
_assert(player_stats["max_hp"] == 5000, "HP cleanly reversed back to 5000")
|
|
_assert(exp_sig["fired"] == true, "energy_expired signal emitted")
|
|
|
|
func _test_serialization() -> void:
|
|
print("\n--- Test 4: Serialization & Restoration ---")
|
|
var sys1 := EnergySystem.new()
|
|
sys1.is_energy_active = true
|
|
sys1.energy_time_remaining = 3600.0
|
|
sys1.applied_bonuses = {"attack_power": 25, "defense": 18}
|
|
|
|
var data = sys1.serialize()
|
|
var sys2 := EnergySystem.new()
|
|
sys2.deserialize(data)
|
|
|
|
_assert(sys2.is_energy_active == true, "Restored is_energy_active")
|
|
_assert(is_equal_approx(sys2.energy_time_remaining, 3600.0), "Restored time remaining")
|
|
_assert(sys2.applied_bonuses["attack_power"] == 25, "Restored attack bonus")
|
|
|
|
func _finish() -> void:
|
|
print("\n=======================================================")
|
|
print("Energy System Parity Tests: %d Passed, %d Failed" % [_passed, _failed])
|
|
print("=======================================================\n")
|
|
if _failed > 0:
|
|
quit(1)
|
|
else:
|
|
quit(0)
|