# 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 `(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 const MobileTouchButton = preload("res://ui/mobile/mobile_touch_button.gd") # 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" # uigameoption RefreshAlwaysShowName / 名字色 / 伤害数字等:radio 选中后广播给渲染层。 # key ∈ DISPLAY_RADIOS 的 cfg key(name_color / always_show_name / show_damage / ...)。 signal display_option_changed(key: String, value: int) 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] 快照 var _mobile_mode := false var _mobile_root: Control static func config_path() -> String: var override_dir := OS.get_environment("MT_CONFIG_DIR") if override_dir != "": return override_dir.path_join("system_option.cfg") return OS.get_temp_dir().path_join("mtgodot_system_option.cfg") \ if DisplayServer.get_name() == "headless" else CFG_PATH 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(config_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 ["target_board", "view_chat", "always_show_name", "show_damage"] else 0)) _block_mode = int(_cfg.get_value("gameopt", "block_mode", 0)) if client and client.has_signal("block_mode_changed"): client.block_mode_changed.connect(_on_server_block_mode) func is_open() -> bool: if _mobile_mode: return _mobile_root != null and is_instance_valid(_mobile_root) return not _win.is_empty() and is_instance_valid(_win.get("root")) func set_mobile_mode(enabled: bool) -> void: if not enabled and _mobile_root != null: var old_mode := _mobile_mode _mobile_mode = true close() _mobile_mode = old_mode _mobile_mode = enabled # 渲染层开局回填用:cfg [gameopt] 里某显示开关的当前值(0/1),缺省同 setup() 的默认。 func display_value(key: String) -> int: return int(_display.get(key, 0)) func toggle() -> void: if is_open(): close() else: open() func close() -> void: if _mobile_mode: if _mobile_root and is_instance_valid(_mobile_root) and ui: ui.close(_mobile_root) _mobile_root = null return if is_open(): ui.close(_win["root"]) _win = {} func open() -> void: if _mobile_mode: _open_mobile() return 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 _open_mobile() -> void: if is_open(): return _mobile_root = Control.new() _mobile_root.name = "MobileGameOptionWindow" _mobile_root.size = Vector2(700, 360) _mobile_root.set_meta("mobile_title", "游戏设置") _mobile_root.mouse_filter = Control.MOUSE_FILTER_STOP var panel := Panel.new() panel.set_anchors_preset(Control.PRESET_FULL_RECT) panel.add_theme_stylebox_override("panel", _mobile_panel_style()) _mobile_root.add_child(panel) var scroll := ScrollContainer.new() scroll.name = "MobileGameOptionScroll" scroll.position = Vector2(14, 48) scroll.size = Vector2(672, 298) scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED scroll.mouse_filter = Control.MOUSE_FILTER_STOP _mobile_root.add_child(scroll) var content := VBoxContainer.new() content.custom_minimum_size = Vector2(650, 0) content.add_theme_constant_override("separation", 8) scroll.add_child(content) _add_mobile_section_title(content, "屏蔽设置") _add_mobile_block_buttons(content) _add_mobile_section_title(content, "PK 模式") _add_mobile_pk_radios(content) _add_mobile_section_title(content, "显示设置") _add_mobile_display_radios(content) ui.open(_mobile_root) func _add_mobile_section_title(parent: VBoxContainer, text: String) -> void: var title := Label.new() title.text = text title.custom_minimum_size = Vector2(640, 28) title.add_theme_font_size_override("font_size", 14) title.add_theme_color_override("font_color", Color(0.95, 0.82, 0.54)) parent.add_child(title) func _add_mobile_block_buttons(parent: VBoxContainer) -> void: var grid := GridContainer.new() grid.columns = 3 grid.custom_minimum_size = Vector2(640, 94) grid.add_theme_constant_override("h_separation", 6) grid.add_theme_constant_override("v_separation", 6) parent.add_child(grid) for name in BLOCK_BITS: var button := Button.new() button.text = String(LABELS.get(name, name)) button.toggle_mode = true button.button_pressed = (_block_mode & int(BLOCK_BITS[name])) != 0 button.custom_minimum_size = Vector2(206, 42) var bit := int(BLOCK_BITS[name]) button.pressed.connect(func(): _toggle_block(bit)) grid.add_child(button) func _add_mobile_pk_radios(parent: VBoxContainer) -> void: var row := HBoxContainer.new() row.custom_minimum_size = Vector2(640, 44) row.add_theme_constant_override("separation", 6) var buttons: Array[Button] = [] for name in ["pvp_peace", "pvp_revenge", "pvp_guild", "pvp_free"]: var button := Button.new() button.text = String(LABELS.get(name, name)) button.toggle_mode = true button.custom_minimum_size = Vector2(145, 42) button.button_pressed = false var picked: String = name button.pressed.connect(func(): for other in buttons.size(): buttons[other].set_pressed_no_signal(buttons[other] == button) if client and client.has_method("say"): client.say(0, "/pkmode %d" % PK_CMD[picked])) buttons.append(button) row.add_child(button) var current: Variant = _cfg.get_value("gameopt", "pk_mode", -1) if int(current) in PK_CMD.values(): for i in PK_CMD.keys().size(): if int(PK_CMD[PK_CMD.keys()[i]]) == int(current): buttons[i].button_pressed = true parent.add_child(row) func _add_mobile_display_radios(parent: VBoxContainer) -> void: for group in RADIO_GROUPS: var row := HBoxContainer.new() row.custom_minimum_size = Vector2(640, 44) row.add_theme_constant_override("separation", 6) var key: String = String(DISPLAY_RADIOS[group[0]][0]) var caption := Label.new() caption.text = String(LABELS.get(key, key)) caption.custom_minimum_size = Vector2(126, 40) caption.vertical_alignment = VERTICAL_ALIGNMENT_CENTER caption.add_theme_font_size_override("font_size", 12) row.add_child(caption) var buttons: Array[Button] = [] var current := int(_display.get(key, 0)) for name in group: var button := Button.new() button.text = String(LABELS.get(name, name)) button.toggle_mode = true button.custom_minimum_size = Vector2(112, 42) button.button_pressed = int(DISPLAY_RADIOS[name][1]) == current var selected: String = name button.pressed.connect(func(): for other in buttons.size(): buttons[other].set_pressed_no_signal(buttons[other] == button) var spec: Array = DISPLAY_RADIOS[selected] _display[String(spec[0])] = int(spec[1]) _save() display_option_changed.emit(String(spec[0]), int(spec[1]))) buttons.append(button) row.add_child(button) parent.add_child(row) func _mobile_panel_style() -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = Color(0.04, 0.065, 0.1, 0.98) style.border_color = Color(0.52, 0.68, 0.9, 0.86) style.set_border_width_all(1) style.set_corner_radius_all(8) return style 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() display_option_changed.emit(String(spec[0]), int(spec[1]))) 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) var err := _cfg.save(config_path()) if err != OK: push_warning("GameOptionUI: save failed (%s): %s" % [err, config_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 _on_server_block_mode(mask: int) -> void: _block_mode = maxi(0, mask) _cfg.set_value("gameopt", "block_mode", _block_mode) _cfg.save(config_path()) _sync_block() func _save() -> void: for k in _display: _cfg.set_value("gameopt", k, int(_display[k])) _cfg.set_value("gameopt", "block_mode", _block_mode) var err := _cfg.save(config_path()) if err != OK: push_warning("GameOptionUI: save failed (%s): %s" % [err, config_path()])