Files
mtgodot-poc/project/ui/system_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

246 lines
7.7 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 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()
# --- 状态(= 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(占位)
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:
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("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 _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(CFG_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)
_cfg.save(CFG_PATH)