- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
224 lines
7.8 KiB
GDScript
224 lines
7.8 KiB
GDScript
# test_player_trade_exchange_parity.gd —— 40250 面对面交易与防调包双向锁定系统回归测试
|
|
extends SceneTree
|
|
|
|
const SystemClass = preload("res://player_trade_exchange_system.gd")
|
|
|
|
var _passed: int = 0
|
|
var _failed: int = 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_player_trade_exchange_parity (Batch 26 Direction 3) ===")
|
|
_test_trade_session_initialization()
|
|
_test_untradeable_items_rejection()
|
|
_test_anti_swap_lock_reset_security()
|
|
_test_gold_offer_validation()
|
|
_test_successful_atomic_exchange()
|
|
_test_insufficient_space_rejection()
|
|
_test_cancel_trade()
|
|
|
|
print("\n=== Result: %d Passed, %d Failed ===" % [_passed, _failed])
|
|
if _failed == 0:
|
|
print("ALL TESTS PASSED!\n")
|
|
else:
|
|
printerr("SOME TESTS FAILED!\n")
|
|
quit(0 if _failed == 0 else 1)
|
|
|
|
func _create_player(id: int, name: String, gold: int, inv_size: int = 10) -> Dictionary:
|
|
var inv: Array = []
|
|
for i in range(inv_size):
|
|
inv.append(null)
|
|
return {
|
|
"id": id,
|
|
"name": name,
|
|
"gold": gold,
|
|
"inv": inv
|
|
}
|
|
|
|
func _test_trade_session_initialization() -> void:
|
|
print("\n--- Test 1: Trade Session Initialization & Distance ---")
|
|
var trade = SystemClass.new()
|
|
var p1 = _create_player(101, "Alice", 500000)
|
|
var p2 = _create_player(102, "Bob", 300000)
|
|
|
|
# 1. Reject distance > 1000
|
|
var r_dist = trade.start_exchange(p1, p2, 1200.0)
|
|
_assert(r_dist["error_code"] == "TOO_FAR", "Reject trade if distance > 1000")
|
|
|
|
# 2. Reject self trade
|
|
var r_self = trade.start_exchange(p1, p1, 100.0)
|
|
_assert(r_self["error_code"] == "CANNOT_TRADE_SELF", "Reject trade with self")
|
|
|
|
# 3. Successful start
|
|
var r_ok = trade.start_exchange(p1, p2, 100.0)
|
|
_assert(r_ok["success"] == true, "Trade session opened successfully")
|
|
_assert(trade.is_active == true, "Trade is active")
|
|
|
|
func _test_untradeable_items_rejection() -> void:
|
|
print("\n--- Test 2: Untradeable Items Rejection ---")
|
|
var trade = SystemClass.new()
|
|
var p1 = _create_player(101, "Alice", 500000)
|
|
var p2 = _create_player(102, "Bob", 300000)
|
|
|
|
p1["inv"][0] = { "vnum": 1, "name": "Normal Item", "anti_give": false, "is_soulbound": false }
|
|
p1["inv"][1] = { "vnum": 2, "name": "Bound Item", "anti_give": false, "is_soulbound": true }
|
|
p1["inv"][2] = { "vnum": 3, "name": "Anti-Give Item", "anti_give": true, "is_soulbound": false }
|
|
p1["inv"][3] = { "vnum": 4, "name": "Equipped Item", "is_equipped": true }
|
|
|
|
trade.start_exchange(p1, p2)
|
|
|
|
# Soulbound rejected
|
|
var r_bound = trade.add_item(101, 1, 0)
|
|
_assert(r_bound["error_code"] == "ITEM_UNTRADEABLE", "Soulbound item rejected from trade")
|
|
|
|
# Anti-give rejected
|
|
var r_antigive = trade.add_item(101, 2, 0)
|
|
_assert(r_antigive["error_code"] == "ITEM_UNTRADEABLE", "Anti-give flag item rejected from trade")
|
|
|
|
# Equipped rejected
|
|
var r_equip = trade.add_item(101, 3, 0)
|
|
_assert(r_equip["error_code"] == "ITEM_UNTRADEABLE", "Equipped item rejected from trade")
|
|
|
|
# Normal item accepted
|
|
var r_norm = trade.add_item(101, 0, 0)
|
|
_assert(r_norm["success"] == true, "Tradeable item successfully added to slot 0")
|
|
|
|
func _test_anti_swap_lock_reset_security() -> void:
|
|
print("\n--- Test 3: 40250 Anti-Swap Security (Lock Reset on Modification) ---")
|
|
var trade = SystemClass.new()
|
|
var p1 = _create_player(101, "Alice", 500000)
|
|
var p2 = _create_player(102, "Bob", 300000)
|
|
p1["inv"][0] = { "vnum": 10, "name": "Sword" }
|
|
p2["inv"][0] = { "vnum": 20, "name": "Shield" }
|
|
p2["inv"][1] = { "vnum": 30, "name": "Dagger" }
|
|
|
|
trade.start_exchange(p1, p2)
|
|
trade.add_item(101, 0, 0)
|
|
trade.add_item(102, 0, 0)
|
|
|
|
# Alice locks ready
|
|
trade.set_ready(101, true)
|
|
_assert(trade.side_a.is_ready == true, "Alice locked ready")
|
|
|
|
# Bob attempts to add another item (or swap item)
|
|
trade.add_item(102, 1, 1)
|
|
_assert(trade.side_a.is_ready == false, "Alice's ready lock instantly revoked on Bob item add")
|
|
_assert(trade.side_b.is_ready == false, "Bob's ready lock also reset")
|
|
|
|
# Alice locks ready again
|
|
trade.set_ready(101, true)
|
|
_assert(trade.side_a.is_ready == true, "Alice locked ready again")
|
|
|
|
# Bob modifies gold
|
|
trade.set_gold(102, 10000)
|
|
_assert(trade.side_a.is_ready == false, "Alice's ready lock instantly revoked on Bob gold change")
|
|
|
|
# Alice locks ready again
|
|
trade.set_ready(101, true)
|
|
|
|
# Bob removes item
|
|
trade.remove_item(102, 1)
|
|
_assert(trade.side_a.is_ready == false, "Alice's ready lock instantly revoked on Bob item remove")
|
|
|
|
func _test_gold_offer_validation() -> void:
|
|
print("\n--- Test 4: Gold Offer Validation ---")
|
|
var trade = SystemClass.new()
|
|
var p1 = _create_player(101, "Alice", 100000)
|
|
var p2 = _create_player(102, "Bob", 50000)
|
|
|
|
trade.start_exchange(p1, p2)
|
|
|
|
# Reject offer exceeding gold
|
|
var r_fail = trade.set_gold(101, 150000)
|
|
_assert(r_fail["error_code"] == "INSUFFICIENT_GOLD", "Reject gold offer exceeding owned gold")
|
|
|
|
# Accept valid offer
|
|
var r_ok = trade.set_gold(101, 80000)
|
|
_assert(r_ok["success"] == true, "Valid gold offer accepted")
|
|
_assert(trade.side_a.offered_gold == 80000, "Alice offered 80,000 gold")
|
|
|
|
func _test_successful_atomic_exchange() -> void:
|
|
print("\n--- Test 5: Successful Atomic Trade Execution ---")
|
|
var trade = SystemClass.new()
|
|
var p1 = _create_player(101, "Alice", 500000, 5)
|
|
var p2 = _create_player(102, "Bob", 200000, 5)
|
|
|
|
p1["inv"][0] = { "vnum": 101, "name": "Dragon Sword+9" }
|
|
p2["inv"][0] = { "vnum": 202, "name": "Black Steel Armor+9" }
|
|
|
|
trade.start_exchange(p1, p2)
|
|
|
|
# Alice puts Dragon Sword and 100,000 gold
|
|
trade.add_item(101, 0, 0)
|
|
trade.set_gold(101, 100000)
|
|
|
|
# Bob puts Black Steel Armor and 40,000 gold
|
|
trade.add_item(102, 0, 0)
|
|
trade.set_gold(102, 40000)
|
|
|
|
# Both confirm
|
|
var r_ready_a = trade.set_ready(101, true)
|
|
_assert(r_ready_a["trade_completed"] == false, "Trade not completed after only Alice ready")
|
|
|
|
var r_ready_b = trade.set_ready(102, true)
|
|
_assert(r_ready_b["trade_completed"] == true, "Trade atomically completed when both ready")
|
|
_assert(trade.is_active == false, "Trade session closed")
|
|
|
|
# Check Gold:
|
|
# Alice: 500,000 - 100,000 + 40,000 = 440,000
|
|
_assert(p1["gold"] == 440000, "Alice gold correctly calculated (440,000)")
|
|
# Bob: 200,000 - 40,000 + 100,000 = 260,000
|
|
_assert(p2["gold"] == 260000, "Bob gold correctly calculated (260,000)")
|
|
|
|
# Check Items:
|
|
# Alice now has Black Steel Armor
|
|
var alice_has_armor := false
|
|
for it in p1["inv"]:
|
|
if it != null and it.get("vnum") == 202:
|
|
alice_has_armor = true
|
|
_assert(alice_has_armor == true, "Alice received Bob's Black Steel Armor")
|
|
|
|
# Bob now has Dragon Sword
|
|
var bob_has_sword := false
|
|
for it in p2["inv"]:
|
|
if it != null and it.get("vnum") == 101:
|
|
bob_has_sword = true
|
|
_assert(bob_has_sword == true, "Bob received Alice's Dragon Sword")
|
|
|
|
func _test_insufficient_space_rejection() -> void:
|
|
print("\n--- Test 6: CheckSpace Rejection (Inventory Full) ---")
|
|
var trade = SystemClass.new()
|
|
# Bob's inventory has only 1 slot, and it is full with an item he is NOT trading
|
|
var p1 = _create_player(101, "Alice", 500000, 5)
|
|
var p2 = _create_player(102, "Bob", 200000, 1)
|
|
|
|
p1["inv"][0] = { "vnum": 101, "name": "Item A" }
|
|
p2["inv"][0] = { "vnum": 999, "name": "Untraded Item (Full)" } # Not trading this
|
|
|
|
trade.start_exchange(p1, p2)
|
|
trade.add_item(101, 0, 0) # Alice offers Item A
|
|
|
|
trade.set_ready(101, true)
|
|
var res = trade.set_ready(102, true)
|
|
_assert(res["trade_completed"] == false, "Trade aborted due to space check")
|
|
_assert(res["error_code"] == "PLAYER_B_INSUFFICIENT_SPACE", "Detected Bob has insufficient inventory space")
|
|
|
|
func _test_cancel_trade() -> void:
|
|
print("\n--- Test 7: Trade Cancellation ---")
|
|
var trade = SystemClass.new()
|
|
var p1 = _create_player(101, "Alice", 500000, 5)
|
|
var p2 = _create_player(102, "Bob", 200000, 5)
|
|
|
|
trade.start_exchange(p1, p2)
|
|
_assert(trade.is_active == true, "Trade started")
|
|
|
|
trade.cancel_trade()
|
|
_assert(trade.is_active == false, "Trade canceled and inactive")
|