Files
mtgodot-poc/project/ui/system_option_ui.gd
T

363 lines
12 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.
# 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()])