Files
mtgodot-poc/project/ui/game_option_ui.gd
T
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
  phases, EntityStore world model, ~all GC/CG headers. char create/delete,
  private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
  quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
  char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
  token), system-option + game-option + ESC system menu, private-shop 39-grid,
  party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
  dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.

Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.

Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).

ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
2026-08-31 20:02:12 +09:00

194 lines
7.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.
# GameOptionUI (P11) —— 游戏设置窗(1:1 迁移 `assets/root/uigameoption.py` `OptionDialog`)。
#
# var go := preload("res://ui/game_option_ui.gd").new()
# add_child(go)
# go.setup(ui_manager, m2client, assets_root)
# go.toggle() # 由 system_menu_ui 的 game_option_button 打开
#
# 布局走真 `assets/uiscript/uiscript/gameoptiondialog.py`。绑定逐字对照 uigameoption.py
# block_{exchange,party,guild,whisper,friend,party_request}_button (toggle) →
# `/setblockmode <mask ^ bit>`EBlockAction 位:1<<0..1<<5)—— 真聊天命令
# pvp_{peace,revenge,guild,free} (radio) → `/pkmode {0,1,4,2}` —— 真聊天命令
# name_color / target_board / view_chat / always_show_name / show_damage / salestext (radio)
# —— 客户端显示开关,持久化到 user://system_option.cfg [gameopt];渲染侧钩子待补。
extends Node
# Packet.h EBlockAction
const BLOCK_BITS := {
"block_exchange_button": 1 << 0,
"block_party_button": 1 << 1,
"block_guild_button": 1 << 2,
"block_whisper_button": 1 << 3,
"block_friend_button": 1 << 4,
"block_party_request_button": 1 << 5,
}
# uigameoption: peace /pkmode 0, revenge /pkmode 1, free /pkmode 2, guild /pkmode 4
const PK_CMD := {"pvp_peace": 0, "pvp_revenge": 1, "pvp_free": 2, "pvp_guild": 4}
# 客户端显示开关:{radio 名 -> [cfg key, 该按钮代表的值]}
const DISPLAY_RADIOS := {
"name_color_normal": ["name_color", 0], "name_color_empire": ["name_color", 1],
"target_board_no_view": ["target_board", 0], "target_board_view": ["target_board", 1],
"view_chat_on_button": ["view_chat", 1], "view_chat_off_button": ["view_chat", 0],
"always_show_name_on_button": ["always_show_name", 1], "always_show_name_off_button": ["always_show_name", 0],
"show_damage_on_button": ["show_damage", 1], "show_damage_off_button": ["show_damage", 0],
"salestext_on_button": ["salestext", 1], "salestext_off_button": ["salestext", 0],
}
const RADIO_GROUPS := [
["name_color_normal", "name_color_empire"],
["target_board_no_view", "target_board_view"],
["view_chat_on_button", "view_chat_off_button"],
["always_show_name_on_button", "always_show_name_off_button"],
["show_damage_on_button", "show_damage_off_button"],
["salestext_on_button", "salestext_off_button"],
]
const LABELS := {
"titlename": "游戏设置", "name_color": "名字颜色", "target_board": "目标框",
"pvp_mode": "PK 模式", "block": "屏蔽", "chat": "聊天显示",
"always_show_name": "总显示名字", "effect_on_off": "伤害数字", "salestext_on_off": "叫卖文字",
"name_color_normal": "普通", "name_color_empire": "阵营",
"target_board_no_view": "不看他国", "target_board_view": "看他国",
"pvp_peace": "和平", "pvp_revenge": "反击", "pvp_guild": "帮会", "pvp_free": "自由",
"block_exchange_button": "交易", "block_party_button": "组队", "block_guild_button": "帮会",
"block_whisper_button": "密语", "block_friend_button": "好友", "block_party_request_button": "组队申请",
"view_chat_on_button": "开", "view_chat_off_button": "关",
"always_show_name_on_button": "开", "always_show_name_off_button": "关",
"show_damage_on_button": "开", "show_damage_off_button": "关",
"salestext_on_button": "开", "salestext_off_button": "关",
}
const CFG_PATH := "user://system_option.cfg"
var ui: CanvasLayer
var client: Node
var assets_root := ""
var uiscript_dir := ""
var _win: Dictionary = {}
var _cfg := ConfigFile.new()
var _block_mode := 0 # blockMode(本地跟踪;服务器回包同步待补)
var _display := {} # cfg [gameopt] 快照
func setup(ui_manager: CanvasLayer, m2client: Node, 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")
_cfg.load(CFG_PATH)
for k in ["name_color", "target_board", "view_chat", "always_show_name", "show_damage", "salestext"]:
_display[k] = int(_cfg.get_value("gameopt", k, 1 if k in ["view_chat", "always_show_name", "show_damage"] else 0))
_block_mode = int(_cfg.get_value("gameopt", "block_mode", 0))
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("gameoptiondialog.py")
if not FileAccess.file_exists(path):
push_warning("GameOptionUI: no gameoptiondialog.py at " + path)
return
_win = ui.open_script(path, assets_root)
if not is_open():
return
_relabel()
_wire()
_sync()
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
func _relabel() -> void:
for nm in LABELS:
var n := _node(nm)
if n and n.has_method("set_text"):
n.set_text(LABELS[nm])
func _wire() -> void:
# 屏蔽 toggle → /setblockmode
for nm in BLOCK_BITS:
var b := _node(nm)
if b is BaseButton:
b.toggle_mode = true
var bit: int = BLOCK_BITS[nm]
b.pressed.connect(func(): _toggle_block(bit))
# PK radio → /pkmode
_bind_radio(PK_CMD.keys(), func(nm):
if client and client.has_method("say"):
client.say(0, "/pkmode %d" % PK_CMD[nm])) # CHAT_TYPE_TALKING
# 显示开关 radio → 持久化
for group in RADIO_GROUPS:
_bind_radio(group, func(nm):
var spec: Array = DISPLAY_RADIOS[nm]
_display[spec[0]] = int(spec[1])
_save())
var tb := _node("titlebar")
if tb:
for x in tb.find_children("*", "BaseButton", true, false):
x.pressed.connect(close)
func _bind_radio(names: Array, on_pick: Callable) -> void:
var boxes := {}
for nm: String in names:
boxes[nm] = _node(nm)
for nm: String in names:
var b = boxes[nm]
if not (b is BaseButton):
continue
b.toggle_mode = true
var picked: String = nm
b.pressed.connect(func():
for other: String in names:
if boxes[other] is BaseButton:
boxes[other].set_pressed_no_signal(other == picked)
on_pick.call(picked))
func _toggle_block(bit: int) -> void:
_block_mode ^= bit
if client and client.has_method("say"):
client.say(0, "/setblockmode %d" % _block_mode)
_cfg.set_value("gameopt", "block_mode", _block_mode)
_cfg.save(CFG_PATH)
_sync_block()
# 开窗时把状态回填到控件
func _sync() -> void:
_sync_block()
for group in RADIO_GROUPS:
var key: String = DISPLAY_RADIOS[group[0]][0]
var cur := int(_display.get(key, 0))
for nm in group:
var b := _node(nm)
if b is BaseButton:
b.set_pressed_no_signal(int(DISPLAY_RADIOS[nm][1]) == cur)
func _sync_block() -> void:
for nm in BLOCK_BITS:
var b := _node(nm)
if b is BaseButton:
b.set_pressed_no_signal((_block_mode & int(BLOCK_BITS[nm])) != 0)
func _save() -> void:
for k in _display:
_cfg.set_value("gameopt", k, int(_display[k]))
_cfg.set_value("gameopt", "block_mode", _block_mode)
_cfg.save(CFG_PATH)