Files
mtgodot-poc/project/test_npc_shop_parity.gd
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

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")