- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
215 lines
8.5 KiB
GDScript
215 lines
8.5 KiB
GDScript
# test_mysterious_merchant_blackmarket_parity.gd —— 40250 野外神秘游商与黑市折扣系统 1:1 回归测试套件
|
|
extends SceneTree
|
|
|
|
const MysteriousMerchantBlackmarketSystem = preload("res://mysterious_merchant_blackmarket_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_mysterious_merchant_blackmarket_parity (Direction 4: Mysterious Merchant Blackmarket 1:1) ===")
|
|
_test_merchant_spawn_and_lifecycle()
|
|
_test_blackmarket_goods_and_discount_purchase()
|
|
_test_high_price_buyback_and_antiflag()
|
|
_test_serialization_and_persistence()
|
|
_finish()
|
|
|
|
func _test_merchant_spawn_and_lifecycle() -> void:
|
|
print("\n--- Test 1: Merchant Spawn & Lifecycle ---")
|
|
var sys := MysteriousMerchantBlackmarketSystem.new()
|
|
_assert(not sys.is_active, "Initially merchant is not active")
|
|
|
|
var spawn_sig := {"fired": false, "map": "", "pos": Vector2.ZERO, "dur": 0.0}
|
|
sys.merchant_appeared.connect(func(m: String, p: Vector2, d: float):
|
|
spawn_sig["fired"] = true
|
|
spawn_sig["map"] = m
|
|
spawn_sig["pos"] = p
|
|
spawn_sig["dur"] = d
|
|
)
|
|
|
|
var depart_sig := {"fired": false}
|
|
sys.merchant_departed.connect(func():
|
|
depart_sig["fired"] = true
|
|
)
|
|
|
|
# Spawn at fixed location index 0 (Desert)
|
|
var spawn_res = sys.spawn_random_merchant(1800.0, 0)
|
|
_assert(spawn_res["ok"] == true, "Spawn result ok")
|
|
_assert(sys.is_active == true, "System active flag is true")
|
|
_assert(sys.current_map == "Desert", "Spawned at Desert")
|
|
_assert(sys.current_pos == Vector2(120, 350), "Spawn pos matches Desert Oasis")
|
|
_assert(spawn_sig["fired"] == true, "merchant_appeared signal fired")
|
|
_assert(spawn_sig["dur"] == 1800.0, "Signal duration is 1800.0s")
|
|
|
|
# Update heartbeat - advance 600s
|
|
sys.update(600.0)
|
|
_assert(is_equal_approx(sys.time_remaining, 1200.0), "Time remaining decremented to 1200s")
|
|
_assert(sys.is_active == true, "Still active after 600s")
|
|
|
|
# Update heartbeat - advance remaining 1200s to timeout
|
|
sys.update(1200.0)
|
|
_assert(sys.is_active == false, "Merchant despawned after timeout")
|
|
_assert(depart_sig["fired"] == true, "merchant_departed signal fired on timeout")
|
|
_assert(sys.get_stock().is_empty(), "Stock cleared after merchant departed")
|
|
|
|
func _test_blackmarket_goods_and_discount_purchase() -> void:
|
|
print("\n--- Test 2: Black Market Goods & Discount Purchases ---")
|
|
var sys := MysteriousMerchantBlackmarketSystem.new()
|
|
sys.spawn_random_merchant(1800.0, 1) # Mount_Sohan
|
|
_assert(sys.current_map == "Mount_Sohan", "Spawned at Mount Sohan")
|
|
|
|
var stock = sys.get_stock()
|
|
_assert(stock.size() == 5, "Stock has 5 unique blackmarket items")
|
|
|
|
# Check item 0 (Cor Draconis): base 2M, blackmarket price 1M (50% off)
|
|
_assert(stock[0]["vnum"] == 51501, "First item is Cor Draconis (51501)")
|
|
_assert(stock[0]["price"] == 1000000, "Half-price discount 1,000,000 gold")
|
|
_assert(stock[0]["base_price"] == 2000000, "Base price 2,000,000 gold")
|
|
_assert(stock[0]["stock"] == 5, "Initial stock is 5")
|
|
|
|
var player := {"gold": 500000} # Only 500k, not enough for 1M
|
|
var inv: Array = [null, null, null]
|
|
|
|
# Attempt buy with insufficient gold
|
|
var buy_fail = sys.buy_item(0, player, inv)
|
|
_assert(buy_fail["ok"] == false, "Buy failed with insufficient gold")
|
|
_assert(buy_fail["reason"] == "NOT_ENOUGH_GOLD", "Failure reason is NOT_ENOUGH_GOLD")
|
|
|
|
# Give enough gold
|
|
player["gold"] = 3000000
|
|
var bought_sig := {"fired": false, "name": "", "cost": 0}
|
|
sys.item_bought.connect(func(n: String, c: int):
|
|
bought_sig["fired"] = true
|
|
bought_sig["name"] = n
|
|
bought_sig["cost"] = c
|
|
)
|
|
|
|
var buy_succ = sys.buy_item(0, player, inv)
|
|
_assert(buy_succ["ok"] == true, "Buy succeeded with sufficient gold")
|
|
_assert(buy_succ["cost"] == 1000000, "Cost is 1,000,000 gold")
|
|
_assert(player["gold"] == 2000000, "Player gold deducted properly to 2,000,000")
|
|
_assert(inv[0] != null and inv[0]["vnum"] == 51501, "Cor Draconis placed into slot 0")
|
|
_assert(bought_sig["fired"] == true, "item_bought signal fired")
|
|
_assert(bought_sig["cost"] == 1000000, "Signal reported correct cost")
|
|
|
|
# Check remaining stock
|
|
var updated_stock = sys.get_stock()
|
|
_assert(updated_stock[0]["stock"] == 4, "Stock decremented from 5 to 4")
|
|
|
|
# Test inventory full
|
|
inv[1] = {"vnum": 1, "name": "Item 1"}
|
|
inv[2] = {"vnum": 2, "name": "Item 2"}
|
|
var buy_full = sys.buy_item(0, player, inv)
|
|
_assert(buy_full["ok"] == false, "Buy failed when inventory full")
|
|
_assert(buy_full["reason"] == "INVENTORY_FULL", "Failure reason is INVENTORY_FULL")
|
|
|
|
# Test out of stock by depleting item 4 (Legendary Elixir, stock=2)
|
|
var big_inv: Array = [null, null, null, null, null]
|
|
player["gold"] = 10000000
|
|
var b1 = sys.buy_item(4, player, big_inv)
|
|
_assert(b1["ok"] == true, "Bought 1st Legendary Elixir")
|
|
var b2 = sys.buy_item(4, player, big_inv)
|
|
_assert(b2["ok"] == true, "Bought 2nd Legendary Elixir")
|
|
var b3 = sys.buy_item(4, player, big_inv)
|
|
_assert(b3["ok"] == false, "3rd buy rejected")
|
|
_assert(b3["reason"] == "OUT_OF_STOCK", "Reason is OUT_OF_STOCK")
|
|
|
|
func _test_high_price_buyback_and_antiflag() -> void:
|
|
print("\n--- Test 3: High-Price Buyback (+30% Markup) & Anti-Flags ---")
|
|
var sys := MysteriousMerchantBlackmarketSystem.new()
|
|
sys.spawn_random_merchant(1800.0, 2) # Doyyumhwan
|
|
|
|
var player := {"gold": 100000}
|
|
# Base price 10,000. Standard sell (1/5) = 2,000.
|
|
# High-price buyback (+30%) = 2,000 * 1.3 = 2,600.
|
|
var test_rare_item := {
|
|
"vnum": 70005,
|
|
"name": "极品黑铁矿",
|
|
"count": 2,
|
|
"shop_buy_price": 10000,
|
|
"anti_flags": 0
|
|
}
|
|
|
|
# For count=2: unit=2600 * 2 = 5200.
|
|
var calculated_price = sys.calculate_sell_price(test_rare_item)
|
|
_assert(calculated_price == 5200, "High-price buyback calculated 5200 gold (2600 * 2)")
|
|
|
|
var inv: Array = [test_rare_item, null]
|
|
|
|
var sold_sig := {"fired": false, "name": "", "earned": 0}
|
|
sys.item_sold.connect(func(n: String, e: int):
|
|
sold_sig["fired"] = true
|
|
sold_sig["name"] = n
|
|
sold_sig["earned"] = e
|
|
)
|
|
|
|
var sell_res = sys.sell_item(0, player, inv)
|
|
_assert(sell_res["ok"] == true, "Sold rare item to wandering merchant")
|
|
_assert(sell_res["earned"] == 5200, "Earned 5200 gold")
|
|
_assert(player["gold"] == 105200, "Player gold increased to 105,200")
|
|
_assert(inv[0] == null, "Inventory slot 0 cleared")
|
|
_assert(sold_sig["fired"] == true, "item_sold signal fired")
|
|
_assert(sold_sig["earned"] == 5200, "Signal reported earned gold")
|
|
|
|
# Test unsellable item with ITEM_ANTIFLAG_SELL (256)
|
|
var unsellable_item := {
|
|
"vnum": 90001,
|
|
"name": "绑定专属神兵",
|
|
"count": 1,
|
|
"shop_buy_price": 500000,
|
|
"anti_flags": 256
|
|
}
|
|
inv[0] = unsellable_item
|
|
var sell_unsellable = sys.sell_item(0, player, inv)
|
|
_assert(sell_unsellable["ok"] == false, "Unsellable item rejected")
|
|
_assert(sell_unsellable["reason"] == "ITEM_ANTIFLAG_SELL", "Reason is ITEM_ANTIFLAG_SELL")
|
|
_assert(inv[0] != null, "Unsellable item not removed from inventory")
|
|
|
|
# Test merchant inactive
|
|
sys.despawn_merchant()
|
|
var sell_inactive = sys.sell_item(0, player, inv)
|
|
_assert(sell_inactive["ok"] == false, "Sell rejected when merchant inactive")
|
|
_assert(sell_inactive["reason"] == "MERCHANT_NOT_ACTIVE", "Reason is MERCHANT_NOT_ACTIVE")
|
|
|
|
func _test_serialization_and_persistence() -> void:
|
|
print("\n--- Test 4: Serialization & Save/Load Parity ---")
|
|
var sys1 := MysteriousMerchantBlackmarketSystem.new()
|
|
sys1.spawn_merchant("Desert", Vector2(120, 350), 1500.0)
|
|
sys1.update(300.0) # 1200s left
|
|
|
|
# Buy 1 Cor Draconis so stock changes
|
|
var player := {"gold": 5000000}
|
|
var inv: Array = [null]
|
|
sys1.buy_item(0, player, inv)
|
|
_assert(sys1.get_stock()[0]["stock"] == 4, "Sys1 stock of item 0 is 4")
|
|
|
|
var save_data = sys1.serialize()
|
|
_assert(save_data["is_active"] == true, "Serialized is_active true")
|
|
_assert(save_data["current_map"] == "Desert", "Serialized map matches")
|
|
_assert(save_data["stock"][0]["stock"] == 4, "Serialized stock of item 0 is 4")
|
|
|
|
var sys2 := MysteriousMerchantBlackmarketSystem.new()
|
|
sys2.deserialize(save_data)
|
|
_assert(sys2.is_active == true, "Sys2 restored is_active")
|
|
_assert(sys2.current_map == "Desert", "Sys2 restored current_map")
|
|
_assert(sys2.current_pos == Vector2(120, 350), "Sys2 restored current_pos")
|
|
_assert(is_equal_approx(sys2.time_remaining, 1200.0), "Sys2 restored time_remaining")
|
|
_assert(sys2.get_stock()[0]["stock"] == 4, "Sys2 restored stock count 4")
|
|
|
|
func _finish() -> void:
|
|
print("\n=======================================================")
|
|
print("Mysterious Merchant & Blackmarket Parity Tests: %d Passed, %d Failed" % [_passed, _failed])
|
|
print("=======================================================\n")
|
|
if _failed > 0:
|
|
quit(1)
|
|
else:
|
|
quit(0)
|