363 lines
12 KiB
GDScript
363 lines
12 KiB
GDScript
# 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 兜底:把 `<KEY>` 文本换成中文
|
||
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()])
|