- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
139 lines
5.9 KiB
GDScript
139 lines
5.9 KiB
GDScript
# test_soulbound_lock_security_parity.gd —— 40250 灵魂绑定与防丢安全锁 1:1 回归测试套件
|
|
extends SceneTree
|
|
|
|
const SoulboundLockSecuritySystem = preload("res://soulbound_lock_security_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_soulbound_lock_security_parity (Direction 3: Soulbound Security 1:1) ===")
|
|
_test_security_predicates()
|
|
_test_soul_binding_flow()
|
|
_test_unbinding_and_cancellation()
|
|
_test_unbinding_completion()
|
|
_finish()
|
|
|
|
func _test_security_predicates() -> void:
|
|
print("\n--- Test 1: Static Security Predicates (Death Drop, Sell, Stall) ---")
|
|
var normal_item := {"vnum": 10, "is_soulbound": false}
|
|
var bound_item := {"vnum": 189, "is_soulbound": true}
|
|
|
|
# Normal item can drop, sell, stall
|
|
_assert(SoulboundLockSecuritySystem.can_drop_on_death(normal_item) == true, "Normal item drops on red name death")
|
|
_assert(SoulboundLockSecuritySystem.can_sell_to_shop(normal_item) == true, "Normal item can be sold to shop")
|
|
_assert(SoulboundLockSecuritySystem.can_drop_to_ground(normal_item) == true, "Normal item can be dropped to ground")
|
|
_assert(SoulboundLockSecuritySystem.can_trade_or_stall(normal_item) == true, "Normal item can be traded or stalled")
|
|
|
|
# Soulbound item is 100% protected
|
|
_assert(SoulboundLockSecuritySystem.can_drop_on_death(bound_item) == false, "Soulbound item IMMUNE to death drop")
|
|
_assert(SoulboundLockSecuritySystem.can_sell_to_shop(bound_item) == false, "Soulbound item CANNOT be sold to shop")
|
|
_assert(SoulboundLockSecuritySystem.can_drop_to_ground(bound_item) == false, "Soulbound item CANNOT be dropped to ground")
|
|
_assert(SoulboundLockSecuritySystem.can_trade_or_stall(bound_item) == false, "Soulbound item CANNOT be put on bazaar stalls")
|
|
|
|
func _test_soul_binding_flow() -> void:
|
|
print("\n--- Test 2: Applying Soul Binding (72051) ---")
|
|
var sys := SoulboundLockSecuritySystem.new()
|
|
var inv: Array = [
|
|
{"vnum": SoulboundLockSecuritySystem.VNUM_SOULBIND_SCROLL, "name": "灵魂绑定卷轴", "count": 2},
|
|
{"vnum": 189, "name": "毒剑+9", "is_soulbound": false}
|
|
]
|
|
|
|
var bind_sig := {"fired": false, "slot": -1, "name": ""}
|
|
sys.item_soulbound.connect(func(s: int, iname: String):
|
|
bind_sig["fired"] = true
|
|
bind_sig["slot"] = s
|
|
bind_sig["name"] = iname
|
|
)
|
|
|
|
var res = sys.bind_item(0, 1, inv)
|
|
_assert(res["ok"] == true, "Item soulbound")
|
|
_assert(inv[1]["is_soulbound"] == true, "is_soulbound is true")
|
|
_assert(inv[0]["count"] == 1, "Scroll count decreased (2 -> 1)")
|
|
_assert(bind_sig["fired"] == true and bind_sig["slot"] == 1 and bind_sig["name"] == "毒剑+9", "item_soulbound emitted")
|
|
|
|
# Duplicate binding rejected
|
|
var fail_dup = sys.bind_item(0, 1, inv)
|
|
_assert(fail_dup["ok"] == false and fail_dup["reason"] == "ALREADY_SOULBOUND", "Duplicate binding rejected")
|
|
_assert(inv[0]["count"] == 1, "Scroll not consumed on failure")
|
|
|
|
func _test_unbinding_and_cancellation() -> void:
|
|
print("\n--- Test 3: Unbinding Initiation (72052) & Cancellation ---")
|
|
var sys := SoulboundLockSecuritySystem.new()
|
|
var inv: Array = [
|
|
{"vnum": SoulboundLockSecuritySystem.VNUM_SOULUNBIND_SCROLL, "name": "灵魂解绑卷轴", "count": 1},
|
|
{"vnum": 189, "name": "毒剑+9", "is_soulbound": true}
|
|
]
|
|
|
|
var unbind_start_sig := {"fired": false, "time": 0.0}
|
|
sys.unbinding_started.connect(func(_s: int, _n: String, t: float):
|
|
unbind_start_sig["fired"] = true
|
|
unbind_start_sig["time"] = t
|
|
)
|
|
|
|
# Start unbinding (72 hours)
|
|
var res = sys.start_unbinding(0, 1, inv, 72.0)
|
|
_assert(res["ok"] == true, "Unbinding started")
|
|
_assert(inv[0] == null, "Unbind scroll consumed")
|
|
_assert(unbind_start_sig["fired"] == true and unbind_start_sig["time"] == 72.0 * 3600.0, "unbinding_started emitted with 72h")
|
|
_assert(inv[1]["unbind_remaining_time"] == 72.0 * 3600.0, "Cooldown active")
|
|
|
|
# Cancel unbinding
|
|
var cancel_sig := {"fired": false}
|
|
sys.unbinding_cancelled.connect(func(_s: int, _n: String): cancel_sig["fired"] = true)
|
|
|
|
var cancel_res = sys.cancel_unbinding(1, inv)
|
|
_assert(cancel_res["ok"] == true, "Unbinding cancelled")
|
|
_assert(cancel_sig["fired"] == true, "unbinding_cancelled emitted")
|
|
_assert(inv[1]["unbind_remaining_time"] == 0.0, "Timer cleared")
|
|
_assert(inv[1]["is_soulbound"] == true, "Item remains safely soulbound")
|
|
|
|
func _test_unbinding_completion() -> void:
|
|
print("\n--- Test 4: Finalizing Unbinding After Cooldown ---")
|
|
var sys := SoulboundLockSecuritySystem.new()
|
|
var inv: Array = [
|
|
{"vnum": SoulboundLockSecuritySystem.VNUM_SOULUNBIND_SCROLL, "count": 1},
|
|
{"vnum": 189, "name": "毒剑+9", "is_soulbound": true}
|
|
]
|
|
|
|
# Start short 1-second unbind for testing
|
|
sys.start_unbinding(0, 1, inv, 1.0 / 3600.0) # 1 second
|
|
|
|
# Immediate finalize attempt should fail
|
|
var fail_early = sys.finalize_unbinding(1, inv)
|
|
_assert(fail_early["ok"] == false and fail_early["reason"] == "COOLDOWN_NOT_FINISHED", "Cannot finalize before timer")
|
|
|
|
# Update 2 seconds
|
|
sys.update(2.0, inv)
|
|
_assert(inv[1]["unbind_remaining_time"] == 0.0, "Timer expired")
|
|
|
|
# Finalize unbind
|
|
var unbound_sig := {"fired": false}
|
|
sys.item_unbound.connect(func(_s: int, _n: String): unbound_sig["fired"] = true)
|
|
|
|
var done_res = sys.finalize_unbinding(1, inv)
|
|
_assert(done_res["ok"] == true, "Item unbound")
|
|
_assert(unbound_sig["fired"] == true, "item_unbound emitted")
|
|
_assert(inv[1]["is_soulbound"] == false, "Item is no longer soulbound")
|
|
_assert(SoulboundLockSecuritySystem.can_drop_on_death(inv[1]) == true, "Now subject to normal drop rules")
|
|
|
|
func _finish() -> void:
|
|
print("\n==========================================")
|
|
print("Soulbound Security 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 Soulbound Security parity tests passed!")
|
|
quit(0)
|