40250 classic: increment 50 (Phase 1 W3) — §8.3 in-game help window

Implement the in-game Help window as a 1:1 migration of uihelp.py's
HelpWindow (single-page: ENABLE_HELP_MULTIPAGE = 0 -> LoadDialogSinglePage).

- project/ui/help_ui.gd (new): loads the real helpwindow.py uiscript via
  UiScript -> UiBuild; _relabel() resolves the <HELP_*>/<CLOSE> placeholders
  (produced when UiManager has no locale callback) from the project Locale
  table (en) with verbatim English fallbacks, covering the original script's
  two duplicate help_02 rows (right/middle camera). open/close/toggle/is_open;
  close_button -> close; ESC handled by the UiManager window stack.
- project/ui/system_menu_ui.gd: help_button changed from the placeholder toast
  to close() + open_help(); adds const HelpUI, an injectable help_ui member,
  _ensure_help() (lazy build, reuses the menu's ui/client/assets_root),
  open_help(), toggle_help(); setup() gains an optional 6th arg (old 5-arg
  callers unaffected).
- project/game_scene.gd: single elif in _unhandled_input — KEY_H without
  ctrl/meta -> system_menu_ui.toggle_help() (Ctrl+H stays free for
  /user_horse_ride).
- project/help_ui_test.gd (new): headless SceneTree test — helpwindow.py load,
  19 verbatim help rows + taskbar labels bound and non-placeholder,
  close_button text, rows() helper, close/open/toggle, ESC stack top,
  SystemMenuUI.open_help() lazy build.

Tests: help_ui_test, system_menu_ui_test, gamescene_test, ui_test all PASS
(plus system_option_ui_test, char_status_ui_test). No build/ in this worktree
so C++ ctest not run (no C++ change). Real-server and visual (.sub textures)
not verified. Docs: CLIENT-GAP.md W3 row + increment 50 bullet;
CLIENT-GAP-FIX.md §8.3 status/改动/测试/验收 + C.5 batch record.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
This commit is contained in:
shenlei
2026-09-02 15:34:36 +09:00
co-authored by Claude Sonnet 5
parent f32064c74a
commit d7711ba5f0
8 changed files with 431 additions and 15 deletions
+197
View File
@@ -0,0 +1,197 @@
# HelpUI (P11 / §8.3) —— 游戏内帮助窗(1:1 迁移 `assets/root/uihelp.py` 的
# `HelpWindow`,单页版:`ENABLE_HELP_MULTIPAGE = 0` → `LoadDialogSinglePage()`)。
#
# var hw := preload("res://ui/help_ui.gd").new()
# add_child(hw)
# hw.setup(ui_manager, m2client, assets_root)
# hw.toggle() # H 键(非 Ctrl
# hw.open() # ESC 系统菜单 help_button
#
# 布局走真 `assets/uiscript/uiscript/helpwindow.py`UiScript → UiBuild)。
# 文本逐字对齐 `uiScriptLocale.HELP_*``locale/locale/<lang>/locale_interface.txt`);
# `ui_manager` 没接 locale 回调时,`helpwindow.py` 里的 `uiScriptLocale.X` 会被
# UiScript 解析成 `<X>` 占位,`_relabel()` 再用本工程的 `Locale` 表 + 英文兜底补回。
#
# 入口逐字对照 `game.py:459 __PressHKey()`:非 Ctrl → `interface.OpenHelpWindow()`
# Ctrl+H 是 `net.SendChatPacket("/user_horse_ride")`,不在本窗职责内)。
# 关闭对照 `helpwindow.py` 的 `HelpWindow``close_button` / `OnPressEscapeKey` /
# `OnPressExitKey` / `OnKeyDown(DIK_H)` 都调用 `eventClose`。ESC 由 `UiManager`
# 的窗口栈统一处理(关最顶层);H 键复用 `toggle()`。
extends Node
# helpwindow.py 的 name → uiScriptLocale key(逐字,含原脚本里重名的两个 `help_02`)。
const FLOAT_ROWS := {
"help_01": "HELP_MOVE_KEY",
"help_02": "HELP_CONTROL_CAMERA_BY_MIDDLEBUTTON", # 原脚本第二个 help_02(后者覆盖)
"help_03": "HELP_SHOW_ALL_NAME",
"help_04": "HELP_OPEN_CHAT",
"help_05": "HELP_OPEN_WHISPER",
"help_06": "HELP_ATTACK_KEY",
"help_07": "HELP_OPEN_CHARACTER",
"help_08": "HELP_OPEN_SKILL",
"help_09": "HELP_OPEN_QUEST",
"help_10": "HELP_OPEN_INVENTORY",
"help_11": "HELP_OPEN_LOG",
"help_12": "HELP_OPEN_ZONEMAP",
"help_13": "HELP_OPEN_MINIMAP",
"help_14": "HELP_CHANGE_PK_MODE",
"help_15": "HELP_PICK_ITEM",
"help_16": "HELP_SCREEN_CAPTURE",
"help_17": "HELP_GUILD_WINDOW",
"help_18": "HELP_MESSENGER_WINDOW",
"help_19": "HELP_HELP",
}
# 任务栏贴纸标签(helpwindow.py 的 `taskbar_help_*` 文本)。
const TASKBAR_ROWS := {
"taskbar_help_01": "HELP_FURY",
"taskbar_help_02": "HELP_HP",
"taskbar_help_03": "HELP_SP",
"taskbar_help_04": "HELP_EXP",
"taskbar_help_05": "HELP_MOUSE_LEFT",
"taskbar_help_06": "HELP_MOUSE_RIGHT",
"taskbar_help_07": "HELP_QUICKSLOT",
"taskbar_help_08a": "HELP_SYSTEM_BUTTON",
"taskbar_help_08b": "HELP_CHARACTER_BUTTON1",
"taskbar_help_08c": "HELP_CHARACTER_BUTTON2",
}
# uiScriptLocale 兜底(逐字取自 `locale/locale/en/locale_interface.txt`)——
# 资产缺 locale 表或 UiManager 没接 locale 回调时用这份。
const FALLBACK := {
"HELP_MOVE_KEY": "Panel: W, A, S, D or arrow keys",
"HELP_CONTROL_CAMERA_BY_RIGHTBUTTON": "Camera View: right or middle mouse button",
"HELP_CONTROL_CAMERA_BY_MIDDLEBUTTON": "Camera View: middle or right mouse button",
"HELP_SHOW_ALL_NAME": "Display Names: Alt",
"HELP_OPEN_CHAT": "Open chat window: Enter",
"HELP_OPEN_WHISPER": "Open Whisper Window: Shift + Enter",
"HELP_ATTACK_KEY": "Attack: left mouse button or space bar",
"HELP_OPEN_CHARACTER": "Open character window: C",
"HELP_OPEN_SKILL": "Open Skill Window: V",
"HELP_OPEN_QUEST": "Open Task Window: N",
"HELP_OPEN_INVENTORY": "Open inventory window: I",
"HELP_OPEN_LOG": "Open Chatlog: L",
"HELP_OPEN_ZONEMAP": "Open Large Map: M",
"HELP_OPEN_MINIMAP": "Open mini map: Shift + M",
"HELP_CHANGE_PK_MODE": "Change attack mode: change setting of left mouse button",
"HELP_PICK_ITEM": "Collect Items: ^ or Y or left mouse button",
"HELP_SCREEN_CAPTURE": "Save Screenshot: Print (will be saved in file \"Metin2\\screenshot\")",
"HELP_GUILD_WINDOW": "Open Guild Window: Alt + G",
"HELP_MESSENGER_WINDOW": "Open Friends List: Alt + M",
"HELP_HELP": "Display help: H",
"HELP_FURY": "Ingame Item Shop",
"HELP_HP": "Hit Points (HP)",
"HELP_SP": "Spell Points (SP)",
"HELP_EXP": "Experience",
"HELP_MOUSE_LEFT": "Function of the Left Mouse button",
"HELP_MOUSE_RIGHT": "Function of the Right Mouse button",
"HELP_QUICKSLOT": "Fast Access Fields",
"HELP_SYSTEM_BUTTON": "System buttons",
"HELP_CHARACTER_BUTTON1": "(Character Window, Inventory Window,",
"HELP_CHARACTER_BUTTON2": "List of Friends and Options)",
"CLOSE": "Close",
}
var ui: CanvasLayer
var client: Node
var assets_root := ""
var uiscript_dir := ""
var _win: Dictionary = {}
var _loc # Localeres://locale.gd
func setup(ui_manager: CanvasLayer, m2client: Node = null, assets := "") -> void:
ui = ui_manager
client = m2client
assets_root = assets
if assets_root == "" and ui and "assets_root" in ui:
assets_root = ui.assets_root
uiscript_dir = assets_root.path_join("uiscript/uiscript")
if not DirAccess.dir_exists_absolute(uiscript_dir):
uiscript_dir = assets_root.path_join("uiscript")
var LocaleScript = load("res://locale.gd")
if LocaleScript:
_loc = LocaleScript.new()
_loc.setup(assets_root, "en")
# --- open / close ---------------------------------------------------
func is_open() -> bool:
return not _win.is_empty() and is_instance_valid(_win.get("root"))
func toggle() -> void:
if is_open(): close()
else: open()
func close() -> void:
if is_open():
ui.close(_win["root"])
_win = {}
func open() -> void:
if is_open():
return
var path := uiscript_dir.path_join("helpwindow.py")
if not FileAccess.file_exists(path):
push_warning("HelpUI: no helpwindow.py at " + path)
return
_win = ui.open_script(path, assets_root)
if not is_open():
return
_relabel()
_wire()
func _node(nm: String) -> Control:
if _win.is_empty():
return null
var n = _win.get("nodes", {}).get(nm, null)
return n if n is Control else null
# --- content (uihelp.py.LoadDialogSinglePage + helpwindow.py 文本) ---------
# 解析 `<KEY>` 占位 / 应用 name→key 映射:先扫所有 Label(覆盖原脚本重名的两个
# `help_02`),再按映射兜一遍(nodes dict 只留最后一个同名)。
func _relabel() -> void:
var root: Control = _win.get("root")
if root:
for lbl in root.find_children("*", "Label", true, false):
var t := String(lbl.text)
if t.length() >= 2 and t.begins_with("<") and t.ends_with(">"):
lbl.text = _text(t.substr(1, t.length() - 2))
for nm in FLOAT_ROWS:
_set_text(nm, _text(FLOAT_ROWS[nm]))
for nm in TASKBAR_ROWS:
_set_text(nm, _text(TASKBAR_ROWS[nm]))
_set_text("close_button", _text("CLOSE"))
func _set_text(nm: String, s: String) -> void:
var n := _node(nm)
if n == null:
return
if n.has_method("set_text"):
n.set_text(s)
elif "text" in n:
n.text = s
# uiScriptLocale.<key> 的取值:优先本工程 Locale 表,否则英文兜底,否则原样 key。
func _text(key: String) -> String:
if _loc != null and _loc.has(key):
return _loc.t(key)
return FALLBACK.get(key, key)
# 已绑定的帮助行文本(供自检 / 调试)。
func rows() -> Array:
var out: Array = []
for nm in FLOAT_ROWS:
var n := _node(nm)
if n and "text" in n:
out.append(String(n.text))
return out
# --- wiring (uihelp.py.HelpWindow.SetCloseEvent) ------------------------
func _wire() -> void:
var btn := _node("close_button")
if btn is BaseButton:
btn.pressed.connect(close)
# helpwindow.py2 的多页按钮在单页版不存在;此处不绑。
+1
View File
@@ -0,0 +1 @@
uid://dfoy732rqh7hd
+23 -5
View File
@@ -7,7 +7,7 @@
# sm.toggle() # ESC(无其它窗打开时)
#
# 按钮绑定逐字对照 uisystem.py
# help_button → OpenHelpWindow暂无帮助窗 → 提示
# help_button → interface.OpenHelpWindowhelp_ui.gd,§8.3
# mall_button → net.SendChatPacket("/in_game_mall")
# system_option_button → uiSystemOption.OptionDialogsystem_option_ui
# game_option_button → uiGameOption.OptionDialoggame_option_ui
@@ -17,6 +17,8 @@
# cancel_button / 标题栏 → 关闭
extends Node
const HelpUI = preload("res://ui/help_ui.gd")
const LABELS := {
"help_button": "帮助", "mall_button": "商城", "system_option_button": "系统设置",
"game_option_button": "游戏设置", "change_button": "选择角色", "logout_button": "登出",
@@ -29,16 +31,18 @@ var assets_root := ""
var uiscript_dir := ""
var system_option_ui: Node
var game_option_ui: Node
var help_ui: Node # HelpUI(§8.3)—— 未注入时 _ensure_help() 懒建
var toast: Callable = Callable() # 可选:func(msg: String) 显示提示
var _win: Dictionary = {}
func setup(ui_manager: CanvasLayer, m2client: Node, assets := "",
sys_opt: Node = null, game_opt: Node = null) -> void:
sys_opt: Node = null, game_opt: Node = null, help_win: Node = null) -> void:
ui = ui_manager
client = m2client
system_option_ui = sys_opt
game_option_ui = game_opt
help_ui = help_win
assets_root = assets
if assets_root == "" and ui and "assets_root" in ui:
assets_root = ui.assets_root
@@ -88,11 +92,25 @@ func _btn(nm: String, cb: Callable) -> void:
if b is BaseButton:
b.pressed.connect(cb)
# uisystem.py: self.interface.OpenHelpWindow()。未注入 help_ui 时懒建一个,
# 复用本菜单的 ui / client / assets_root。
func _ensure_help() -> Node:
if not is_instance_valid(help_ui):
help_ui = HelpUI.new()
add_child(help_ui)
help_ui.setup(ui, client, assets_root)
return help_ui
func open_help() -> void:
_ensure_help().open()
func toggle_help() -> void:
_ensure_help().toggle()
func _wire() -> void:
_btn("help_button", func():
if toast.is_valid():
toast.call("帮助窗暂未实现")
)
close()
open_help())
_btn("mall_button", func():
if client and client.has_method("say"):
client.say(0, "/in_game_mall") # net.SendChatPacket("/in_game_mall")