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
122 lines
5.0 KiB
GDScript
122 lines
5.0 KiB
GDScript
# system_option_ui_test —— P11 系统设置窗 headless 自检(真 uiscript + 假 audio/cam/env)。
|
|
# godot --headless --path project --script system_option_ui_test.gd
|
|
# 校验 systemoptiondialog.py 装载、音量 / 镜头 / 雾控件绑定(对照 uisystemoption.py)、
|
|
# 持久化到 user://system_option.cfg。缺 uiscript 资产时优雅跳过。
|
|
extends SceneTree
|
|
|
|
const SystemOptionUI = preload("res://ui/system_option_ui.gd")
|
|
|
|
class FakeAudio extends Node:
|
|
var master_bgm := 0.6
|
|
var master_sfx := 0.9
|
|
var _bgm := [null, null]
|
|
var _bgm_cur := 0
|
|
|
|
class FakeCam extends Node:
|
|
var max_dist := 20.0
|
|
var dist := 18.0
|
|
|
|
var _fail := 0
|
|
func _ck(c: bool, m: String) -> void:
|
|
if not c:
|
|
_fail += 1
|
|
printerr("FAIL: " + m)
|
|
|
|
func _init() -> void:
|
|
_run()
|
|
if _fail == 0:
|
|
print("PASS: system_option_ui_test (layout + volume/camera/fog bind + cfg persist)")
|
|
quit(0)
|
|
else:
|
|
printerr("%d check(s) failed" % _fail)
|
|
quit(1)
|
|
|
|
func _cfg_path() -> String:
|
|
return ProjectSettings.globalize_path("user://system_option.cfg")
|
|
|
|
func _run() -> void:
|
|
var assets := AssetRoot.path()
|
|
if not FileAccess.file_exists(assets.path_join("uiscript/uiscript/systemoptiondialog.py")):
|
|
print(" (skip: no systemoptiondialog.py — assets checkout missing)")
|
|
return
|
|
# 干净起步
|
|
if FileAccess.file_exists(_cfg_path()):
|
|
DirAccess.remove_absolute(_cfg_path())
|
|
|
|
var UiManager = load("res://ui/ui_manager.gd")
|
|
var ui: CanvasLayer = UiManager.new()
|
|
get_root().add_child(ui)
|
|
ui._ready()
|
|
var au := FakeAudio.new()
|
|
var cam := FakeCam.new()
|
|
get_root().add_child(au)
|
|
get_root().add_child(cam)
|
|
var env := Environment.new()
|
|
|
|
var so: Node = SystemOptionUI.new()
|
|
get_root().add_child(so)
|
|
so.setup(ui, assets, au, func(): return cam, func(): return env)
|
|
so.open()
|
|
_ck(so.is_open(), "system-option window opened")
|
|
if not so.is_open():
|
|
return
|
|
|
|
# --- 音量 sliderbar → snd.SetMusicVolume / SetSoundVolume ---
|
|
var mv: Control = so._node("music_volume_controller")
|
|
var sv: Control = so._node("sound_volume_controller")
|
|
_ck(mv is Range and sv is Range, "volume controllers are Range sliders")
|
|
_ck(is_equal_approx(mv.min_value, 0.0) and is_equal_approx(mv.max_value, 1.0),
|
|
"music slider range set to 0..1")
|
|
_ck(mv.value_changed.is_connected(so._on_music_volume), "music slider wired to _on_music_volume")
|
|
_ck(sv.value_changed.is_connected(so._on_sound_volume), "sound slider wired to _on_sound_volume")
|
|
so._on_music_volume(0.30) # = 拖动 music_volume_controller
|
|
_ck(is_equal_approx(au.master_bgm, 0.30), "music slider -> audio.master_bgm (got %s)" % au.master_bgm)
|
|
so._on_sound_volume(0.75)
|
|
_ck(is_equal_approx(au.master_sfx, 0.75), "sound slider -> audio.master_sfx (got %s)" % au.master_sfx)
|
|
|
|
# --- 镜头 근/원 radio → max_dist ---
|
|
(so._node("camera_short") as BaseButton).pressed.emit()
|
|
_ck(so.camera_mode == 0, "camera_short -> mode 0")
|
|
_ck(is_equal_approx(cam.max_dist, SystemOptionUI.CAMERA_MAX[0]), "camera short -> cam.max_dist=%s" % cam.max_dist)
|
|
_ck(cam.dist <= cam.max_dist, "camera dist clamped to new max")
|
|
(so._node("camera_long") as BaseButton).pressed.emit()
|
|
_ck(so.camera_mode == 1 and is_equal_approx(cam.max_dist, SystemOptionUI.CAMERA_MAX[1]), "camera_long -> mode 1 / far max")
|
|
# 라디오 배타성
|
|
_ck(not (so._node("camera_short") as BaseButton).button_pressed, "camera_short unset after camera_long")
|
|
|
|
# --- 안개 0/1/2 radio → Environment.fog_density ---
|
|
(so._node("fog_level0") as BaseButton).pressed.emit()
|
|
_ck(so.fog_level == 0 and env.fog_enabled
|
|
and is_equal_approx(env.fog_density, SystemOptionUI.FOG_DENSITY[0]),
|
|
"fog_level0 (dense) -> env.fog_density=%s" % env.fog_density)
|
|
(so._node("fog_level2") as BaseButton).pressed.emit()
|
|
_ck(so.fog_level == 2 and is_equal_approx(env.fog_density, SystemOptionUI.FOG_DENSITY[2]),
|
|
"fog_level2 (light) -> env.fog_density=%s" % env.fog_density)
|
|
|
|
# --- 持久화: user://system_option.cfg ---
|
|
_ck(FileAccess.file_exists(_cfg_path()), "cfg written")
|
|
var cfg := ConfigFile.new()
|
|
cfg.load(_cfg_path())
|
|
_ck(is_equal_approx(float(cfg.get_value("audio", "music_volume", -1.0)), 0.30), "cfg music_volume persisted")
|
|
_ck(int(cfg.get_value("video", "fog_level", -1)) == 2, "cfg fog_level persisted")
|
|
_ck(int(cfg.get_value("video", "camera_mode", -1)) == 1, "cfg camera_mode persisted")
|
|
|
|
so.close()
|
|
_ck(not so.is_open(), "system-option window closed")
|
|
|
|
# --- 두 번째 인스턴스가 cfg 를 읽어 즉시 적용 ---
|
|
var au2 := FakeAudio.new()
|
|
var cam2 := FakeCam.new()
|
|
get_root().add_child(au2)
|
|
get_root().add_child(cam2)
|
|
var env2 := Environment.new()
|
|
var so2: Node = SystemOptionUI.new()
|
|
get_root().add_child(so2)
|
|
so2.setup(ui, assets, au2, func(): return cam2, func(): return env2)
|
|
_ck(is_equal_approx(au2.master_bgm, 0.30), "reload: cfg music_volume applied on setup")
|
|
_ck(is_equal_approx(cam2.max_dist, SystemOptionUI.CAMERA_MAX[1]), "reload: cfg camera_mode applied")
|
|
_ck(is_equal_approx(env2.fog_density, SystemOptionUI.FOG_DENSITY[2]), "reload: cfg fog_level applied")
|
|
|
|
if FileAccess.file_exists(_cfg_path()):
|
|
DirAccess.remove_absolute(_cfg_path())
|