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

370 lines
14 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
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.queue_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_hold()
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_hold()
func _show_title_hold() -> void:
_build_world_base()
var safe := MarginContainer.new()
safe.name = "TitleHoldSafeArea"
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 copy := VBoxContainer.new()
copy.alignment = BoxContainer.ALIGNMENT_END
safe.add_child(copy)
copy.add_child(_label(_tr("裂脉远征团", "AETHERBOUND GUILD"), _scaled(38), Color("#f3f6f2")))
copy.add_child(_label(_tr("首次启动设置已保存", "First-launch setup saved"), _scaled(17), Color("#d7e0dd")))
func _build_world_base() -> void:
for child in get_children():
child.queue_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 _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))