Files
mtgodot-poc/project/system_menu_ui_test.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

190 lines
8.0 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.
# system_menu_ui_test —— ESC 系统菜单 + 游戏设置窗 headless 自检(真 uiscript + 假 client)。
# godot --headless --path project --script system_menu_ui_test.gd
# 校验 systemdialog.py / gameoptiondialog.py 装载、/setblockmode + /pkmode 真命令、
# /in_game_mall + /phase_select + /logout、显示开关持久化。缺 uiscript 时优雅跳过。
extends SceneTree
const GameOptionUI = preload("res://ui/game_option_ui.gd")
const SystemMenuUI = preload("res://ui/system_menu_ui.gd")
class FakeClient extends Node:
var says: Array = []
func say(type: int, text: String) -> bool:
says.append([type, text]); return true
func last_say() -> String:
return String(says[-1][1]) if not says.is_empty() else ""
class StubWin extends Node:
var opened := 0
func open() -> void: opened += 1
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: system_menu_ui_test (systemdialog + gameoptiondialog + /setblockmode /pkmode /phase_select)")
quit(0)
else:
printerr("%d check(s) failed" % _fail)
quit(1)
func _cfg_path() -> String:
return ProjectSettings.globalize_path(GameOptionUI.config_path())
func _has_say(fc, text: String) -> bool:
for s in fc.says:
if String(s[1]) == text:
return true
return false
func _run() -> void:
var assets := AssetRoot.path()
if not FileAccess.file_exists(assets.path_join("uiscript/uiscript/systemdialog.py")) \
or not FileAccess.file_exists(assets.path_join("uiscript/uiscript/gameoptiondialog.py")):
print(" (skip: no systemdialog.py / gameoptiondialog.py — assets checkout missing)")
return
if FileAccess.file_exists(_cfg_path()):
DirAccess.remove_absolute(_cfg_path())
var UiManager = load("res://ui/ui_manager.gd")
var ui: CanvasLayer = UiManager.new()
get_root().add_child(ui)
ui._ready()
var fc := FakeClient.new()
get_root().add_child(fc)
# ---------------- GameOptionUI ----------------
var go: Node = GameOptionUI.new()
get_root().add_child(go)
go.setup(ui, fc, assets)
go.open()
_ck(go.is_open(), "game_option window opened")
if not go.is_open():
return
# 屏蔽 toggle -> /setblockmode <mask ^ bit>EBlockAction 位)
(go._node("block_exchange_button") as BaseButton).pressed.emit()
_ck(fc.last_say() == "/setblockmode 1", "block exchange -> /setblockmode 1 (bit 1<<0)")
(go._node("block_party_button") as BaseButton).pressed.emit()
_ck(fc.last_say() == "/setblockmode 3", "block party -> /setblockmode 3 (1|2)")
(go._node("block_exchange_button") as BaseButton).pressed.emit()
_ck(fc.last_say() == "/setblockmode 2", "block exchange again -> /setblockmode 2 (bit cleared)")
_ck(go._block_mode == 2, "_block_mode tracked = 2")
# PK radio -> /pkmode {0,1,4,2}
(go._node("pvp_revenge") as BaseButton).pressed.emit()
_ck(fc.last_say() == "/pkmode 1", "pvp_revenge -> /pkmode 1")
(go._node("pvp_guild") as BaseButton).pressed.emit()
_ck(fc.last_say() == "/pkmode 4", "pvp_guild -> /pkmode 4")
_ck(not (go._node("pvp_revenge") as BaseButton).button_pressed, "pvp radio exclusive")
# 显示开关 -> 持久化 user://system_option.cfg [gameopt]
(go._node("always_show_name_off_button") as BaseButton).pressed.emit()
_ck(int(go._display.get("always_show_name", -1)) == 0, "always_show_name -> 0")
var cfg := ConfigFile.new()
cfg.load(_cfg_path())
_ck(int(cfg.get_value("gameopt", "always_show_name", -1)) == 0, "cfg gameopt.always_show_name persisted")
_ck(int(cfg.get_value("gameopt", "block_mode", -1)) == 2, "cfg gameopt.block_mode persisted")
# §8.4 名字色 radioname_color_normal / name_color_empire-> _display + cfg + 信号
var opt_evt := []
go.display_option_changed.connect(func(k, v): opt_evt.append([k, v]))
(go._node("name_color_empire") as BaseButton).pressed.emit()
_ck(int(go._display.get("name_color", -1)) == 1, "name_color_empire -> _display.name_color = 1")
_ck(opt_evt.size() >= 1 and opt_evt[-1] == ["name_color", 1], "name_color_empire -> display_option_changed('name_color',1)")
# §8.4 看他国目标框 radiotarget_board_view / target_board_no_viewconstinfo 默认 1 = 看)
_ck(int(go._display.get("target_board", -1)) == 1, "target_board 默认 1VIEW_OTHER_EMPIRE_PLAYER_TARGET_BOARD")
(go._node("target_board_no_view") as BaseButton).pressed.emit()
_ck(int(go._display.get("target_board", -1)) == 0, "target_board_no_view -> _display.target_board = 0")
_ck(opt_evt[-1] == ["target_board", 0], "target_board_no_view -> display_option_changed('target_board',0)")
var cfg2 := ConfigFile.new()
cfg2.load(_cfg_path())
_ck(int(cfg2.get_value("gameopt", "name_color", -1)) == 1, "cfg gameopt.name_color persisted")
_ck(int(cfg2.get_value("gameopt", "target_board", -1)) == 0, "cfg gameopt.target_board persisted")
go.close()
# 二次实例读 cfgblock_mode 回填
var go2: Node = GameOptionUI.new()
get_root().add_child(go2)
go2.setup(ui, fc, assets)
_ck(go2._block_mode == 2, "reload: block_mode from cfg")
go2.open()
_ck((go2._node("block_party_button") as BaseButton).button_pressed, "reload: block_party button reflects bit 1<<1")
_ck(not (go2._node("block_exchange_button") as BaseButton).button_pressed, "reload: block_exchange button off")
go2.close()
ui.set_mobile_mode(true)
go2.set_mobile_mode(true)
go2.open()
_ck(go2.is_open() and go2._mobile_root.size == Vector2(700, 360),
"mobile game-option window uses landscape base size")
_ck(go2._mobile_root.get_node_or_null("MobileGameOptionScroll") is ScrollContainer,
"mobile game-option settings are scrollable")
go2._toggle_block(1 << 0)
_ck(fc.last_say() == "/setblockmode 3", "mobile block toggle keeps server command")
go2.close()
# ---------------- SystemMenuUI ----------------
fc.says.clear()
var sys_opt := StubWin.new()
var game_opt := StubWin.new()
get_root().add_child(sys_opt)
get_root().add_child(game_opt)
var sm: Node = SystemMenuUI.new()
get_root().add_child(sm)
sm.setup(ui, fc, assets, sys_opt, game_opt)
sm.open()
_ck(sm.is_open(), "system menu opened")
# mall_button removed in 40250 systemdialog.py (only present in locale variant)
sm.open()
(sm._node("change_button") as BaseButton).pressed.emit()
_ck(_has_say(fc, "/phase_select"), "change_button -> /phase_select (net.ExitGame)")
sm.open()
(sm._node("logout_button") as BaseButton).pressed.emit()
_ck(_has_say(fc, "/logout"), "logout_button -> /logout (net.LogOutGame)")
sm.open()
(sm._node("system_option_button") as BaseButton).pressed.emit()
_ck(sys_opt.opened == 1 and not sm.is_open(), "system_option_button -> opens system_option_ui + closes menu")
sm.open()
(sm._node("game_option_button") as BaseButton).pressed.emit()
_ck(game_opt.opened == 1 and not sm.is_open(), "game_option_button -> opens game_option_ui + closes menu")
sm.open()
(sm._node("cancel_button") as BaseButton).pressed.emit()
_ck(not sm.is_open(), "cancel_button closes menu")
# 移动端使用独立横屏菜单,仍复用同一组设置、帮助和服务器命令入口。
ui.set_mobile_mode(true)
sm.set_mobile_mode(true)
sm.open()
_ck(sm.is_open() and sm._mobile_root.size == Vector2(700, 330),
"mobile system menu uses landscape base size")
for action in ["帮助", "商城", "系统设置", "游戏设置", "选择角色", "登出", "退出游戏", "取消"]:
_ck(sm._mobile_buttons.has(action), "mobile system menu action: " + action)
(sm._mobile_buttons["商城"] as Button).pressed.emit()
_ck(_has_say(fc, "/in_game_mall"), "mobile mall action -> /in_game_mall")
_ck(not sm.is_open(), "mobile action closes menu")
sm.open()
(sm._mobile_buttons["系统设置"] as Button).pressed.emit()
_ck(sys_opt.opened == 2 and not sm.is_open(),
"mobile system option opens feature and closes menu")
sm.open()
(sm._mobile_buttons["游戏设置"] as Button).pressed.emit()
_ck(game_opt.opened == 2 and not sm.is_open(),
"mobile game option opens feature and closes menu")
sm.open()
(sm._mobile_buttons["取消"] as Button).pressed.emit()
_ck(not sm.is_open(), "mobile cancel closes menu")
if FileAccess.file_exists(_cfg_path()):
DirAccess.remove_absolute(_cfg_path())