792 lines
34 KiB
GDScript
792 lines
34 KiB
GDScript
class_name ABGEntryShell
|
||
extends Control
|
||
|
||
const BootService = preload("res://entry/boot_service.gd")
|
||
const SettingsStore = preload("res://entry/settings_store.gd")
|
||
const GUILD_BACKGROUND = preload("res://assets/backgrounds/p9_3_guild_hall.png")
|
||
const UI_FONT = preload("res://assets/fonts/NotoSansSC.ttf")
|
||
|
||
signal boot_completed
|
||
|
||
@export var auto_start := true
|
||
var boot_service := BootService.new()
|
||
var settings_store := SettingsStore.new()
|
||
var settings: Dictionary = {}
|
||
var setup_draft: Dictionary = {}
|
||
var boot_snapshot: Dictionary = {}
|
||
var boot_generation := 0
|
||
var status_label: Label
|
||
var detail_label: Label
|
||
var progress_bar: ProgressBar
|
||
var retry_button: Button
|
||
var diagnostics_label: Label
|
||
var current_screen := "boot"
|
||
var title_notice := ""
|
||
var settings_draft: Dictionary = {}
|
||
var settings_category := "display"
|
||
var settings_notice := ""
|
||
|
||
|
||
func _ready() -> void:
|
||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
_build_boot_screen()
|
||
if auto_start:
|
||
call_deferred("start_boot")
|
||
|
||
|
||
func start_boot() -> void:
|
||
boot_generation += 1
|
||
var generation := boot_generation
|
||
boot_snapshot = boot_service.begin()
|
||
_render_boot_state()
|
||
while generation == boot_generation and String(boot_snapshot.state) == "running":
|
||
await get_tree().process_frame
|
||
boot_snapshot = boot_service.verify_next()
|
||
_render_boot_state()
|
||
if generation == boot_generation and String(boot_snapshot.state) == "complete":
|
||
await get_tree().create_timer(0.18).timeout
|
||
if generation == boot_generation:
|
||
boot_completed.emit()
|
||
_open_after_boot()
|
||
|
||
|
||
func retry_boot() -> void:
|
||
if String(boot_snapshot.get("state", "")) != "error":
|
||
return
|
||
start_boot()
|
||
|
||
|
||
func _build_boot_screen() -> void:
|
||
for child in get_children():
|
||
child.free()
|
||
|
||
var background := TextureRect.new()
|
||
background.name = "EntryBackground"
|
||
background.texture = GUILD_BACKGROUND
|
||
background.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
background.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
|
||
background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
background.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(background)
|
||
|
||
var shade := ColorRect.new()
|
||
shade.name = "EntryShade"
|
||
shade.color = Color(0.035, 0.07, 0.10, 0.40)
|
||
shade.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
shade.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(shade)
|
||
|
||
var safe := MarginContainer.new()
|
||
safe.name = "EntrySafeArea"
|
||
safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
safe.add_theme_constant_override("margin_left", 56)
|
||
safe.add_theme_constant_override("margin_right", 56)
|
||
safe.add_theme_constant_override("margin_top", 40)
|
||
safe.add_theme_constant_override("margin_bottom", 40)
|
||
add_child(safe)
|
||
|
||
var layout := VBoxContainer.new()
|
||
layout.name = "BootLayout"
|
||
layout.alignment = BoxContainer.ALIGNMENT_END
|
||
layout.add_theme_constant_override("separation", 10)
|
||
safe.add_child(layout)
|
||
|
||
var identity := Label.new()
|
||
identity.name = "BootIdentity"
|
||
identity.text = "AETHERBOUND GUILD"
|
||
identity.add_theme_font_override("font", UI_FONT)
|
||
identity.add_theme_font_size_override("font_size", 34)
|
||
identity.add_theme_color_override("font_color", Color("#f3f6f2"))
|
||
identity.add_theme_color_override("font_shadow_color", Color(0.05, 0.08, 0.10, 0.8))
|
||
identity.add_theme_constant_override("shadow_offset_x", 2)
|
||
identity.add_theme_constant_override("shadow_offset_y", 2)
|
||
layout.add_child(identity)
|
||
|
||
status_label = Label.new()
|
||
status_label.name = "BootStatus"
|
||
status_label.add_theme_font_override("font", UI_FONT)
|
||
status_label.add_theme_font_size_override("font_size", 20)
|
||
status_label.add_theme_color_override("font_color", Color("#f3f6f2"))
|
||
layout.add_child(status_label)
|
||
|
||
detail_label = Label.new()
|
||
detail_label.name = "BootDetail"
|
||
detail_label.add_theme_font_override("font", UI_FONT)
|
||
detail_label.add_theme_font_size_override("font_size", 16)
|
||
detail_label.add_theme_color_override("font_color", Color("#d7e0dd"))
|
||
detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
layout.add_child(detail_label)
|
||
|
||
progress_bar = ProgressBar.new()
|
||
progress_bar.name = "BootProgress"
|
||
progress_bar.custom_minimum_size = Vector2(420, 12)
|
||
progress_bar.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||
progress_bar.show_percentage = false
|
||
layout.add_child(progress_bar)
|
||
|
||
retry_button = Button.new()
|
||
retry_button.name = "BootRetry"
|
||
retry_button.text = "Retry verification"
|
||
retry_button.custom_minimum_size = Vector2(240, 52)
|
||
retry_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||
retry_button.add_theme_font_override("font", UI_FONT)
|
||
retry_button.add_theme_font_size_override("font_size", 18)
|
||
retry_button.pressed.connect(retry_boot)
|
||
layout.add_child(retry_button)
|
||
|
||
diagnostics_label = Label.new()
|
||
diagnostics_label.name = "BootDiagnostics"
|
||
diagnostics_label.add_theme_font_override("font", UI_FONT)
|
||
diagnostics_label.add_theme_font_size_override("font_size", 14)
|
||
diagnostics_label.add_theme_color_override("font_color", Color("#ffd4d4"))
|
||
layout.add_child(diagnostics_label)
|
||
|
||
|
||
func _render_boot_state() -> void:
|
||
if status_label == null:
|
||
return
|
||
var state := String(boot_snapshot.get("state", "running"))
|
||
var step := String(boot_snapshot.get("step", "resources"))
|
||
progress_bar.max_value = max(1, int(boot_snapshot.get("total", 1)))
|
||
progress_bar.value = int(boot_snapshot.get("completed", 0))
|
||
retry_button.visible = state == "error"
|
||
diagnostics_label.visible = state == "error"
|
||
match state:
|
||
"error":
|
||
status_label.text = "Verification needs attention"
|
||
detail_label.text = "Local data was left unchanged. Resolve the issue, then retry this check."
|
||
diagnostics_label.text = "Diagnostic: %s" % String(boot_snapshot.get("code", "UNKNOWN"))
|
||
"complete":
|
||
status_label.text = "Local verification complete"
|
||
detail_label.text = "Resources, settings storage and save access are ready."
|
||
diagnostics_label.text = ""
|
||
_:
|
||
status_label.text = "Preparing the guild threshold"
|
||
detail_label.text = _step_text(step)
|
||
diagnostics_label.text = ""
|
||
|
||
|
||
func _step_text(step: String) -> String:
|
||
match step:
|
||
"resources": return "Checking required local resources..."
|
||
"storage": return "Checking local storage access..."
|
||
"preferences": return "Checking display and accessibility settings..."
|
||
"save": return "Checking the latest local run record..."
|
||
return "Completing local verification..."
|
||
|
||
|
||
func _open_after_boot() -> void:
|
||
var loaded: Dictionary = settings_store.load_settings()
|
||
if not loaded.ok:
|
||
boot_snapshot = {"state": "error", "step": "preferences", "code": String(loaded.code), "completed": 0, "total": 1}
|
||
_build_boot_screen()
|
||
_render_boot_state()
|
||
return
|
||
settings = loaded.settings.duplicate(true)
|
||
if bool(settings.setup_completed):
|
||
show_title()
|
||
else:
|
||
show_first_launch()
|
||
|
||
|
||
func show_first_launch() -> void:
|
||
setup_draft = settings.duplicate(true) if not settings.is_empty() else settings_store.defaults()
|
||
_build_world_base()
|
||
var safe := MarginContainer.new()
|
||
safe.name = "FirstLaunchSafeArea"
|
||
safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
var margin := int(setup_draft.safe_area) + 24
|
||
for side in ["left", "right", "top", "bottom"]:
|
||
safe.add_theme_constant_override("margin_%s" % side, margin)
|
||
add_child(safe)
|
||
var row := HBoxContainer.new()
|
||
row.name = "FirstLaunchLayout"
|
||
row.add_theme_constant_override("separation", 28)
|
||
safe.add_child(row)
|
||
|
||
var preview := VBoxContainer.new()
|
||
preview.name = "PaintedPreview"
|
||
preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
preview.alignment = BoxContainer.ALIGNMENT_END
|
||
row.add_child(preview)
|
||
preview.add_child(_label(_tr("公会门槛", "GUILD THRESHOLD"), 18, Color("#d7e0dd")))
|
||
var preview_title := _label(_tr("裂脉远征团", "AETHERBOUND GUILD"), _scaled(34), Color("#f3f6f2"))
|
||
preview_title.name = "SetupPreviewTitle"
|
||
preview_title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
preview.add_child(preview_title)
|
||
preview.add_child(_label(_tr("字体、边距与字幕会在这里即时预览。", "Text, safe margins and captions preview here."), _scaled(16), Color("#f3f6f2")))
|
||
|
||
var controls := VBoxContainer.new()
|
||
controls.name = "FirstLaunchControls"
|
||
controls.custom_minimum_size = Vector2(390, 0)
|
||
controls.add_theme_constant_override("separation", 5)
|
||
row.add_child(controls)
|
||
controls.add_child(_label(_tr("首次启动设置", "FIRST-LAUNCH SETUP"), _scaled(24), Color("#f3f6f2")))
|
||
controls.add_child(_label(_tr("这些选项只影响显示与辅助功能。", "These choices affect presentation and accessibility only."), _scaled(14), Color("#d7e0dd")))
|
||
var grid := GridContainer.new()
|
||
grid.name = "SetupGrid"
|
||
grid.columns = 2
|
||
grid.add_theme_constant_override("h_separation", 8)
|
||
grid.add_theme_constant_override("v_separation", 5)
|
||
controls.add_child(grid)
|
||
grid.add_child(_choice_button("SetupLanguage", _tr("语言", "Language"), _tr("简体中文", "English"), toggle_setup_locale))
|
||
grid.add_child(_choice_button("SetupSafeArea", _tr("安全区", "Safe area"), "%d px" % int(setup_draft.safe_area), cycle_setup_safe_area))
|
||
grid.add_child(_choice_button("SetupTextScale", _tr("字体", "Text"), "%d%%" % int(round(float(setup_draft.text_scale) * 100.0)), cycle_setup_text_scale))
|
||
grid.add_child(_check_button("SetupReducedMotion", _tr("减少动画", "Reduced motion"), bool(setup_draft.reduced_motion), set_setup_reduced_motion))
|
||
grid.add_child(_check_button("SetupReducedFlashes", _tr("减少闪烁", "Reduced flashes"), bool(setup_draft.reduced_flashes), set_setup_reduced_flashes))
|
||
grid.add_child(_check_button("SetupCaptions", _tr("字幕与事件标签", "Captions"), bool(setup_draft.captions), set_setup_captions))
|
||
var apply := Button.new()
|
||
apply.name = "SetupApply"
|
||
apply.text = _tr("应用并进入标题首页", "Apply and continue to Title")
|
||
apply.custom_minimum_size = Vector2(0, 52)
|
||
apply.add_theme_font_override("font", UI_FONT)
|
||
apply.add_theme_font_size_override("font_size", _scaled(17))
|
||
apply.pressed.connect(apply_first_launch)
|
||
controls.add_child(apply)
|
||
var error := _label("", _scaled(13), Color("#ffd4d4"))
|
||
error.name = "SetupError"
|
||
controls.add_child(error)
|
||
|
||
|
||
func toggle_setup_locale() -> void:
|
||
setup_draft.locale = "en" if String(setup_draft.locale) == "zh_CN" else "zh_CN"
|
||
settings = setup_draft.duplicate(true)
|
||
show_first_launch()
|
||
|
||
|
||
func cycle_setup_safe_area() -> void:
|
||
var options := SettingsStore.SAFE_AREA_PRESETS
|
||
var next := (options.find(int(setup_draft.safe_area)) + 1) % options.size()
|
||
setup_draft.safe_area = options[next]
|
||
settings = setup_draft.duplicate(true)
|
||
show_first_launch()
|
||
|
||
|
||
func cycle_setup_text_scale() -> void:
|
||
var options := SettingsStore.TEXT_SCALES
|
||
var current := 0
|
||
for index in range(options.size()):
|
||
if is_equal_approx(float(setup_draft.text_scale), float(options[index])):
|
||
current = index
|
||
setup_draft.text_scale = options[(current + 1) % options.size()]
|
||
settings = setup_draft.duplicate(true)
|
||
show_first_launch()
|
||
|
||
|
||
func set_setup_reduced_motion(value: bool) -> void:
|
||
setup_draft.reduced_motion = value
|
||
|
||
|
||
func set_setup_reduced_flashes(value: bool) -> void:
|
||
setup_draft.reduced_flashes = value
|
||
|
||
|
||
func set_setup_captions(value: bool) -> void:
|
||
setup_draft.captions = value
|
||
|
||
|
||
func apply_first_launch() -> void:
|
||
setup_draft.setup_completed = true
|
||
var saved: Dictionary = settings_store.save_settings(setup_draft)
|
||
if not saved.ok:
|
||
var error := get_node_or_null("FirstLaunchSafeArea/FirstLaunchLayout/FirstLaunchControls/SetupError")
|
||
if error != null:
|
||
error.text = _tr("无法保存设置。请重试。", "Settings could not be saved. Retry.") + " [%s]" % String(saved.code)
|
||
return
|
||
settings = saved.settings.duplicate(true)
|
||
show_title()
|
||
|
||
|
||
func show_title() -> void:
|
||
current_screen = "title"
|
||
_build_world_base()
|
||
var safe := MarginContainer.new()
|
||
safe.name = "TitleSafeArea"
|
||
safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
var margin := int(settings.get("safe_area", 24)) + 32
|
||
safe.add_theme_constant_override("margin_left", margin)
|
||
safe.add_theme_constant_override("margin_right", margin)
|
||
safe.add_theme_constant_override("margin_top", margin)
|
||
safe.add_theme_constant_override("margin_bottom", margin)
|
||
add_child(safe)
|
||
var layout := VBoxContainer.new()
|
||
layout.name = "TitleLayout"
|
||
layout.alignment = BoxContainer.ALIGNMENT_END
|
||
safe.add_child(layout)
|
||
var tools := HBoxContainer.new()
|
||
tools.name = "TitleTools"
|
||
tools.alignment = BoxContainer.ALIGNMENT_END
|
||
tools.add_theme_constant_override("separation", 8)
|
||
tools.add_child(_tool_button("TitleSettings", _tr("设置", "Settings"), show_settings_placeholder))
|
||
tools.add_child(_tool_button("TitleCredits", _tr("制作人员", "Credits"), show_credits_placeholder))
|
||
layout.add_child(tools)
|
||
var spacer := Control.new()
|
||
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
layout.add_child(spacer)
|
||
var copy := VBoxContainer.new()
|
||
copy.name = "TitleCopy"
|
||
copy.custom_minimum_size = Vector2(440, 0)
|
||
copy.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||
copy.add_theme_constant_override("separation", 6)
|
||
layout.add_child(copy)
|
||
copy.add_child(_label(_tr("风路公会 · 远征入口", "WIND-ROAD GUILD · EXPEDITION THRESHOLD"), _scaled(14), Color("#d7e0dd")))
|
||
copy.add_child(_label(_tr("裂脉远征团", "AETHERBOUND GUILD"), _scaled(38), Color("#f3f6f2")))
|
||
var save_status := _save_status()
|
||
var summary := _label(
|
||
_tr("发现可恢复的本地远征记录", "A local expedition record is available")
|
||
if save_status == "valid" else _tr("尚无远征记录 · 从新游戏开始", "No expedition record · begin with New Game"),
|
||
_scaled(15),
|
||
Color("#d7e0dd")
|
||
)
|
||
summary.name = "TitleSaveSummary"
|
||
copy.add_child(summary)
|
||
var actions := HBoxContainer.new()
|
||
actions.name = "TitleActions"
|
||
actions.add_theme_constant_override("separation", 10)
|
||
copy.add_child(actions)
|
||
var new_game := _action_button("TitleNewGame", _tr("新游戏", "New Game"), open_new_game_boundary)
|
||
actions.add_child(new_game)
|
||
var continue_button := _action_button("TitleContinue", _tr("继续游戏", "Continue"), open_continue_boundary)
|
||
continue_button.disabled = save_status != "valid"
|
||
continue_button.tooltip_text = _tr("没有可继续的本地远征记录", "No local expedition is available") if continue_button.disabled else _tr("查看可恢复的远征记录", "Review the resumable expedition")
|
||
actions.add_child(continue_button)
|
||
var reason := _label(
|
||
_tr("继续游戏:没有可恢复的本地远征记录", "Continue: no resumable local expedition")
|
||
if continue_button.disabled else _tr("继续游戏会先进入恢复确认,不会立即推进模拟。", "Continue opens resume review before simulation advances."),
|
||
_scaled(13),
|
||
Color("#d7e0dd")
|
||
)
|
||
reason.name = "TitleContinueReason"
|
||
copy.add_child(reason)
|
||
var notice := _label(title_notice, _scaled(13), Color("#ffe1a6"))
|
||
notice.name = "TitleNotice"
|
||
notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
copy.add_child(notice)
|
||
|
||
|
||
func open_new_game_boundary() -> void:
|
||
title_notice = _tr("新游戏入口已就绪;四选二创建流程属于冻结的 DEV-2,当前不会创建或覆盖存档。", "New Game is ready; the choose-two creation flow is frozen for DEV-2, so no save is created or replaced here.")
|
||
show_title()
|
||
|
||
|
||
func open_continue_boundary() -> void:
|
||
if _save_status() != "valid":
|
||
title_notice = _tr("没有可恢复的本地远征记录。", "No resumable local expedition is available.")
|
||
else:
|
||
title_notice = _tr("本地远征记录保持不变;恢复确认属于后续存档页面。", "The local expedition remains unchanged; Resume Review belongs to the later save pages.")
|
||
show_title()
|
||
|
||
|
||
func show_settings_placeholder() -> void:
|
||
show_settings()
|
||
|
||
|
||
func show_credits_placeholder() -> void:
|
||
show_credits()
|
||
|
||
|
||
func show_settings(category := "") -> void:
|
||
current_screen = "settings"
|
||
if settings_draft.is_empty() or not category.is_empty():
|
||
settings_draft = settings.duplicate(true)
|
||
if not category.is_empty():
|
||
settings_category = category
|
||
_build_world_base()
|
||
var safe := MarginContainer.new()
|
||
safe.name = "SettingsSafeArea"
|
||
safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
var margin := int(settings_draft.get("safe_area", 24)) + 20
|
||
safe.add_theme_constant_override("margin_left", margin)
|
||
safe.add_theme_constant_override("margin_right", margin)
|
||
safe.add_theme_constant_override("margin_top", 24)
|
||
safe.add_theme_constant_override("margin_bottom", 24)
|
||
add_child(safe)
|
||
var outer := VBoxContainer.new()
|
||
outer.name = "SettingsOuter"
|
||
outer.add_theme_constant_override("separation", 8)
|
||
safe.add_child(outer)
|
||
var heading := HBoxContainer.new()
|
||
heading.name = "SettingsHeading"
|
||
outer.add_child(heading)
|
||
var title := _label(_tr("设置", "SETTINGS"), _scaled(26), Color("#f3f6f2"))
|
||
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
heading.add_child(title)
|
||
heading.add_child(_label(_tr("来源:标题首页", "Caller: Title"), _scaled(13), Color("#d7e0dd")))
|
||
var workspace := HBoxContainer.new()
|
||
workspace.name = "SettingsWorkspace"
|
||
workspace.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
workspace.add_theme_constant_override("separation", 14)
|
||
outer.add_child(workspace)
|
||
var rail := VBoxContainer.new()
|
||
rail.name = "SettingsCategories"
|
||
rail.custom_minimum_size = Vector2(174, 0)
|
||
rail.add_theme_constant_override("separation", 4)
|
||
workspace.add_child(rail)
|
||
for row in [
|
||
["display", _tr("显示与文字", "Display & Text")],
|
||
["motion", _tr("动态与省电", "Motion & Power")],
|
||
["audio", _tr("音频与字幕", "Audio & Captions")],
|
||
["controls", _tr("控制", "Controls")],
|
||
["language", _tr("语言", "Language")],
|
||
["credits", _tr("制作人员与数据", "Credits & Data")],
|
||
]:
|
||
var button := _tool_button("SettingsCategory_%s" % row[0], row[1], open_settings_category.bind(row[0]))
|
||
button.custom_minimum_size = Vector2(174, 44)
|
||
button.disabled = settings_category == row[0]
|
||
rail.add_child(button)
|
||
var scroll := ScrollContainer.new()
|
||
scroll.name = "SettingsScroll"
|
||
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
workspace.add_child(scroll)
|
||
var content := VBoxContainer.new()
|
||
content.name = "SettingsContent"
|
||
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
content.add_theme_constant_override("separation", 6)
|
||
scroll.add_child(content)
|
||
_build_settings_category(content)
|
||
var footer := HBoxContainer.new()
|
||
footer.name = "SettingsFooter"
|
||
footer.add_theme_constant_override("separation", 8)
|
||
outer.add_child(footer)
|
||
var notice := _label(settings_notice, _scaled(13), Color("#ffe1a6"))
|
||
notice.name = "SettingsNotice"
|
||
notice.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
footer.add_child(notice)
|
||
footer.add_child(_tool_button("SettingsBack", _tr("返回", "Back"), request_settings_back))
|
||
footer.add_child(_tool_button("SettingsDiscard", _tr("放弃并返回", "Discard & Back"), discard_settings))
|
||
footer.add_child(_action_button("SettingsApply", _tr("应用", "Apply"), apply_settings))
|
||
|
||
|
||
func _build_settings_category(content: VBoxContainer) -> void:
|
||
match settings_category:
|
||
"display":
|
||
content.add_child(_settings_heading(_tr("显示与文字", "DISPLAY & TEXT")))
|
||
content.add_child(_choice_button("SettingSafeArea", _tr("安全区域", "Safe area"), "%d px" % int(settings_draft.safe_area), cycle_setting_safe_area))
|
||
content.add_child(_choice_button("SettingTextScale", _tr("文字大小", "Text size"), "%d%%" % int(round(float(settings_draft.text_scale) * 100.0)), cycle_setting_text_scale))
|
||
content.add_child(_slider_row("SettingBrightness", _tr("亮度", "Brightness"), float(settings_draft.brightness), 0.7, 1.2, 0.1, set_brightness))
|
||
content.add_child(_check_button("SettingHighContrast", _tr("高对比轮廓", "High-contrast contours"), bool(settings_draft.high_contrast), set_high_contrast))
|
||
content.add_child(_check_button("SettingColorCues", _tr("颜色以外的形状提示", "Shape cues beyond color"), bool(settings_draft.color_cues), set_color_cues))
|
||
"motion":
|
||
content.add_child(_settings_heading(_tr("动态、闪烁与省电", "MOTION, FLASHES & POWER")))
|
||
content.add_child(_check_button("SettingReducedMotion", _tr("减少动画", "Reduced motion"), bool(settings_draft.reduced_motion), set_reduced_motion))
|
||
content.add_child(_check_button("SettingReducedFlashes", _tr("减少闪烁", "Reduced flashes"), bool(settings_draft.reduced_flashes), set_reduced_flashes))
|
||
content.add_child(_slider_row("SettingShake", _tr("镜头震动", "Camera shake"), float(settings_draft.shake_intensity), 0.0, 1.0, 0.25, set_shake))
|
||
content.add_child(_check_button("SettingHitPause", _tr("命中停顿", "Hit pause"), bool(settings_draft.hit_pause), set_hit_pause))
|
||
content.add_child(_choice_button("SettingParticles", _tr("粒子密度", "Particles"), _tr("低", "Low") if settings_draft.particle_density == "low" else _tr("标准", "Standard"), cycle_particles))
|
||
content.add_child(_check_button("SettingLowPower", _tr("低功耗模式", "Low-power mode"), bool(settings_draft.low_power), set_low_power))
|
||
"audio":
|
||
content.add_child(_settings_heading(_tr("音频与字幕", "AUDIO & CAPTIONS")))
|
||
content.add_child(_check_button("SettingMasterMute", _tr("主静音", "Master mute"), bool(settings_draft.master_muted), set_master_mute))
|
||
content.add_child(_slider_row("SettingMusic", _tr("音乐", "Music"), float(settings_draft.music_volume), 0.0, 1.0, 0.1, set_music_volume))
|
||
content.add_child(_slider_row("SettingSFX", _tr("音效", "SFX"), float(settings_draft.sfx_volume), 0.0, 1.0, 0.1, set_sfx_volume))
|
||
content.add_child(_unavailable_row("SettingAmbienceUnavailable", _tr("环境音:DEV-1 未集成独立总线", "Ambience: no separate DEV-1 bus is integrated")))
|
||
content.add_child(_unavailable_row("SettingVoiceUnavailable", _tr("语音:当前没有语音资产", "Voice: no voice assets are present")))
|
||
content.add_child(_check_button("SettingCaptions", _tr("字幕", "Captions"), bool(settings_draft.captions), set_captions))
|
||
content.add_child(_choice_button("SettingCaptionDetail", _tr("字幕细节", "Caption detail"), _tr("完整", "Full") if settings_draft.caption_detail == "full" else _tr("关键事件", "Critical"), cycle_caption_detail))
|
||
content.add_child(_choice_button("SettingDynamicRange", _tr("动态范围", "Dynamic range"), String(settings_draft.dynamic_range).capitalize(), cycle_dynamic_range))
|
||
"controls":
|
||
content.add_child(_settings_heading(_tr("控制", "CONTROLS")))
|
||
content.add_child(_label(_tr("触控、键鼠与控制器共享确认、取消和导航语义。", "Touch, keyboard/mouse and controller share confirm, cancel and navigation semantics."), _scaled(15), Color("#d7e0dd")))
|
||
content.add_child(_slider_row("SettingHoldDuration", _tr("长按确认时间", "Hold duration"), float(settings_draft.hold_duration), 0.3, 1.5, 0.1, set_hold_duration))
|
||
content.add_child(_check_button("SettingVibration", _tr("震动", "Vibration"), bool(settings_draft.vibration), set_vibration))
|
||
content.add_child(_unavailable_row("SettingRemapUnavailable", _tr("按键重映射:DEV-1 仅提供默认入口映射;完整冲突检查属于后续控制页面", "Remapping: DEV-1 uses the default entry map; conflict-safe remapping belongs to the later Controls page")))
|
||
"language":
|
||
content.add_child(_settings_heading(_tr("语言与地区格式", "LANGUAGE & LOCALE")))
|
||
content.add_child(_choice_button("SettingLocale", _tr("界面语言", "Interface language"), _tr("简体中文", "English"), cycle_setting_locale))
|
||
content.add_child(_label(_tr("日期 2026-08-17 · 数字 1,234.5 · 文本可换行", "Date 2026-08-17 · Number 1,234.5 · Text may wrap"), _scaled(16), Color("#f3f6f2")))
|
||
"credits":
|
||
content.add_child(_settings_heading(_tr("制作人员、许可与数据", "CREDITS, LICENSES & DATA")))
|
||
content.add_child(_label(_credits_text(), _scaled(14), Color("#f3f6f2")))
|
||
|
||
|
||
func open_settings_category(category: String) -> void:
|
||
settings_category = category
|
||
show_settings()
|
||
|
||
|
||
func request_settings_back() -> void:
|
||
if settings_draft != settings:
|
||
settings_notice = _tr("有未保存的更改。请选择应用或放弃并返回。", "Unsaved changes. Choose Apply or Discard & Back.")
|
||
show_settings()
|
||
else:
|
||
back_to_title()
|
||
|
||
|
||
func discard_settings() -> void:
|
||
settings_draft = {}
|
||
settings_notice = ""
|
||
back_to_title()
|
||
|
||
|
||
func apply_settings() -> void:
|
||
var saved: Dictionary = settings_store.save_settings(settings_draft)
|
||
if not saved.ok:
|
||
settings_notice = _tr("设置保存失败,请重试。", "Settings save failed. Retry.") + " [%s]" % String(saved.code)
|
||
show_settings()
|
||
return
|
||
settings = saved.settings.duplicate(true)
|
||
settings_draft = settings.duplicate(true)
|
||
_apply_runtime_settings()
|
||
settings_notice = _tr("设置已应用", "Settings applied")
|
||
show_settings()
|
||
|
||
|
||
func show_credits() -> void:
|
||
settings_draft = settings.duplicate(true)
|
||
settings_category = "credits"
|
||
show_settings("credits")
|
||
|
||
|
||
func _credits_text() -> String:
|
||
return _tr(
|
||
"Aetherbound Guild · 原创游戏与运行实现\n字体:Noto Sans SC,SIL Open Font License 1.1(完整许可随游戏提供)\n数据:核心游戏完全离线;DEV-1 不收集分析数据,不创建账户,不打开外部链接。\n隐私:设置与远征记录只保存在本地。",
|
||
"Aetherbound Guild · original game and runtime implementation\nFont: Noto Sans SC, SIL Open Font License 1.1 (full license included)\nData: core play is fully offline; DEV-1 collects no analytics, creates no account and opens no external links.\nPrivacy: settings and expedition records remain local."
|
||
)
|
||
|
||
|
||
func _apply_runtime_settings() -> void:
|
||
var master := AudioServer.get_bus_index(&"Master")
|
||
var music := AudioServer.get_bus_index(&"Music")
|
||
var sfx := AudioServer.get_bus_index(&"SFX")
|
||
if master >= 0:
|
||
AudioServer.set_bus_mute(master, bool(settings.master_muted))
|
||
if music >= 0:
|
||
AudioServer.set_bus_volume_db(music, linear_to_db(maxf(0.001, float(settings.music_volume))))
|
||
if sfx >= 0:
|
||
AudioServer.set_bus_volume_db(sfx, linear_to_db(maxf(0.001, float(settings.sfx_volume))))
|
||
|
||
|
||
func cycle_setting_safe_area() -> void:
|
||
settings_draft.safe_area = _next_value(SettingsStore.SAFE_AREA_PRESETS, int(settings_draft.safe_area))
|
||
show_settings()
|
||
|
||
|
||
func cycle_setting_text_scale() -> void:
|
||
settings_draft.text_scale = _next_float(SettingsStore.TEXT_SCALES, float(settings_draft.text_scale))
|
||
show_settings()
|
||
|
||
|
||
func cycle_setting_locale() -> void:
|
||
settings_draft.locale = "en" if String(settings_draft.locale) == "zh_CN" else "zh_CN"
|
||
setup_draft = settings_draft.duplicate(true)
|
||
show_settings()
|
||
|
||
|
||
func cycle_particles() -> void:
|
||
settings_draft.particle_density = "low" if settings_draft.particle_density == "standard" else "standard"
|
||
show_settings()
|
||
|
||
|
||
func cycle_caption_detail() -> void:
|
||
settings_draft.caption_detail = "full" if settings_draft.caption_detail == "critical" else "critical"
|
||
show_settings()
|
||
|
||
|
||
func cycle_dynamic_range() -> void:
|
||
settings_draft.dynamic_range = _next_value(["wide", "standard", "night"], String(settings_draft.dynamic_range))
|
||
show_settings()
|
||
|
||
|
||
func set_brightness(value: float) -> void: settings_draft.brightness = value
|
||
func set_high_contrast(value: bool) -> void: settings_draft.high_contrast = value
|
||
func set_color_cues(value: bool) -> void: settings_draft.color_cues = value
|
||
func set_reduced_motion(value: bool) -> void: settings_draft.reduced_motion = value
|
||
func set_reduced_flashes(value: bool) -> void: settings_draft.reduced_flashes = value
|
||
func set_shake(value: float) -> void: settings_draft.shake_intensity = value
|
||
func set_hit_pause(value: bool) -> void: settings_draft.hit_pause = value
|
||
func set_low_power(value: bool) -> void: settings_draft.low_power = value
|
||
func set_master_mute(value: bool) -> void: settings_draft.master_muted = value
|
||
func set_music_volume(value: float) -> void: settings_draft.music_volume = value
|
||
func set_sfx_volume(value: float) -> void: settings_draft.sfx_volume = value
|
||
func set_captions(value: bool) -> void: settings_draft.captions = value
|
||
func set_hold_duration(value: float) -> void: settings_draft.hold_duration = value
|
||
func set_vibration(value: bool) -> void: settings_draft.vibration = value
|
||
|
||
|
||
func _next_value(options: Array, current: Variant) -> Variant:
|
||
return options[(options.find(current) + 1) % options.size()]
|
||
|
||
|
||
func _next_float(options: Array, current: float) -> float:
|
||
for index in range(options.size()):
|
||
if is_equal_approx(float(options[index]), current):
|
||
return float(options[(index + 1) % options.size()])
|
||
return float(options[0])
|
||
|
||
|
||
func _settings_heading(text_value: String) -> Label:
|
||
return _label(text_value, _scaled(20), Color("#f3f6f2"))
|
||
|
||
|
||
func _slider_row(node_name: String, text_value: String, value: float, minimum: float, maximum: float, step: float, callback: Callable) -> HBoxContainer:
|
||
var row := HBoxContainer.new()
|
||
row.name = node_name
|
||
row.custom_minimum_size.y = 42
|
||
var label := _label("%s %.1f" % [text_value, value], _scaled(15), Color("#f3f6f2"))
|
||
label.custom_minimum_size.x = 190
|
||
row.add_child(label)
|
||
var slider := HSlider.new()
|
||
slider.name = "Slider"
|
||
slider.min_value = minimum
|
||
slider.max_value = maximum
|
||
slider.step = step
|
||
slider.value = value
|
||
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
slider.value_changed.connect(callback)
|
||
row.add_child(slider)
|
||
return row
|
||
|
||
|
||
func _unavailable_row(node_name: String, reason: String) -> Button:
|
||
var button := _choice_button(node_name, reason, _tr("不可用", "Unavailable"), show_unavailable_reason.bind(reason))
|
||
button.disabled = true
|
||
button.tooltip_text = reason
|
||
return button
|
||
|
||
|
||
func show_unavailable_reason(reason: String) -> void:
|
||
settings_notice = reason
|
||
show_settings()
|
||
|
||
|
||
func _show_child_placeholder(screen_id: String, heading: String, body: String) -> void:
|
||
current_screen = screen_id
|
||
_build_world_base()
|
||
var safe := MarginContainer.new()
|
||
safe.name = "ChildSafeArea"
|
||
safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
safe.add_theme_constant_override("margin_left", 56)
|
||
safe.add_theme_constant_override("margin_right", 56)
|
||
safe.add_theme_constant_override("margin_top", 40)
|
||
safe.add_theme_constant_override("margin_bottom", 40)
|
||
add_child(safe)
|
||
var box := VBoxContainer.new()
|
||
box.name = "ChildLayout"
|
||
box.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
box.add_theme_constant_override("separation", 14)
|
||
safe.add_child(box)
|
||
box.add_child(_label(heading, _scaled(28), Color("#f3f6f2")))
|
||
var explanation := _label(body, _scaled(17), Color("#d7e0dd"))
|
||
explanation.name = "ChildExplanation"
|
||
explanation.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
box.add_child(explanation)
|
||
box.add_child(_action_button("ChildBack", _tr("返回标题首页", "Back to Title"), back_to_title))
|
||
|
||
|
||
func back_to_title() -> void:
|
||
title_notice = ""
|
||
show_title()
|
||
|
||
|
||
func request_exit() -> void:
|
||
current_screen = "exit_review"
|
||
_show_child_placeholder("exit_review", _tr("退出游戏?", "EXIT GAME?"), _tr("不会改变设置或本地远征记录。", "Settings and the local expedition will remain unchanged."))
|
||
var box := get_node("ChildSafeArea/ChildLayout")
|
||
box.get_node("ChildBack").text = _tr("取消", "Cancel")
|
||
box.add_child(_action_button("ExitConfirm", _tr("退出", "Exit"), confirm_exit))
|
||
|
||
|
||
func confirm_exit() -> void:
|
||
get_tree().quit()
|
||
|
||
|
||
func _unhandled_input(event: InputEvent) -> void:
|
||
if event.is_action_pressed("ui_cancel"):
|
||
get_viewport().set_input_as_handled()
|
||
if current_screen == "title":
|
||
request_exit()
|
||
elif current_screen in ["settings", "credits", "exit_review"]:
|
||
back_to_title()
|
||
|
||
|
||
func _save_status() -> String:
|
||
var save_path := BootService.DEFAULT_SAVE_PATH
|
||
if not FileAccess.file_exists(save_path):
|
||
return "missing"
|
||
var parser := JSON.new()
|
||
if parser.parse(FileAccess.get_file_as_string(save_path)) != OK or not parser.data is Dictionary:
|
||
return "unsafe"
|
||
return "valid" if parser.data.has("save_kind") and parser.data.has("save_version") else "unsafe"
|
||
|
||
|
||
func _build_world_base() -> void:
|
||
for child in get_children():
|
||
child.free()
|
||
var background := TextureRect.new()
|
||
background.texture = GUILD_BACKGROUND
|
||
background.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
background.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
|
||
background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
background.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(background)
|
||
var shade := ColorRect.new()
|
||
shade.color = Color(0.03, 0.06, 0.08, 0.34)
|
||
shade.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
shade.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(shade)
|
||
|
||
|
||
func _choice_button(node_name: String, label_text: String, value_text: String, callback: Callable) -> Button:
|
||
var button := Button.new()
|
||
button.name = node_name
|
||
button.text = "%s %s" % [label_text, value_text]
|
||
button.custom_minimum_size = Vector2(0, 48)
|
||
button.add_theme_font_override("font", UI_FONT)
|
||
button.add_theme_font_size_override("font_size", _scaled(16))
|
||
button.pressed.connect(callback)
|
||
return button
|
||
|
||
|
||
func _action_button(node_name: String, text_value: String, callback: Callable) -> Button:
|
||
var button := Button.new()
|
||
button.name = node_name
|
||
button.text = text_value
|
||
button.custom_minimum_size = Vector2(190, 52)
|
||
button.add_theme_font_override("font", UI_FONT)
|
||
button.add_theme_font_size_override("font_size", _scaled(17))
|
||
button.pressed.connect(callback)
|
||
return button
|
||
|
||
|
||
func _tool_button(node_name: String, text_value: String, callback: Callable) -> Button:
|
||
var button := Button.new()
|
||
button.name = node_name
|
||
button.text = text_value
|
||
button.custom_minimum_size = Vector2(112, 48)
|
||
button.add_theme_font_override("font", UI_FONT)
|
||
button.add_theme_font_size_override("font_size", _scaled(15))
|
||
button.pressed.connect(callback)
|
||
return button
|
||
|
||
|
||
func _check_button(node_name: String, text_value: String, enabled: bool, callback: Callable) -> CheckButton:
|
||
var check := CheckButton.new()
|
||
check.name = node_name
|
||
check.text = text_value
|
||
check.button_pressed = enabled
|
||
check.custom_minimum_size = Vector2(0, 48)
|
||
check.add_theme_font_override("font", UI_FONT)
|
||
check.add_theme_font_size_override("font_size", _scaled(16))
|
||
check.toggled.connect(callback)
|
||
return check
|
||
|
||
|
||
func _label(text_value: String, font_size: int, color: Color) -> Label:
|
||
var label := Label.new()
|
||
label.text = text_value
|
||
label.add_theme_font_override("font", UI_FONT)
|
||
label.add_theme_font_size_override("font_size", font_size)
|
||
label.add_theme_color_override("font_color", color)
|
||
return label
|
||
|
||
|
||
func _tr(zh: String, en: String) -> String:
|
||
var locale := String(setup_draft.get("locale", settings.get("locale", "zh_CN")))
|
||
return zh if locale == "zh_CN" else en
|
||
|
||
|
||
func _scaled(value: int) -> int:
|
||
var scale := float(setup_draft.get("text_scale", settings.get("text_scale", 1.0)))
|
||
return int(round(float(value) * scale))
|