Files
aetherbound-guild/runtime/entry/entry_shell.gd
T

792 lines
34 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.
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))