- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
128 lines
5.1 KiB
GDScript
128 lines
5.1 KiB
GDScript
# test_npc_shop_parity.gd —— 验证方向 1:NPC 商店库存、金币购买与 1/5 折价出售 40250 1:1 对齐
|
|
extends SceneTree
|
|
|
|
const NpcShopSystem = preload("res://npc_shop_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_npc_shop_parity (Direction 1: NPC Shop Buy/Sell 1:1) ===")
|
|
test_npc_shop_stocks()
|
|
test_sell_price_calculation()
|
|
test_buy_flow_and_gold_deduction()
|
|
test_buy_rejections()
|
|
test_sell_flow_and_antiflag()
|
|
|
|
print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed])
|
|
if _failed > 0:
|
|
printerr("TEST FAILED!")
|
|
quit(1)
|
|
else:
|
|
print("ALL TESTS PASSED!")
|
|
quit(0)
|
|
|
|
func test_npc_shop_stocks() -> void:
|
|
print("\n--- Test 1: NPC Shop Stock Configurations ---")
|
|
_assert(NpcShopSystem.has_shop(9003), "General Store (9003) has shop registered")
|
|
_assert(NpcShopSystem.has_shop(9001), "Weapon Shop (9001) has shop registered")
|
|
_assert(NpcShopSystem.has_shop(9002), "Armor Shop (9002) has shop registered")
|
|
_assert(not NpcShopSystem.has_shop(101), "Wild dog (101) does not have a shop")
|
|
|
|
var general_items := NpcShopSystem.get_shop_items(9003)
|
|
_assert(general_items.size() >= 10, "General Store has at least 10 basic stock items")
|
|
_assert(general_items[0].vnum == 27001, "Slot 0 is Red Potion S (27001)")
|
|
_assert(general_items[6].vnum == 22001, "Slot 6 is Town Scroll (22001)")
|
|
_assert(general_items[9].vnum == 27400, "Slot 9 is Fishing Rod (27400)")
|
|
|
|
func test_sell_price_calculation() -> void:
|
|
print("\n--- Test 2: 40250 Official 1/5 Sell Price Calculation (shop.cpp:520) ---")
|
|
# 1. Normal item with buy_price = 1000 -> sell_price = 1000 / 5 = 200
|
|
var normal_item := {"shop_buy_price": 1000, "anti_flags": 0}
|
|
var sell_val := NpcShopSystem.calculate_sell_price(normal_item, 1)
|
|
_assert(sell_val == 200, "1000 gold item sells for 200 gold (1/5 buy price)")
|
|
|
|
# 2. Count = 5 -> sell_price = 200 * 5 = 1000
|
|
var sell_val_5 := NpcShopSystem.calculate_sell_price(normal_item, 5)
|
|
_assert(sell_val_5 == 1000, "5 count sells for 1000 gold")
|
|
|
|
# 3. Item with ITEM_ANTIFLAG_SELL cannot be sold
|
|
var unsellable_item := {"shop_buy_price": 50000, "anti_flags": NpcShopSystem.ITEM_ANTIFLAG_SELL}
|
|
var sell_unsellable := NpcShopSystem.calculate_sell_price(unsellable_item, 1)
|
|
_assert(sell_unsellable == 0, "ITEM_ANTIFLAG_SELL item sells for 0 (forbidden)")
|
|
|
|
func test_buy_flow_and_gold_deduction() -> void:
|
|
print("\n--- Test 3: Buying Flow and Gold Deduction ---")
|
|
var player_ctx := {
|
|
"gold": 50000,
|
|
"inventory": []
|
|
}
|
|
|
|
# Buy 1 pack of Red Potion S from General Store (pos 0: 200 count, 1200 gold)
|
|
var buy_res := NpcShopSystem.buy_item(0, 1, 9003, player_ctx)
|
|
_assert(buy_res.ok and buy_res.code == "SUCCESS", "Buy succeeded")
|
|
_assert(buy_res.cost == 1200, "Cost is 1200 gold")
|
|
_assert(player_ctx.gold == 50000 - 1200, "Player gold deducted to 48800")
|
|
_assert(player_ctx.inventory.size() == 1, "Item added to player inventory")
|
|
_assert(player_ctx.inventory[0].vnum == 27001, "Added item is 27001")
|
|
_assert(player_ctx.inventory[0].count == 200, "Added item has 200 count")
|
|
|
|
func test_buy_rejections() -> void:
|
|
print("\n--- Test 4: Buy Rejections (Insufficient Gold / Inventory Full) ---")
|
|
# Insufficient Gold
|
|
var poor_player := {
|
|
"gold": 500, # Needs 1200
|
|
"inventory": []
|
|
}
|
|
var res_poor := NpcShopSystem.buy_item(0, 1, 9003, poor_player)
|
|
_assert(not res_poor.ok and res_poor.code == "NOT_ENOUGH_GOLD", "Rejected due to insufficient gold")
|
|
_assert(poor_player.gold == 500, "Gold untouched")
|
|
|
|
# Inventory Full
|
|
var full_player := {
|
|
"gold": 100000,
|
|
"inventory": []
|
|
}
|
|
for i in 90:
|
|
full_player.inventory.append({"cell": i, "vnum": 10, "count": 1})
|
|
var res_full := NpcShopSystem.buy_item(0, 1, 9003, full_player)
|
|
_assert(not res_full.ok and res_full.code == "INVENTORY_FULL", "Rejected due to inventory full")
|
|
|
|
func test_sell_flow_and_antiflag() -> void:
|
|
print("\n--- Test 5: Selling Flow & Anti-Flag Verification ---")
|
|
var player_ctx := {
|
|
"gold": 1000,
|
|
"inventory": [
|
|
{"cell": 0, "vnum": 10, "count": 1}, # Sword +0 (buy_price: 1000 -> sells for 200)
|
|
{"cell": 1, "vnum": 71085, "count": 2} # Scroll with anti-sell
|
|
]
|
|
}
|
|
|
|
var proto_mock := func(vnum: int) -> Dictionary:
|
|
if vnum == 10:
|
|
return {"shop_buy_price": 1000, "anti_flags": 0}
|
|
elif vnum == 71085:
|
|
return {"shop_buy_price": 50000, "anti_flags": NpcShopSystem.ITEM_ANTIFLAG_SELL}
|
|
return {}
|
|
|
|
# Sell unsellable item in cell 1
|
|
var res_unsellable := NpcShopSystem.sell_item(1, 1, player_ctx, proto_mock)
|
|
_assert(not res_unsellable.ok and res_unsellable.code == "ITEM_ANTIFLAG_SELL",
|
|
"ITEM_ANTIFLAG_SELL item sale rejected")
|
|
|
|
# Sell valid item in cell 0
|
|
var res_sell := NpcShopSystem.sell_item(0, 1, player_ctx, proto_mock)
|
|
_assert(res_sell.ok and res_sell.code == "SUCCESS", "Sale succeeded")
|
|
_assert(res_sell.gold_earned == 200, "Earned 200 gold")
|
|
_assert(player_ctx.gold == 1200, "Player gold increased to 1200")
|
|
_assert(player_ctx.inventory.size() == 1, "Sold item removed from inventory")
|
|
_assert(player_ctx.inventory[0].cell == 1, "Remaining item is cell 1")
|