# SystemOptionUI (P11) —— 系统设置窗(1:1 迁移 `assets/root/uisystemoption.py` `OptionDialog`)。 # # var so := preload("res://ui/system_option_ui.gd").new() # add_child(so) # so.setup(ui_manager, assets_root, audio_node, func(): return cam, func(): return env) # so.toggle() # ESC(无其它窗打开时) # # 布局走真 `assets/uiscript/uiscript/systemoptiondialog.py`(UiScript → UiBuild)。 # 控件绑定逐字对照 uisystemoption.py: # music_volume_controller → snd.SetMusicVolume + systemSetting.SetMusicVolume # sound_volume_controller → snd.SetSoundVolume + systemSetting.SetSoundVolume # camera_short / camera_long → systemSetting 镜头最大距离(近/远) # fog_level0 / 1 / 2 (浓/中/淡) → systemSetting.SetFogLevel(0..2) # tiling_cpu / tiling_gpu / apply → 原为 CPU/GPU 分块渲染,Godot 渲染器无对应,保留为占位 # 设置持久化到 `user://system_option.cfg`(= 原 systemSetting 的配置文件)。 extends Node const MobileTouchButton = preload("res://ui/mobile/mobile_touch_button.gd") const CFG_PATH := "user://system_option.cfg" const CAMERA_MAX := [11.0, 20.0] # 近 / 远(game_camera.max_dist) const FOG_DENSITY := [0.055, 0.018, 0.004] # 浓 / 中 / 淡(Environment.fog_density) # uiScriptLocale 未接全局 Locale 时的中文兜底 const LABELS := { "titlename": "系统设置", "music_name": "背景音乐", "sound_name": "音效", "bgm_button": "更换", "bgm_file": "默认主题", "camera_mode": "镜头距离", "camera_short": "近", "camera_long": "远", "fog_mode": "雾", "fog_level0": "浓", "fog_level1": "中", "fog_level2": "淡", "tiling_mode": "图形内存", "tiling_cpu": "CPU", "tiling_gpu": "GPU", "tiling_apply": "应用", } var ui: CanvasLayer var assets_root := "" var uiscript_dir := "" var audio: Node var _cam_get: Callable = Callable() var _env_get: Callable = Callable() var _win: Dictionary = {} var _cfg := ConfigFile.new() var _mobile_mode := false var _mobile_root: Control # --- 状态(= systemSetting 值)--- var music_volume := 0.6 var sound_volume := 0.9 var camera_mode := 1 # 0 近, 1 远 var fog_level := 1 # 0 浓, 1 中, 2 淡 var tiling_mode := 1 # 0 CPU, 1 GPU(占位) 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") # Godot headless 在受限执行环境里可能无法写 user://;测试配置放在系统 # 临时目录,正常图形客户端仍使用 user://。 return OS.get_temp_dir().path_join("mtgodot_system_option.cfg") \ if DisplayServer.get_name() == "headless" else CFG_PATH func setup(ui_manager: CanvasLayer, assets: String, audio_node: Node = null, cam_getter := Callable(), env_getter := Callable()) -> void: ui = ui_manager 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") audio = audio_node _cam_get = cam_getter _env_get = env_getter _load_cfg() _apply_all() # 持久化的设置在开窗前就生效 # --- open / close --------------------------------------------------- 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 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("systemoptiondialog.py") if not FileAccess.file_exists(path): push_warning("SystemOptionUI: no systemoptiondialog.py at " + path) return _win = ui.open_script(path, assets_root) if not is_open(): return _relabel() _wire() _sync_controls() func _open_mobile() -> void: if is_open(): return _mobile_root = Control.new() _mobile_root.name = "MobileSystemOptionWindow" _mobile_root.size = Vector2(680, 340) _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 content := VBoxContainer.new() content.position = Vector2(18, 50) content.size = Vector2(644, 270) content.add_theme_constant_override("separation", 8) _mobile_root.add_child(content) _add_mobile_slider(content, "背景音乐", music_volume, func(v: float): _on_music_volume(v)) _add_mobile_slider(content, "音效", sound_volume, func(v: float): _on_sound_volume(v)) _add_mobile_radio(content, "镜头距离", ["近", "远"], camera_mode, func(i: int): camera_mode = i; _apply_camera(); _save()) _add_mobile_radio(content, "雾", ["浓", "中", "淡"], fog_level, func(i: int): fog_level = i; _apply_fog(); _save()) _add_mobile_radio(content, "图形内存", ["CPU", "GPU"], tiling_mode, func(i: int): tiling_mode = i; _save()) var hint := Label.new() hint.text = "设置会自动保存;图形内存选项将在下次启动时使用" hint.add_theme_font_size_override("font_size", 11) hint.add_theme_color_override("font_color", Color(0.62, 0.72, 0.84)) content.add_child(hint) ui.open(_mobile_root) func _add_mobile_slider(parent: VBoxContainer, caption: String, initial: float, changed: Callable) -> void: var row := HBoxContainer.new() row.custom_minimum_size = Vector2(640, 42) var label := Label.new() label.text = caption label.custom_minimum_size = Vector2(92, 38) label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.add_theme_font_size_override("font_size", 13) row.add_child(label) var slider := HSlider.new() slider.min_value = 0.0 slider.max_value = 1.0 slider.step = 0.01 slider.value = initial slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL slider.custom_minimum_size = Vector2(500, 38) slider.value_changed.connect(changed) row.add_child(slider) parent.add_child(row) func _add_mobile_radio(parent: VBoxContainer, caption: String, choices: Array, active: int, picked: Callable) -> void: var row := HBoxContainer.new() row.custom_minimum_size = Vector2(640, 44) var label := Label.new() label.text = caption label.custom_minimum_size = Vector2(92, 40) label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.add_theme_font_size_override("font_size", 13) row.add_child(label) var buttons: Array[Button] = [] for i in choices.size(): var button := Button.new() button.text = String(choices[i]) button.toggle_mode = true button.button_pressed = i == active button.custom_minimum_size = Vector2(100, 40) var idx := i button.pressed.connect(func(): for other in buttons.size(): buttons[other].set_pressed_no_signal(other == idx) picked.call(idx)) 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 # uiScriptLocale 兜底:把 `` 文本换成中文 func _relabel() -> void: for nm in LABELS: var n := _node(nm) if n and n.has_method("set_text"): n.set_text(LABELS[nm]) # --- wiring (uisystemoption.__Load) --------------------------------- func _wire() -> void: var mv := _node("music_volume_controller") if mv is Range: mv.min_value = 0.0 mv.max_value = 1.0 mv.step = 0.01 mv.value = music_volume mv.value_changed.connect(_on_music_volume) var sv := _node("sound_volume_controller") if sv is Range: sv.min_value = 0.0 sv.max_value = 1.0 sv.step = 0.01 sv.value = sound_volume sv.value_changed.connect(_on_sound_volume) _bind_radio(["camera_short", "camera_long"], func(i): camera_mode = i _apply_camera() _save()) _bind_radio(["fog_level0", "fog_level1", "fog_level2"], func(i): fog_level = i _apply_fog() _save()) _bind_radio(["tiling_cpu", "tiling_gpu"], func(i): tiling_mode = i _save()) var apply_btn := _node("tiling_apply") if apply_btn is BaseButton: apply_btn.pressed.connect(func(): # 原 __OnClickTilingApplyButton 会重启客户端应用 CPU/GPU 分块; # Godot 渲染器无此项,仅记录选择。 _save()) var bgm_btn := _node("bgm_button") if bgm_btn is BaseButton: bgm_btn.disabled = true # 换 BGM 需音乐文件选择器,暂缺 func _bind_radio(names: Array, on_pick: Callable) -> void: var boxes: Array = [] for nm in names: boxes.append(_node(nm)) for i in boxes.size(): var b = boxes[i] if not (b is BaseButton): continue b.toggle_mode = true var idx := i b.pressed.connect(func(): for j in boxes.size(): if boxes[j] is BaseButton: boxes[j].set_pressed_no_signal(j == idx) on_pick.call(idx)) # 把当前状态回填到控件(开窗时) func _sync_controls() -> void: _set_radio(["camera_short", "camera_long"], camera_mode) _set_radio(["fog_level0", "fog_level1", "fog_level2"], fog_level) _set_radio(["tiling_cpu", "tiling_gpu"], tiling_mode) func _set_radio(names: Array, active: int) -> void: for i in names.size(): var b = _node(names[i]) if b is BaseButton: b.set_pressed_no_signal(i == active) # --- change handlers ---------------------------------------------------- func _on_music_volume(v: float) -> void: music_volume = clampf(v, 0.0, 1.0) _apply_music() _save() func _on_sound_volume(v: float) -> void: sound_volume = clampf(v, 0.0, 1.0) _apply_sound() _save() # --- apply to engine -------------------------------------------------- func _apply_all() -> void: _apply_music() _apply_sound() _apply_camera() _apply_fog() func _apply_music() -> void: if audio == null: return if "master_bgm" in audio: audio.master_bgm = music_volume # 已在播放的 BGM 声道即时跟随 if "_bgm" in audio and "_bgm_cur" in audio: var pl = audio._bgm[audio._bgm_cur] if is_instance_valid(pl): pl.volume_db = linear_to_db(maxf(music_volume, 0.0001)) func _apply_sound() -> void: if audio and "master_sfx" in audio: audio.master_sfx = sound_volume # play_ui / play_at 每次读取 func _apply_camera() -> void: if not _cam_get.is_valid(): return var c = _cam_get.call() if c == null: return var m: float = CAMERA_MAX[clampi(camera_mode, 0, 1)] if "max_dist" in c: c.max_dist = m if "dist" in c: c.dist = minf(c.dist, m) func _apply_fog() -> void: if not _env_get.is_valid(): return var e = _env_get.call() if not (e is Environment): return e.fog_enabled = true e.fog_density = FOG_DENSITY[clampi(fog_level, 0, 2)] # --- persistence (= systemSetting 配置文件) -------------------------- func _load_cfg() -> void: if _cfg.load(config_path()) != OK: return music_volume = float(_cfg.get_value("audio", "music_volume", music_volume)) sound_volume = float(_cfg.get_value("audio", "sound_volume", sound_volume)) camera_mode = int(_cfg.get_value("video", "camera_mode", camera_mode)) fog_level = int(_cfg.get_value("video", "fog_level", fog_level)) tiling_mode = int(_cfg.get_value("video", "tiling_mode", tiling_mode)) func _save() -> void: _cfg.set_value("audio", "music_volume", music_volume) _cfg.set_value("audio", "sound_volume", sound_volume) _cfg.set_value("video", "camera_mode", camera_mode) _cfg.set_value("video", "fog_level", fog_level) _cfg.set_value("video", "tiling_mode", tiling_mode) var err := _cfg.save(config_path()) if err != OK: push_warning("SystemOptionUI: save failed (%s): %s" % [err, config_path()])