Files
mtgodot-poc/project/test_npc_dialog_shop_ui_parity.gd
T

230 lines
9.1 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# test_npc_dialog_shop_ui_parity.gd
# 严格按照《CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md》对齐 40250
# 验证 NPC 对话面板 (QuestDialog / thinboard / BarButton / QuestCurtain)
# 与 NPC 商店面板 (ShopUI / shopdialog.py 184x328 / board / ItemSlot 5x8 / Buy & Sell 按钮) 1:1 对拍。
extends SceneTree
const QuestDialog = preload("res://ui/quest_dialog.gd")
const ShopUI = preload("res://ui/shop_ui.gd")
const QuestCurtain = preload("res://ui/quest_curtain.gd")
class MockClient extends Node:
signal shop_opened(vid: int)
signal shop_closed
signal shop_updated(pos: int)
signal shop_error(kind: String)
signal script_dialog(skin: int, text: String)
var buy_calls: Array = []
var sell_calls: Array = []
var script_answers: Array = []
var shop_data: Dictionary = {
"tabs": [
{
"name": "杂货",
"items": [
{"pos": 0, "vnum": 27001, "count": 1, "price": 100},
{"pos": 1, "vnum": 27002, "count": 50, "price": 5000},
]
}
]
}
func is_in_game() -> bool:
return true
func get_main_vid() -> int:
return 1000
func get_entity(_vid: int) -> Dictionary:
return {"pos": Vector3(0, 0, 0)}
func get_shop() -> Dictionary:
return shop_data
func get_shop_items() -> Array:
return shop_data["tabs"][0]["items"]
func shop_buy(pos: int, count: int = 1) -> bool:
buy_calls.append({"pos": pos, "count": count})
return true
func shop_sell(cell: int, count: int = 1) -> bool:
sell_calls.append({"cell": cell, "count": count})
return true
func shop_close() -> bool:
return true
func script_answer(answer: int) -> bool:
script_answers.append(answer)
return true
func get_inventory() -> Array:
return [{"cell": 5, "vnum": 27001, "count": 10, "flags": 0, "anti_flags": 0}]
class MockProto extends Node:
func item(vnum: int) -> Dictionary:
if vnum == 27001:
return {"name": "红药水(小)", "locale_name": "红药水(小)", "sell_price": 50, "flags": 0, "anti_flags": 0}
elif vnum == 27002:
return {"name": "红药水(中)", "locale_name": "红药水(中)", "sell_price": 100, "flags": 0, "anti_flags": 0}
return {}
func _init() -> void:
call_deferred("_run")
func _run() -> void:
print("=== Starting test_npc_dialog_shop_ui_parity ===")
var fails := [0]
var check = func(cond: bool, msg: String):
if not cond:
fails[0] += 1
printerr("FAIL: ", msg)
else:
print("PASS: ", msg)
var root_node := Control.new()
root.add_child(root_node)
var client := MockClient.new()
root.add_child(client)
var proto := MockProto.new()
root.add_child(proto)
# -------------------------------------------------------------
# Test 1: ShopUI 184x328 官方尺寸、Board 外框与 TitleBar
# -------------------------------------------------------------
print("\n--- Test 1: ShopUI 184x328 & 40250 Board Parity ---")
var shop := ShopUI.new()
root.add_child(shop)
shop.setup(client, root_node, proto)
shop.open()
check.call(shop.is_open(), "Shop window opened")
var win: Control = shop._root
check.call(win != null, "Shop window root exists")
check.call(win.size == Vector2(184, 328), "Shop window size matches 40250 canonical 184x328 (got %s)" % str(win.size))
var titlebar: Control = win.find_child("TitleBar", true, false)
check.call(titlebar != null, "Official TitleBar exists")
check.call(titlebar.size.x == 169 or titlebar.custom_minimum_size.x == 169, "TitleBar width is 169")
var title_lbl: Label = titlebar.find_child("TitleName", true, false)
check.call(title_lbl != null and title_lbl.text == "商店", "TitleBar text is '商店'")
# -------------------------------------------------------------
# Test 2: ShopUI 5x8 ItemSlot (40 格) 货架与商品渲染
# -------------------------------------------------------------
print("\n--- Test 2: ShopUI 5x8 ItemSlot Grid & Item Rendering ---")
check.call(shop._grid != null, "ItemSlot container exists")
check.call(shop._grid.columns == 5, "ItemSlot grid has 5 columns")
check.call(shop._grid.get_child_count() == 40, "ItemSlot contains 40 slots (5x8)")
var slot0: Button = shop._grid.get_child(0)
check.call(slot0 != null, "Slot 0 button exists")
check.call(slot0.size == Vector2(32, 32) or slot0.custom_minimum_size == Vector2(32, 32), "Slot 0 is 32x32")
check.call(not slot0.disabled, "Slot 0 is occupied and enabled")
check.call("价格:100" in slot0.tooltip_text, "Slot 0 tooltip contains price 100")
var slot1: Button = shop._grid.get_child(1)
check.call(slot1 != null and not slot1.disabled, "Slot 1 is occupied")
var count_lbl: Label = slot1.find_child("Count", true, false)
check.call(count_lbl != null and count_lbl.text == "50", "Slot 1 stacked count label displays '50'")
var slot2: Button = shop._grid.get_child(2)
check.call(slot2 != null and slot2.disabled, "Empty slot 2 is disabled")
# -------------------------------------------------------------
# Test 3: Buy & Sell 切换按钮 (middle_button 61x21) 与购买/出售交互
# -------------------------------------------------------------
print("\n--- Test 3: Buy/Sell Buttons & Purchase/Sell Actions ---")
check.call(shop._buy_mode_button != null, "BuyButton exists")
check.call(shop._buy_mode_button.custom_minimum_size == Vector2(61, 21), "BuyButton custom_minimum_size is 61x21")
check.call(shop._sell_mode_button != null, "SellButton exists")
check.call(shop._sell_mode_button.custom_minimum_size == Vector2(61, 21), "SellButton custom_minimum_size is 61x21")
# 40250 UnselectItemSlot: 右键槽位直接购买
var right_click := InputEventMouseButton.new()
right_click.button_index = MOUSE_BUTTON_RIGHT
right_click.pressed = true
slot0.gui_input.emit(right_click)
check.call(client.buy_calls.size() == 1 and client.buy_calls[0]["pos"] == 0, "Right-click direct buy sent shop_buy for slot 0")
# 背包右键道具出售
shop.sell(5, 2)
check.call(client.sell_calls.size() == 1 and client.sell_calls[0]["cell"] == 5, "Selling from inventory triggered shop_sell for cell 5")
shop.close()
check.call(not shop.is_open(), "Shop closed cleanly")
# -------------------------------------------------------------
# Test 4: QuestDialog 40250 thinboard 与 BarButton 样式
# -------------------------------------------------------------
print("\n--- Test 4: QuestDialog thinboard & BarButton Parity ---")
var qd := QuestDialog.new()
root.add_child(qd)
qd.setup(client, root_node, proto)
# 40250 经典 NPC 脚本对话
var script_src := "欢迎光临杂货铺![ENTER]请问有什么需要帮忙的?[QUESTION 1;我要买卖物品|2;打听城里的消息|3;离开]"
client.script_dialog.emit(0, script_src)
qd.process_events(100.0)
check.call(qd.is_open(), "QuestDialog opened")
check.call(qd._panel != null, "QuestDialog background panel exists")
check.call(qd._panel.visible, "QuestDialog thinboard panel is visible")
# 检查选项按钮是否采用了 40250 风格的 BarButton
check.call(qd._btnrow.get_child_count() == 3, "Choice buttons created (3 options)")
var choice0: Button = qd._btnrow.get_child(0)
check.call(choice0 != null and choice0.text == "我要买卖物品", "Choice 0 is '我要买卖物品'")
check.call(choice0.custom_minimum_size.y == 26.0, "Choice 0 height is 26px (40250 BarButton standard)")
# 点击选项 0
choice0.pressed.emit()
check.call(client.script_answers == [0], "Clicking choice 0 sent script_answer(0)")
check.call(not qd.is_open(), "QuestDialog closed on choice select")
# -------------------------------------------------------------
# Test 5: Next/Done 按钮 (100x26 居中 BarButton)
# -------------------------------------------------------------
print("\n--- Test 5: Next/Done 100x26 BarButton ---")
var next_script := "明天还会有新货运到。[NEXT]"
client.script_dialog.emit(0, next_script)
qd.process_events(100.0)
check.call(qd.is_open(), "QuestDialog opened for NEXT")
check.call(qd._btnrow.get_child_count() == 1, "Single next button created")
var next_btn: Button = qd._btnrow.get_child(0)
check.call(next_btn != null and next_btn.text == "继续", "Button text is '继续'")
check.call(next_btn.custom_minimum_size == Vector2(100, 26), "Next button size is 100x26 (40250 centered button)")
next_btn.pressed.emit()
check.call(client.script_answers == [0, 254], "Next button sent script_answer(254)")
check.call(not qd.is_open(), "QuestDialog closed after NEXT")
# -------------------------------------------------------------
# Test 6: QuestCurtain 联动
# -------------------------------------------------------------
print("\n--- Test 6: QuestCurtain open/close lifecycle ---")
var curtain := QuestCurtain.new()
root_node.add_child(curtain)
check.call(not curtain.is_open(), "QuestCurtain initially closed")
qd.opened.connect(func(): curtain.open())
qd.closed.connect(func(): curtain.close())
client.script_dialog.emit(0, "欢迎来到神龙帝国![DONE]")
qd.process_events(100.0)
check.call(curtain.is_open(), "QuestCurtain opened when QuestDialog opened")
qd.close()
check.call(not curtain.is_open(), "QuestCurtain closed when QuestDialog closed")
# 清理
qd.queue_free()
shop.queue_free()
curtain.queue_free()
root_node.queue_free()
client.queue_free()
proto.queue_free()
print("\n=== All NPC Dialog & Shop UI Parity Tests finished with %d failures ===" % fails[0])
quit(fails[0])