- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
178 lines
6.8 KiB
GDScript
178 lines
6.8 KiB
GDScript
# popup_windows_parity_test.gd —— 校验弹窗(人物属性面板、仓库、背包等)
|
|
# 严格按照 40250 客户端的 UI 规范与关闭行为。
|
|
# godot --headless --path project --script popup_windows_parity_test.gd
|
|
extends SceneTree
|
|
|
|
const UiManager = preload("res://ui/ui_manager.gd")
|
|
const CharStatusUI = preload("res://ui/char_status_ui.gd")
|
|
const SafeboxUI = preload("res://ui/safebox_ui.gd")
|
|
const InventoryUI = preload("res://ui/inventory_ui.gd")
|
|
const AssetRoot = preload("res://asset_root.gd")
|
|
|
|
class FakeClient extends Node:
|
|
signal points_changed(points: Dictionary)
|
|
signal entity_main_set(vid: int)
|
|
signal safebox_opened(size: int, gold: int)
|
|
signal safebox_closed()
|
|
signal safebox_item_set(cell: int, vnum: int, count: int, flags: int, anti_flags: int, sockets: Array, attrs: Array)
|
|
signal inventory_changed(window: int, cell: int)
|
|
|
|
var pts: Array = []
|
|
var _main_vid := 7
|
|
|
|
func _init() -> void:
|
|
pts.resize(255)
|
|
pts.fill(0)
|
|
pts[1] = 42
|
|
pts[5] = 850
|
|
pts[6] = 900
|
|
pts[7] = 120
|
|
pts[8] = 150
|
|
|
|
func get_main_vid() -> int: return _main_vid
|
|
func get_points() -> Dictionary:
|
|
return {
|
|
"points": pts, "hp": pts[5], "max_hp": pts[6], "sp": pts[7], "max_sp": pts[8],
|
|
"level": pts[1], "exp": 0, "next_exp": 1000, "gold": 10000,
|
|
}
|
|
func get_entity(vid: int) -> Dictionary:
|
|
return {"name": "TestHero", "guild": 1, "race": 0}
|
|
func get_guild_name(gid: int) -> String:
|
|
return "HeroGuild" if gid == 1 else ""
|
|
func get_inventory() -> Array: return []
|
|
func get_equipment() -> Array: return []
|
|
func get_belt_inventory() -> Array: return []
|
|
func get_safebox_items() -> Array: return []
|
|
func get_safebox_gold() -> int: return 5000
|
|
func get_safebox_size() -> int: return 45
|
|
func safebox_close() -> void: pass
|
|
func say(type: int, text: String) -> bool: return true
|
|
|
|
var _fail := 0
|
|
|
|
func _ck(c: bool, m: String) -> void:
|
|
if not c:
|
|
_fail += 1
|
|
printerr("FAIL: " + m)
|
|
|
|
func _init() -> void:
|
|
_run()
|
|
if _fail == 0:
|
|
print("PASS: popup_windows_parity_test (CharacterWindow, SafeboxWindow, InventoryWindow layout + 40250 close behaviors)")
|
|
quit(0)
|
|
else:
|
|
printerr("%d check(s) failed" % _fail)
|
|
quit(1)
|
|
|
|
func _run() -> void:
|
|
var assets := AssetRoot.path()
|
|
if not FileAccess.file_exists(assets.path_join("uiscript/uiscript/characterwindow.py")):
|
|
print(" (skip: missing uiscript assets)")
|
|
return
|
|
|
|
var ui: CanvasLayer = UiManager.new()
|
|
get_root().add_child(ui)
|
|
ui._ready()
|
|
|
|
var client := FakeClient.new()
|
|
get_root().add_child(client)
|
|
|
|
# =========================================================================
|
|
# 1. CharacterWindow
|
|
# =========================================================================
|
|
var char_ui := CharStatusUI.new()
|
|
get_root().add_child(char_ui)
|
|
char_ui.setup(ui, client, assets)
|
|
char_ui.open("STATUS")
|
|
_ck(char_ui.is_open(), "CharacterWindow is_open() after open('STATUS')")
|
|
|
|
var char_root: Control = char_ui._win.get("root")
|
|
_ck(char_root != null, "CharacterWindow root node exists")
|
|
if char_root:
|
|
_ck(char_root.position.x >= 0.0, "CharacterWindow position.x (%f) >= 0" % char_root.position.x)
|
|
_ck(char_root.position.y >= 0.0, "CharacterWindow position.y (%f) >= 0" % char_root.position.y)
|
|
|
|
# Face slot / Guild slot auto-deduction of dimensions
|
|
var face_slot: Control = char_ui._node("Face_Slot")
|
|
if face_slot:
|
|
_ck(face_slot.size.x > 0 and face_slot.size.y > 0,
|
|
"Face_Slot has non-zero size: %s" % str(face_slot.size))
|
|
|
|
var guild_slot: Control = char_ui._node("Guild_Name_Slot")
|
|
if guild_slot:
|
|
_ck(guild_slot.size.x > 0 and guild_slot.size.y > 0,
|
|
"Guild_Name_Slot has non-zero size: %s" % str(guild_slot.size))
|
|
|
|
# Tab button type check: must be Button with toggle_mode, NOT CheckBox
|
|
var tab1: Control = char_ui._node("Tab_Button_01")
|
|
if tab1:
|
|
_ck(tab1 is Button and not (tab1 is CheckBox),
|
|
"Tab_Button_01 is Button (not CheckBox)")
|
|
_ck((tab1 as Button).toggle_mode, "Tab_Button_01 has toggle_mode = true")
|
|
|
|
# Close button inside TitleBar
|
|
var close_btn: BaseButton = char_root.find_child("CloseButton", true, false)
|
|
_ck(close_btn != null, "CharacterWindow has CloseButton in titlebar")
|
|
if close_btn:
|
|
close_btn.pressed.emit()
|
|
_ck(not char_ui.is_open(), "CharacterWindow closes when CloseButton is pressed")
|
|
|
|
# =========================================================================
|
|
# 2. SafeboxWindow
|
|
# =========================================================================
|
|
var safe_ui := SafeboxUI.new()
|
|
get_root().add_child(safe_ui)
|
|
safe_ui.setup(client, ui, null, assets)
|
|
safe_ui.open()
|
|
_ck(safe_ui.is_open(), "SafeboxWindow is_open() after open()")
|
|
|
|
var safe_root: Control = safe_ui._root
|
|
_ck(safe_root != null, "SafeboxWindow _root node exists")
|
|
if safe_root:
|
|
_ck(safe_root.position.x >= 0.0, "SafeboxWindow position.x (%f) >= 0 (not negative off-screen)" % safe_root.position.x)
|
|
_ck(safe_root.position.y >= 0.0, "SafeboxWindow position.y (%f) >= 0 (not negative off-screen)" % safe_root.position.y)
|
|
|
|
# Page buttons check: page radio buttons are Buttons with toggle_mode, not CheckBoxes
|
|
var page1: Control = safe_root.find_child("PAGE1_BUTTON", true, false)
|
|
if page1:
|
|
_ck(page1 is Button and not (page1 is CheckBox),
|
|
"PAGE1_BUTTON is Button (not CheckBox)")
|
|
|
|
# CloseButton on TitleBar
|
|
var safe_close_btn: BaseButton = safe_root.find_child("CloseButton", true, false)
|
|
_ck(safe_close_btn != null, "SafeboxWindow has CloseButton in titlebar")
|
|
if safe_close_btn:
|
|
safe_close_btn.pressed.emit()
|
|
_ck(not safe_ui.is_open(), "SafeboxWindow closes when CloseButton is pressed")
|
|
|
|
# Re-open and test ExitButton ("关闭" button at bottom)
|
|
safe_ui.open()
|
|
_ck(safe_ui.is_open(), "SafeboxWindow re-opened")
|
|
if safe_ui.is_open():
|
|
var exit_btn: BaseButton = safe_ui._root.find_child("ExitButton", true, false)
|
|
_ck(exit_btn != null, "SafeboxWindow has ExitButton at bottom")
|
|
if exit_btn:
|
|
exit_btn.pressed.emit()
|
|
_ck(not safe_ui.is_open(), "SafeboxWindow closes when ExitButton is pressed")
|
|
|
|
# =========================================================================
|
|
# 3. InventoryWindow
|
|
# =========================================================================
|
|
var inv_ui := InventoryUI.new()
|
|
get_root().add_child(inv_ui)
|
|
inv_ui.setup(ui, client, null, assets)
|
|
inv_ui.open()
|
|
_ck(inv_ui.is_open(), "InventoryWindow is_open() after open()")
|
|
|
|
var inv_root: Control = inv_ui._win.get("root")
|
|
_ck(inv_root != null, "InventoryWindow root node exists")
|
|
if inv_root:
|
|
_ck(inv_root.position.x >= 0.0, "InventoryWindow position.x (%f) >= 0" % inv_root.position.x)
|
|
_ck(inv_root.position.y >= 0.0, "InventoryWindow position.y (%f) >= 0" % inv_root.position.y)
|
|
|
|
var inv_close_btn: BaseButton = inv_root.find_child("CloseButton", true, false)
|
|
_ck(inv_close_btn != null, "InventoryWindow has CloseButton in TitleBar")
|
|
if inv_close_btn:
|
|
inv_close_btn.pressed.emit()
|
|
_ck(not inv_ui.is_open(), "InventoryWindow closes when CloseButton is pressed")
|