- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
173 lines
6.8 KiB
GDScript
173 lines
6.8 KiB
GDScript
# test_offline_bazaar_market_parity.gd —— 40250 单人单机广场离线摆摊与虚拟交易 1:1 回归测试套件
|
|
extends SceneTree
|
|
|
|
const OfflineBazaarMarketSystem = preload("res://offline_bazaar_market_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_offline_bazaar_market_parity (Direction 1: Offline Bazaar Market 1:1) ===")
|
|
_test_npc_stalls_browse_and_buy()
|
|
_test_buy_validation_errors()
|
|
_test_player_stall_open()
|
|
_test_offline_sales_and_revenue()
|
|
_test_stall_close_and_item_return()
|
|
_finish()
|
|
|
|
func _test_npc_stalls_browse_and_buy() -> void:
|
|
print("\n--- Test 1: NPC Stalls Browsing & Purchasing ---")
|
|
var mkt := OfflineBazaarMarketSystem.new()
|
|
var stalls = mkt.get_all_stalls()
|
|
_assert(stalls.size() == 3, "Found 3 default NPC stalls in village square")
|
|
|
|
# Browse Stall 1 (Materials)
|
|
var b_res = mkt.browse_stall(1)
|
|
_assert(b_res["ok"] == true and b_res["owner"] == "阿隆", "Browsed Stall 1 (阿隆)")
|
|
_assert(b_res["items"].size() == 4, "Stall 1 has 4 goods")
|
|
|
|
# Buy Pearl (27993) from Stall 1
|
|
var player := {"gold": 2000000}
|
|
var inv: Array = []
|
|
inv.resize(10)
|
|
for i in range(10):
|
|
inv[i] = null
|
|
|
|
var buy_sig := {"fired": false, "name": "", "cost": 0}
|
|
mkt.item_bought_from_stall.connect(func(_sid: int, iname: String, _c: int, cost: int):
|
|
buy_sig["fired"] = true
|
|
buy_sig["name"] = iname
|
|
buy_sig["cost"] = cost
|
|
)
|
|
|
|
# Buy 2 pearls: 500,000 * 2 = 1,000,000 gold
|
|
var buy_res = mkt.buy_from_stall(1, 0, 2, player, inv)
|
|
_assert(buy_res["ok"] == true, "Purchased 2 pearls")
|
|
_assert(player["gold"] == 1000000, "Gold deducted 2M -> 1M")
|
|
_assert(inv[0]["vnum"] == 27993 and inv[0]["count"] == 2, "Pearls placed in slot 0")
|
|
_assert(mkt.npc_stalls[1]["items"][0]["stock"] == 3, "Stock decreased 5 -> 3")
|
|
_assert(buy_sig["fired"] == true and buy_sig["name"] == "珍珠", "item_bought_from_stall signal fired")
|
|
|
|
func _test_buy_validation_errors() -> void:
|
|
print("\n--- Test 2: Purchase Error Validations ---")
|
|
var mkt := OfflineBazaarMarketSystem.new()
|
|
var player := {"gold": 100} # Very poor
|
|
var inv: Array = [null]
|
|
|
|
# Not enough gold
|
|
var fail_gold = mkt.buy_from_stall(1, 0, 1, player, inv)
|
|
_assert(fail_gold["ok"] == false and fail_gold["reason"] == "NOT_ENOUGH_GOLD", "Rejected: not enough gold")
|
|
|
|
# Out of stock
|
|
player["gold"] = 999999999
|
|
var fail_stock = mkt.buy_from_stall(1, 0, 999, player, inv)
|
|
_assert(fail_stock["ok"] == false and fail_stock["reason"] == "OUT_OF_STOCK", "Rejected: out of stock")
|
|
|
|
# Full inventory
|
|
var full_inv := [{"vnum": 1}]
|
|
var fail_inv = mkt.buy_from_stall(1, 0, 1, player, full_inv)
|
|
_assert(fail_inv["ok"] == false and fail_inv["reason"] == "INVENTORY_FULL", "Rejected: inventory full")
|
|
|
|
func _test_player_stall_open() -> void:
|
|
print("\n--- Test 3: Opening Private Player Stall (50200 Bundle) ---")
|
|
var mkt := OfflineBazaarMarketSystem.new()
|
|
var inv: Array = [
|
|
{"vnum": OfflineBazaarMarketSystem.ITEM_BUNDLE_VNUM, "name": "摆摊包袱", "count": 1},
|
|
{"vnum": 10, "name": "巨剑+9", "count": 1},
|
|
{"vnum": 27003, "name": "红色药水(特大)", "count": 100}
|
|
]
|
|
|
|
var open_sig := {"fired": false, "title": "", "count": 0}
|
|
mkt.player_stall_opened.connect(func(t: String, c: int):
|
|
open_sig["fired"] = true
|
|
open_sig["title"] = t
|
|
open_sig["count"] = c
|
|
)
|
|
|
|
# Try open without bundle
|
|
var no_bundle_inv: Array = [{"vnum": 10, "count": 1}]
|
|
var fail_bundle = mkt.open_player_stall("我的摊位", [{"slot": 0, "price": 1000}], no_bundle_inv, true)
|
|
_assert(fail_bundle["ok"] == false and fail_bundle["reason"] == "NEED_BUNDLE_ITEM", "Rejected without bundle")
|
|
|
|
# Open stall with bundle and 2 items
|
|
var items_to_list := [
|
|
{"slot": 1, "price": 500000, "count": 1},
|
|
{"slot": 2, "price": 200, "count": 50} # sell 50 out of 100
|
|
]
|
|
|
|
var res = mkt.open_player_stall("【东方极速特惠商铺】", items_to_list, inv, true)
|
|
_assert(res["ok"] == true, "Player stall opened")
|
|
_assert(inv[0] == null, "Bundle item (50200) consumed")
|
|
_assert(inv[1] == null, "Listed Sword removed from inventory slot 1")
|
|
_assert(inv[2]["count"] == 50, "Potion count halved in inventory (100 - 50 = 50)")
|
|
_assert(mkt.player_stall["is_open"] == true, "Stall is open")
|
|
_assert(mkt.player_stall["items"].size() == 2, "2 goods listed in stall")
|
|
_assert(open_sig["fired"] == true and open_sig["count"] == 2, "player_stall_opened signal fired")
|
|
|
|
func _test_offline_sales_and_revenue() -> void:
|
|
print("\n--- Test 4: Simulated Offline Sales & Revenue Collection ---")
|
|
var mkt := OfflineBazaarMarketSystem.new()
|
|
var inv: Array = [
|
|
{"vnum": 50200, "count": 1},
|
|
{"vnum": 28430, "name": "战士灵石+4", "count": 1}
|
|
]
|
|
|
|
mkt.open_player_stall("【灵石专卖】", [{"slot": 1, "price": 1000000, "count": 1}], inv, true)
|
|
_assert(mkt.player_stall["items"].size() == 1, "1 item listed")
|
|
|
|
# Simulate 30 minutes of offline time
|
|
var sim = mkt.simulate_offline_sales(30.0)
|
|
_assert(sim["sold_count"] == 1, "Stone sold during simulated offline time")
|
|
_assert(mkt.player_stall["revenue"] == 1000000, "1M gold earned in stall revenue")
|
|
_assert(mkt.player_stall["items"].is_empty(), "Stall inventory now empty")
|
|
|
|
# Collect revenue
|
|
var player := {"gold": 50000}
|
|
var col_res = mkt.collect_stall_revenue(player)
|
|
_assert(col_res["ok"] == true and player["gold"] == 1050000, "Collected revenue into player wallet")
|
|
_assert(mkt.player_stall["revenue"] == 0, "Stall revenue reset to 0")
|
|
|
|
func _test_stall_close_and_item_return() -> void:
|
|
print("\n--- Test 5: Closing Stall & Returning Unsold Items ---")
|
|
var mkt := OfflineBazaarMarketSystem.new()
|
|
var inv: Array = [
|
|
{"vnum": 50200, "count": 1},
|
|
{"vnum": 11299, "name": "黑铁战甲+9", "count": 1}
|
|
]
|
|
|
|
mkt.open_player_stall("【神装铺】", [{"slot": 1, "price": 10000000, "count": 1}], inv, true)
|
|
|
|
var close_sig := {"fired": false, "rev": -1}
|
|
mkt.player_stall_closed.connect(func(rev: int):
|
|
close_sig["fired"] = true
|
|
close_sig["rev"] = rev
|
|
)
|
|
|
|
# Close stall immediately without sales
|
|
var player := {"gold": 0}
|
|
var close_res = mkt.close_player_stall(inv, player)
|
|
_assert(close_res["ok"] == true, "Stall closed successfully")
|
|
_assert(mkt.player_stall["is_open"] == false, "Stall is no longer open")
|
|
_assert(close_sig["fired"] == true and close_sig["rev"] == 0, "player_stall_closed signal fired")
|
|
_assert(inv[0]["vnum"] == 11299, "Unsold armor safely returned to first empty inventory slot")
|
|
|
|
func _finish() -> void:
|
|
print("\n==========================================")
|
|
print("Offline Bazaar Market 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 Offline Bazaar Market parity tests passed!")
|
|
quit(0)
|