230 lines
9.1 KiB
GDScript
230 lines
9.1 KiB
GDScript
# 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])
|