2017 lines
97 KiB
GDScript
2017 lines
97 KiB
GDScript
class_name ABGEntryShell
|
||
extends Control
|
||
|
||
const BootService = preload("res://entry/boot_service.gd")
|
||
const SettingsStore = preload("res://entry/settings_store.gd")
|
||
const EntryChrome = preload("res://entry/entry_chrome.gd")
|
||
const InitialCompanyArt = preload("res://entry/initial_company_art.gd")
|
||
const GuildDestinationSticker = preload("res://entry/guild_destination_sticker.gd")
|
||
const InitialCompanyDraftService = preload("res://entry/initial_company_draft_service.gd")
|
||
const GuildContractService = preload("res://entry/guild_contract_service.gd")
|
||
const RuntimeContentRegistry = preload("res://p10_0/content_registry.gd")
|
||
const MarketLocalization = preload("res://entry/market_localization.gd")
|
||
const InitialCompanyPage = preload("res://entry/initial_company_page.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 initial_company_service := InitialCompanyDraftService.new()
|
||
var guild_contract_service := GuildContractService.new()
|
||
var initial_company_page: Control
|
||
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 := ""
|
||
var settings_caller := "title"
|
||
var guild_company: Dictionary = {}
|
||
var contract_difficulty := "standard"
|
||
var contract_notice := ""
|
||
var market_category := "recruit"
|
||
var market_selected_kind := ""
|
||
var market_selected_offer_id := ""
|
||
var market_notice := ""
|
||
var market_content: Dictionary = RuntimeContentRegistry.snapshot()
|
||
|
||
|
||
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", 32 if size.y < 500.0 else 48)
|
||
safe.add_theme_constant_override("margin_right", 32 if size.y < 500.0 else 48)
|
||
safe.add_theme_constant_override("margin_top", 24 if size.y < 500.0 else 40)
|
||
safe.add_theme_constant_override("margin_bottom", 24 if size.y < 500.0 else 40)
|
||
add_child(safe)
|
||
safe.add_child(_entry_chrome("BootPlate", "boot_plate"))
|
||
|
||
var layout := VBoxContainer.new()
|
||
layout.name = "BootLayout"
|
||
layout.alignment = BoxContainer.ALIGNMENT_END
|
||
layout.add_theme_constant_override("separation", 6)
|
||
safe.add_child(layout)
|
||
|
||
var overline := _label(_tr("本地入口检查", "LOCAL ENTRY CHECK"), 12, Color("#e8d39d"))
|
||
overline.name = "BootOverline"
|
||
layout.add_child(overline)
|
||
|
||
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", 28 if size.y < 500.0 else 34)
|
||
identity.add_theme_color_override("font_color", Color("#f5ead0"))
|
||
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", 18 if size.y < 500.0 else 20)
|
||
status_label.add_theme_color_override("font_color", Color("#f5ead0"))
|
||
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", 14 if size.y < 500.0 else 16)
|
||
detail_label.add_theme_color_override("font_color", Color("#aebbc2"))
|
||
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
|
||
progress_bar.add_theme_stylebox_override("background", _flat_style(Color("#0d151d"), Color("#866f47"), 1, 4))
|
||
progress_bar.add_theme_stylebox_override("fill", _flat_style(Color("#72b9e8"), Color("#ead49b"), 1, 4))
|
||
layout.add_child(progress_bar)
|
||
|
||
retry_button = _action_button("BootRetry", _tr("重试检查", "Retry verification"), retry_boot)
|
||
retry_button.custom_minimum_size = Vector2(240, 52)
|
||
retry_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||
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 overline := get_node_or_null("EntrySafeArea/BootLayout/BootOverline") as Label
|
||
if overline != null:
|
||
overline.text = _tr("本地入口检查", "LOCAL ENTRY CHECK")
|
||
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 = _tr("本地检查需要处理", "Verification needs attention")
|
||
detail_label.text = _tr("本地数据保持不变。解决问题后重试本检查。", "Local data was left unchanged. Resolve the issue, then retry this check.")
|
||
diagnostics_label.text = "%s: %s" % [_tr("诊断", "Diagnostic"), String(boot_snapshot.get("code", "UNKNOWN"))]
|
||
"complete":
|
||
status_label.text = _tr("本地检查完成", "Local verification complete")
|
||
detail_label.text = _tr("资源、设置存储与存档访问均已就绪。", "Resources, settings storage and save access are ready.")
|
||
diagnostics_label.text = ""
|
||
_:
|
||
status_label.text = _tr("正在准备公会门槛", "Preparing the guild threshold")
|
||
detail_label.text = _step_text(step)
|
||
diagnostics_label.text = ""
|
||
|
||
|
||
func _step_text(step: String) -> String:
|
||
match step:
|
||
"resources": return _tr("正在检查必需的本地资源...", "Checking required local resources...")
|
||
"storage": return _tr("正在检查本地存储访问...", "Checking local storage access...")
|
||
"preferences": return _tr("正在检查显示与辅助设置...", "Checking display and accessibility settings...")
|
||
"save": return _tr("正在检查最近的本地远征记录...", "Checking the latest local run record...")
|
||
return _tr("正在完成本地检查...", "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):
|
||
var company := initial_company_service.load_company()
|
||
if company.ok:
|
||
_open_company_destination(company.company)
|
||
else:
|
||
if String(company.code) != "COMPANY_MISSING":
|
||
title_notice = _tr("远征团存档需要处理,已停止创建新游戏。", "Company data needs attention; New Game is blocked.") + " [%s]" % String(company.code)
|
||
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
|
||
safe.add_theme_constant_override("margin_left", margin)
|
||
safe.add_theme_constant_override("margin_right", margin)
|
||
safe.add_theme_constant_override("margin_top", mini(24, margin))
|
||
safe.add_theme_constant_override("margin_bottom", mini(24, margin))
|
||
add_child(safe)
|
||
safe.add_child(_entry_chrome("FirstLaunchChrome", "setup_split"))
|
||
var row := HBoxContainer.new()
|
||
row.name = "FirstLaunchLayout"
|
||
row.add_theme_constant_override("separation", 16)
|
||
safe.add_child(row)
|
||
|
||
var preview := VBoxContainer.new()
|
||
preview.name = "PaintedPreview"
|
||
preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
preview.clip_contents = true
|
||
preview.alignment = BoxContainer.ALIGNMENT_END
|
||
row.add_child(preview)
|
||
preview.add_child(_label(_tr("公会门槛", "GUILD THRESHOLD"), 15, Color("#e8d39d")))
|
||
var preview_title := _label(_tr("裂脉远征团", "AETHERBOUND GUILD"), _scaled(30), Color("#f5ead0"))
|
||
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(14), Color("#f5ead0")))
|
||
|
||
var controls := VBoxContainer.new()
|
||
controls.name = "FirstLaunchControls"
|
||
controls.custom_minimum_size = Vector2(minf(390.0, size.x * 0.46), 0)
|
||
controls.add_theme_constant_override("separation", 5)
|
||
row.add_child(controls)
|
||
controls.add_child(_label(_tr("首次启动设置", "FIRST-LAUNCH SETUP"), _scaled(22), Color("#f5ead0")))
|
||
controls.add_child(_label(_tr("这些选项只影响显示与辅助功能。", "These choices affect presentation and accessibility only."), _scaled(13), Color("#aebbc2")))
|
||
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 := _action_button("SetupApply", _tr("应用并进入标题首页", "Apply and continue to Title"), apply_first_launch)
|
||
apply.custom_minimum_size = Vector2(0, 52)
|
||
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 := maxi(12, int(settings.get("safe_area", 24)))
|
||
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 := Control.new()
|
||
layout.name = "TitleLayout"
|
||
safe.add_child(layout)
|
||
var available := Vector2(maxf(1.0, size.x - margin * 2.0), maxf(1.0, size.y - margin * 2.0))
|
||
var short_layout := size.y < 500.0
|
||
var header_height := 82.0 if short_layout else 116.0
|
||
var tools_width := 252.0 if short_layout else 292.0
|
||
var header_width := minf(720.0, maxf(330.0, available.x - tools_width - 12.0))
|
||
var header := Control.new()
|
||
header.name = "TitleHeader"
|
||
header.position = Vector2.ZERO
|
||
header.size = Vector2(header_width, header_height)
|
||
header.add_child(_entry_chrome("TitleHeaderChrome", "title_header"))
|
||
layout.add_child(header)
|
||
var crest := Control.new()
|
||
crest.name = "TitleGuildBanner"
|
||
crest.position = Vector2(12.0, 0.0)
|
||
crest.size = Vector2(66.0 if short_layout else 82.0, header_height + (8.0 if short_layout else 14.0))
|
||
crest.add_child(_entry_chrome("TitleGuildBannerChrome", "crest_banner"))
|
||
header.add_child(crest)
|
||
var header_left := crest.position.x + crest.size.x + (14.0 if short_layout else 20.0)
|
||
var overline := _label(_tr("风路公会 // 远征入口", "WIND-ROAD GUILD // EXPEDITION THRESHOLD"), _scaled(11 if short_layout else 13), Color("#e8d39d"))
|
||
overline.name = "TitleOverline"
|
||
overline.position = Vector2(header_left, 13.0 if short_layout else 20.0)
|
||
overline.size = Vector2(maxf(40.0, header_width - header_left - 20.0), 20.0 if short_layout else 24.0)
|
||
header.add_child(overline)
|
||
var identity := _label(_tr("裂脉远征团", "AETHERBOUND GUILD"), _scaled(24 if short_layout else 34), Color("#f5ead0"))
|
||
identity.name = "TitleIdentity"
|
||
identity.position = Vector2(header_left, 31.0 if short_layout else 43.0)
|
||
identity.size = Vector2(maxf(40.0, header_width - header_left - 20.0), 40.0 if short_layout else 52.0)
|
||
identity.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
header.add_child(identity)
|
||
var chapter := _label(_tr("原创横屏幻想远征", "THE FIRST CHRONICLE"), _scaled(10 if short_layout else 12), Color("#8fc7e8"))
|
||
chapter.name = "TitleChronicle"
|
||
chapter.position = Vector2(header_left, 61.0 if short_layout else 87.0)
|
||
chapter.size = Vector2(maxf(40.0, header_width - header_left - 20.0), 18.0)
|
||
header.add_child(chapter)
|
||
var tools := HBoxContainer.new()
|
||
tools.name = "TitleTools"
|
||
tools.alignment = BoxContainer.ALIGNMENT_END
|
||
tools.position = Vector2(maxf(0.0, available.x - tools_width), 0.0)
|
||
tools.size = Vector2(tools_width, 48.0 if short_layout else 54.0)
|
||
tools.add_theme_constant_override("separation", 4)
|
||
var settings_button := _title_button("TitleSettings", _tr("设置", "Settings"), show_settings_placeholder, "settings", "utility")
|
||
settings_button.custom_minimum_size = Vector2((tools_width - 4.0) * 0.5, tools.size.y)
|
||
tools.add_child(settings_button)
|
||
var credits_button := _title_button("TitleCredits", _tr("制作人员", "Credits"), show_credits_placeholder, "credits", "utility")
|
||
credits_button.custom_minimum_size = Vector2((tools_width - 4.0) * 0.5, tools.size.y)
|
||
tools.add_child(credits_button)
|
||
layout.add_child(tools)
|
||
var dock_height := 128.0 if short_layout else 156.0
|
||
var dock := Control.new()
|
||
dock.name = "TitleCommandDock"
|
||
dock.position = Vector2(0.0, maxf(header_height + 10.0, available.y - dock_height))
|
||
dock.size = Vector2(available.x, dock_height)
|
||
dock.add_child(_entry_chrome("TitleCommandDockChrome", "command_dock"))
|
||
layout.add_child(dock)
|
||
var copy := Control.new()
|
||
copy.name = "TitleCopy"
|
||
copy.position = dock.position
|
||
copy.size = dock.size
|
||
layout.add_child(copy)
|
||
var ledger := _label(_tr("远征记录", "EXPEDITION LEDGER"), _scaled(10 if short_layout else 12), Color("#e8d39d"))
|
||
ledger.name = "TitleLedgerHeading"
|
||
ledger.position = Vector2(22.0, 20.0 if short_layout else 26.0)
|
||
ledger.size = Vector2(maxf(120.0, available.x * 0.35), 20.0)
|
||
copy.add_child(ledger)
|
||
var save_status := _save_status()
|
||
var company_result := initial_company_service.load_company()
|
||
var has_company := bool(company_result.ok)
|
||
var data_unsafe := save_status == "unsafe"
|
||
var summary := _label(
|
||
_tr("远征团已建立 · 可继续进入公会", "Expedition Company established · continue to the Guild")
|
||
if has_company else _tr("本地数据需要处理", "Local data needs attention")
|
||
if data_unsafe else _tr("发现可恢复的本地远征记录", "A local expedition record is available")
|
||
if save_status == "valid" else _tr("尚无远征记录 · 从新游戏开始", "No expedition record · begin with New Game"),
|
||
_scaled(14 if short_layout else 16),
|
||
Color("#f5ead0")
|
||
)
|
||
summary.name = "TitleSaveSummary"
|
||
summary.position = Vector2(22.0, 42.0 if short_layout else 52.0)
|
||
summary.size = Vector2(maxf(120.0, available.x - (342.0 if short_layout else 470.0)), 28.0)
|
||
summary.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
copy.add_child(summary)
|
||
var actions := HBoxContainer.new()
|
||
actions.name = "TitleActions"
|
||
var action_width := minf(442.0, maxf(304.0, available.x * (0.48 if short_layout else 0.40)))
|
||
actions.position = Vector2(available.x - action_width - 20.0, 42.0 if short_layout else 51.0)
|
||
actions.size = Vector2(action_width, 60.0 if short_layout else 70.0)
|
||
actions.add_theme_constant_override("separation", 8)
|
||
copy.add_child(actions)
|
||
var new_game := _title_button("TitleNewGame", _tr("新游戏", "New Game"), open_new_game_boundary, "new_game", "action_primary")
|
||
new_game.custom_minimum_size = Vector2((action_width - 8.0) * 0.5, actions.size.y)
|
||
new_game.disabled = has_company or data_unsafe
|
||
new_game.tooltip_text = _tr("远征团已建立;继续游戏进入公会", "A Company already exists; Continue opens the Guild") if has_company else _tr("本地数据需要先处理", "Local data needs attention") if new_game.disabled else _tr("建立初始远征团", "Create the initial Company")
|
||
_set_title_button_disabled(new_game, new_game.disabled)
|
||
actions.add_child(new_game)
|
||
var continue_button := _title_button("TitleContinue", _tr("继续游戏", "Continue"), open_continue_boundary, "continue", "action_secondary")
|
||
continue_button.custom_minimum_size = Vector2((action_width - 8.0) * 0.5, actions.size.y)
|
||
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")
|
||
_set_title_button_disabled(continue_button, continue_button.disabled)
|
||
actions.add_child(continue_button)
|
||
(new_game if not new_game.disabled else continue_button if not continue_button.disabled else settings_button).call_deferred("grab_focus")
|
||
var reason := _label(
|
||
_tr("本地数据需要处理;新游戏与继续游戏均已停止。", "Local data needs attention; New Game and Continue are blocked.")
|
||
if data_unsafe else _tr("继续游戏:没有可恢复的本地远征记录", "Continue: no resumable local expedition")
|
||
if continue_button.disabled else _tr("继续游戏会先进入恢复确认,不会立即推进模拟。", "Continue opens resume review before simulation advances."),
|
||
_scaled(11 if short_layout else 13),
|
||
Color("#aebbc2")
|
||
)
|
||
reason.name = "TitleContinueReason"
|
||
reason.position = Vector2(22.0, 72.0 if short_layout else 84.0)
|
||
reason.size = Vector2(maxf(120.0, available.x - action_width - 64.0), 42.0 if short_layout else 50.0)
|
||
reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
copy.add_child(reason)
|
||
var notice := _label(title_notice, _scaled(11 if short_layout else 13), Color("#f0ca75"))
|
||
notice.name = "TitleNotice"
|
||
notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
notice.position = reason.position
|
||
notice.size = reason.size
|
||
reason.visible = title_notice.is_empty()
|
||
copy.add_child(notice)
|
||
|
||
|
||
func open_new_game_boundary() -> void:
|
||
var company := initial_company_service.load_company()
|
||
if company.ok:
|
||
_open_company_destination(company.company)
|
||
return
|
||
if String(company.code) != "COMPANY_MISSING":
|
||
title_notice = _tr("远征团存档需要处理,未创建新候选。", "Company data needs attention; no new candidates were created.") + " [%s]" % String(company.code)
|
||
show_title()
|
||
return
|
||
show_initial_company()
|
||
|
||
|
||
func show_initial_company() -> void:
|
||
current_screen = "initial_company"
|
||
for child in get_children():
|
||
child.free()
|
||
initial_company_page = InitialCompanyPage.new()
|
||
initial_company_page.name = "InitialCompanyPage"
|
||
initial_company_page.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
initial_company_page.back_requested.connect(back_to_title, CONNECT_DEFERRED)
|
||
initial_company_page.company_committed.connect(show_guild_handoff, CONNECT_DEFERRED)
|
||
add_child(initial_company_page)
|
||
initial_company_page.open(initial_company_service, settings)
|
||
|
||
|
||
func open_continue_boundary() -> void:
|
||
var company := initial_company_service.load_company()
|
||
if company.ok:
|
||
_open_company_destination(company.company)
|
||
return
|
||
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 _open_company_destination(company: Dictionary) -> void:
|
||
if company.has("active_contract") and not company.active_contract.is_empty():
|
||
show_market_handoff(company)
|
||
else:
|
||
show_guild_handoff(company)
|
||
|
||
|
||
func show_guild_handoff(company := {}) -> void:
|
||
var resolved: Dictionary = Dictionary(company).duplicate(true) if company is Dictionary else {}
|
||
if resolved.is_empty():
|
||
var loaded := initial_company_service.load_company()
|
||
if not loaded.ok:
|
||
title_notice = _tr("无法读取远征团。", "The Company could not be loaded.") + " [%s]" % String(loaded.code)
|
||
show_title()
|
||
return
|
||
resolved = loaded.company.duplicate(true)
|
||
guild_contract_service.company_path = initial_company_service.company_path
|
||
guild_company = resolved
|
||
current_screen = "guild_handoff"
|
||
_build_world_base()
|
||
var frame_safe := MarginContainer.new()
|
||
frame_safe.name = "GuildHandoffFrameSafeArea"
|
||
frame_safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
frame_safe.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
var margin := maxi(12, int(settings.get("safe_area", 24)))
|
||
var vertical_margin := 12 if size.y < 500.0 else margin
|
||
frame_safe.add_theme_constant_override("margin_left", margin)
|
||
frame_safe.add_theme_constant_override("margin_right", margin)
|
||
frame_safe.add_theme_constant_override("margin_top", vertical_margin)
|
||
frame_safe.add_theme_constant_override("margin_bottom", vertical_margin)
|
||
add_child(frame_safe)
|
||
frame_safe.add_child(_entry_chrome("GuildHandoffFrame", "initial_company_scene_frame"))
|
||
var safe := MarginContainer.new()
|
||
safe.name = "GuildHandoffSafeArea"
|
||
safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
safe.add_theme_constant_override("margin_left", margin + 12)
|
||
safe.add_theme_constant_override("margin_right", margin + 12)
|
||
safe.add_theme_constant_override("margin_top", vertical_margin + 8)
|
||
safe.add_theme_constant_override("margin_bottom", vertical_margin + 8)
|
||
add_child(safe)
|
||
var layout := VBoxContainer.new()
|
||
layout.name = "GuildHandoffLayout"
|
||
layout.add_theme_constant_override("separation", 6)
|
||
safe.add_child(layout)
|
||
var header := HBoxContainer.new()
|
||
header.custom_minimum_size = Vector2(0, 50)
|
||
layout.add_child(header)
|
||
var copy := VBoxContainer.new()
|
||
copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
header.add_child(copy)
|
||
copy.add_child(_label(_tr("公会大厅", "GUILD HALL"), _scaled(21), Color("#f5ead0")))
|
||
var active_contract: bool = resolved.has("active_contract") and not resolved.active_contract.is_empty()
|
||
copy.add_child(_label(
|
||
_tr("首个委托已开始", "FIRST CONTRACT ACTIVE")
|
||
if active_contract else _tr("初始远征团已建立", "INITIAL EXPEDITION COMPANY ESTABLISHED"),
|
||
_scaled(11),
|
||
Color("#e8d39d")
|
||
))
|
||
var identity := _label(
|
||
_tr("市场准备", "MARKET READY") if active_contract else _tr("持续远征", "PERSISTENT COMPANY"),
|
||
_scaled(12),
|
||
Color("#73b9e7")
|
||
)
|
||
identity.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
header.add_child(identity)
|
||
var body := HBoxContainer.new()
|
||
body.name = "GuildHandoffBody"
|
||
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
body.add_theme_constant_override("separation", 18)
|
||
layout.add_child(body)
|
||
var roster := HBoxContainer.new()
|
||
roster.name = "GuildRoster"
|
||
roster.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
roster.size_flags_stretch_ratio = 1.45
|
||
roster.add_theme_constant_override("separation", 12)
|
||
body.add_child(roster)
|
||
for index in range(resolved.owned_recruits.size()):
|
||
roster.add_child(_guild_member_column(index, resolved.owned_recruits[index]))
|
||
var next := VBoxContainer.new()
|
||
next.name = "GuildNextActions"
|
||
next.custom_minimum_size = Vector2(minf(330.0, size.x * 0.36), 0)
|
||
next.add_theme_constant_override("separation", 6)
|
||
body.add_child(next)
|
||
next.add_child(_label(_tr("下一步", "NEXT"), _scaled(10), Color("#e8d39d")))
|
||
var next_copy := _label(
|
||
_tr("风镜初渡已建立。前往市场扩充远征团。", "First Windglass Crossing is active. Enter the Market to expand the Company.")
|
||
if active_contract else _tr("选择首个委托与难度,然后进入市场。", "Choose the first Contract and difficulty, then enter the Market."),
|
||
_scaled(13),
|
||
Color("#f5ead0")
|
||
)
|
||
next_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
next.add_child(next_copy)
|
||
var contract := _guild_destination_button(
|
||
"GuildContractActive" if active_contract else "GuildContractBoard",
|
||
"contract",
|
||
_tr("委托已开始", "Contract Active") if active_contract else _tr("委托板", "Contract Board"),
|
||
func(): _set_guild_notice(_tr("已有进行中的委托。先进入市场。", "A Contract is active. Enter its Market next.")) if active_contract else show_contract_choice()
|
||
)
|
||
_set_entry_button_disabled(contract, active_contract)
|
||
contract.tooltip_text = _tr("已有进行中的委托", "A Contract is already active") if active_contract else _tr("查看风镜初渡并选择难度", "Review First Windglass Crossing and choose difficulty")
|
||
next.add_child(contract)
|
||
var contract_reason := _label(
|
||
_tr("已开始:风镜初渡 · %s", "Active: First Windglass Crossing · %s") % _difficulty_name(String(resolved.active_contract.difficulty))
|
||
if active_contract else _tr("可用:风镜初渡 · 选择难度后确认", "Available: First Windglass Crossing · choose difficulty, then confirm"),
|
||
_scaled(10),
|
||
Color("#aebbc2")
|
||
)
|
||
contract_reason.name = "GuildContractReason"
|
||
contract_reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
next.add_child(contract_reason)
|
||
var market := _guild_destination_button(
|
||
"GuildMarket" if active_contract else "GuildMarketUnavailable",
|
||
"market",
|
||
_tr("进入市场", "Enter Market") if active_contract else _tr("市场", "Market"),
|
||
func(): show_market_handoff(resolved) if active_contract else _set_guild_notice(_tr("请先选择并确认首个委托。", "Choose and confirm the first Contract before entering the Market."))
|
||
)
|
||
_set_entry_button_disabled(market, not active_contract)
|
||
market.tooltip_text = _tr("进入首个委托市场", "Enter the first Contract Market") if active_contract else _tr("先确认首个委托", "Confirm the first Contract first")
|
||
next.add_child(market)
|
||
var market_reason := _label(
|
||
_tr("可用:52 金币 · 远征团 2 · 队列容量 4", "Available: 52 Coin · Company 2 · Line capacity 4")
|
||
if active_contract else _tr("不可用:尚未建立首个委托。", "Unavailable: no first Contract has been created."),
|
||
_scaled(10),
|
||
Color("#aebbc2")
|
||
)
|
||
market_reason.name = "GuildMarketReason"
|
||
market_reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
next.add_child(market_reason)
|
||
var notice := _label("", _scaled(10), Color("#f0ca75"))
|
||
notice.name = "GuildNotice"
|
||
notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
next.add_child(notice)
|
||
var footer := HBoxContainer.new()
|
||
footer.name = "GuildHandoffFooter"
|
||
footer.custom_minimum_size = Vector2(0, 52)
|
||
footer.add_theme_constant_override("separation", 8)
|
||
layout.add_child(footer)
|
||
footer.add_child(_company_button("GuildTitle", _tr("标题首页", "Title"), back_to_title, "company_utility", "back"))
|
||
var spacer := Control.new()
|
||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
footer.add_child(spacer)
|
||
footer.add_child(_company_button("GuildSettings", _tr("设置", "Settings"), show_settings_from_guild, "company_secondary", "settings"))
|
||
(contract if not contract.disabled else market).call_deferred("grab_focus")
|
||
|
||
|
||
func show_contract_choice() -> void:
|
||
var loaded := initial_company_service.load_company()
|
||
if not loaded.ok:
|
||
contract_notice = _tr("无法读取远征团。", "The Company could not be loaded.") + " [%s]" % String(loaded.code)
|
||
show_guild_handoff()
|
||
return
|
||
if not loaded.company.active_contract.is_empty():
|
||
show_market_handoff(loaded.company)
|
||
return
|
||
guild_company = loaded.company.duplicate(true)
|
||
current_screen = "contract_choice"
|
||
_build_world_base()
|
||
var frame_safe := _framed_page_safe("ContractChoice")
|
||
frame_safe.add_child(_entry_chrome("ContractChoiceFrame", "initial_company_scene_frame"))
|
||
var safe := _content_page_safe("ContractChoice")
|
||
var layout := VBoxContainer.new()
|
||
layout.name = "ContractChoiceLayout"
|
||
layout.add_theme_constant_override("separation", 6)
|
||
safe.add_child(layout)
|
||
var header := HBoxContainer.new()
|
||
header.custom_minimum_size = Vector2(0, 48)
|
||
layout.add_child(header)
|
||
var heading := VBoxContainer.new()
|
||
heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
header.add_child(heading)
|
||
heading.add_child(_label(_tr("委托板", "CONTRACT BOARD"), _scaled(21), Color("#f5ead0")))
|
||
heading.add_child(_label(_tr("风镜原野 · 首次远征", "WINDGLASS VERGE · FIRST EXPEDITION"), _scaled(10), Color("#e8d39d")))
|
||
var company_status := _label(_tr("远征团 2 · 持续保留", "COMPANY 2 · PERSISTENT"), _scaled(11), Color("#73b9e7"))
|
||
company_status.name = "ContractCompanyStatus"
|
||
company_status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
header.add_child(company_status)
|
||
var body := HBoxContainer.new()
|
||
body.name = "ContractChoiceBody"
|
||
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
body.add_theme_constant_override("separation", 18)
|
||
layout.add_child(body)
|
||
var scene := VBoxContainer.new()
|
||
scene.name = "ContractBoardScene"
|
||
scene.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
scene.size_flags_stretch_ratio = 1.25
|
||
scene.add_theme_constant_override("separation", 2)
|
||
body.add_child(scene)
|
||
scene.add_child(_label(_tr("01 · 风镜初渡", "01 · FIRST WINDGLASS CROSSING"), _scaled(16), Color("#f5ead0")))
|
||
var route := _label(_tr("穿越悬浮风镜,建立公会的第一条安全通路。", "Cross the suspended windglass and establish the Guild's first safe route."), _scaled(11), Color("#e8d39d"))
|
||
route.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
scene.add_child(route)
|
||
scene.add_child(_contract_company_scene(guild_company.owned_recruits))
|
||
var review := VBoxContainer.new()
|
||
review.name = "ContractReview"
|
||
review.custom_minimum_size = Vector2(minf(360.0, size.x * 0.42), 0)
|
||
review.add_theme_constant_override("separation", 5)
|
||
body.add_child(review)
|
||
review.add_child(_label(_tr("选择难度", "CHOOSE DIFFICULTY"), _scaled(11), Color("#e8d39d")))
|
||
var segments := HBoxContainer.new()
|
||
segments.name = "ContractDifficultySegments"
|
||
segments.custom_minimum_size = Vector2(0, 44)
|
||
segments.add_theme_constant_override("separation", 3)
|
||
review.add_child(segments)
|
||
for difficulty in GuildContractService.DIFFICULTIES:
|
||
segments.add_child(_contract_segment_button(String(difficulty)))
|
||
var difficulty_copy := _label(_difficulty_copy(contract_difficulty), _scaled(11), Color("#f5ead0"))
|
||
difficulty_copy.name = "ContractDifficultyCopy"
|
||
difficulty_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
difficulty_copy.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
review.add_child(difficulty_copy)
|
||
var facts := _label(_tr("建立后:52 金币 · 队列容量 4\n市场:3 名招募 · 4 件装备 · 3 条战斗路线", "On creation: 52 Coin · Line capacity 4\nMarket: 3 Recruits · 4 equipment · 3 battle routes"), _scaled(11), Color("#9ed7aa"))
|
||
facts.name = "ContractCreationFacts"
|
||
facts.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
review.add_child(facts)
|
||
var notice := _label(contract_notice, _scaled(10), Color("#f0ca75"))
|
||
notice.name = "ContractNotice"
|
||
notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
review.add_child(notice)
|
||
var footer := HBoxContainer.new()
|
||
footer.name = "ContractChoiceFooter"
|
||
footer.custom_minimum_size = Vector2(0, 52)
|
||
footer.add_theme_constant_override("separation", 8)
|
||
layout.add_child(footer)
|
||
footer.add_child(_company_button("ContractCancel", _tr("返回公会", "Back to Guild"), back_to_guild, "company_utility", "back"))
|
||
var spacer := Control.new()
|
||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
footer.add_child(spacer)
|
||
var confirm := _company_button("ContractConfirm", _tr("建立委托", "Create Contract"), confirm_contract_choice, "company_primary", "confirm")
|
||
confirm.custom_minimum_size = Vector2(230, 52)
|
||
confirm.tooltip_text = _tr("确认风镜初渡、当前难度、52 金币与队列容量 4", "Confirm First Windglass Crossing, current difficulty, 52 Coin and Line capacity 4")
|
||
footer.add_child(confirm)
|
||
segments.get_child(GuildContractService.DIFFICULTIES.find(contract_difficulty)).call_deferred("grab_focus")
|
||
|
||
|
||
func select_contract_difficulty(value: String) -> void:
|
||
if value not in GuildContractService.DIFFICULTIES:
|
||
return
|
||
contract_difficulty = value
|
||
contract_notice = ""
|
||
show_contract_choice()
|
||
|
||
|
||
func confirm_contract_choice() -> void:
|
||
guild_contract_service.company_path = initial_company_service.company_path
|
||
var operation_id := ("%s|first-contract|%s" % [String(guild_company.get("company_id", "")), contract_difficulty]).sha256_text().substr(0, 32)
|
||
var created: Dictionary = guild_contract_service.create_contract(operation_id, GuildContractService.FIRST_CONTRACT_ID, contract_difficulty)
|
||
if not created.ok:
|
||
contract_notice = _contract_error_text(String(created.get("code", "CONTRACT_CREATE_FAILED")))
|
||
show_contract_choice()
|
||
return
|
||
contract_notice = ""
|
||
guild_company = created.company.duplicate(true)
|
||
show_market_handoff(guild_company)
|
||
|
||
|
||
func show_market_handoff(company := {}) -> void:
|
||
show_market(company)
|
||
|
||
|
||
func show_market(company := {}) -> void:
|
||
var resolved: Dictionary = Dictionary(company).duplicate(true) if company is Dictionary else {}
|
||
if resolved.is_empty():
|
||
var loaded := initial_company_service.load_company()
|
||
if not loaded.ok:
|
||
title_notice = _tr("无法读取远征团。", "The Company could not be loaded.") + " [%s]" % String(loaded.code)
|
||
show_title()
|
||
return
|
||
resolved = loaded.company.duplicate(true)
|
||
if not resolved.has("active_contract") or resolved.active_contract.is_empty():
|
||
show_guild_handoff(resolved)
|
||
return
|
||
guild_company = resolved
|
||
current_screen = "market"
|
||
_build_world_base()
|
||
var frame_safe := _framed_page_safe("Market")
|
||
frame_safe.add_child(_entry_chrome("MarketFrame", "initial_company_scene_frame"))
|
||
var safe := _content_page_safe("Market")
|
||
var layout := VBoxContainer.new()
|
||
layout.name = "MarketLayout"
|
||
layout.add_theme_constant_override("separation", 6)
|
||
safe.add_child(layout)
|
||
var contract: Dictionary = resolved.active_contract
|
||
var short_market := size.y < 380.0
|
||
var header := HBoxContainer.new()
|
||
header.custom_minimum_size = Vector2(0, 40 if short_market else 44)
|
||
layout.add_child(header)
|
||
var heading := VBoxContainer.new()
|
||
heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
header.add_child(heading)
|
||
heading.add_child(_label(_tr("首次市场", "FIRST MARKET"), _scaled(21), Color("#f5ead0")))
|
||
heading.add_child(_label(_tr("风镜初渡 · 稳定货单", "FIRST WINDGLASS CROSSING · STABLE LEDGER"), _scaled(10), Color("#e8d39d")))
|
||
var company_line := _label(_tr("远征团 %d | 队列容量 %d", "Company %d | Line capacity %d") % [resolved.owned_recruits.size(), int(contract.line_capacity)], _scaled(12), Color("#73b9e7"))
|
||
company_line.name = "MarketCompanyStatus"
|
||
company_line.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
header.add_child(company_line)
|
||
var coin := _label(_tr("%d 金币", "%d COIN") % int(contract.coin), _scaled(14), Color("#f0ca75"))
|
||
coin.name = "MarketCoinStatus"
|
||
coin.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
header.add_child(coin)
|
||
var body := HBoxContainer.new()
|
||
body.name = "MarketBody"
|
||
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
body.add_theme_constant_override("separation", 14)
|
||
layout.add_child(body)
|
||
var ledger := VBoxContainer.new()
|
||
ledger.name = "MarketLedger"
|
||
ledger.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
ledger.size_flags_stretch_ratio = 1.35
|
||
ledger.add_theme_constant_override("separation", 4)
|
||
body.add_child(ledger)
|
||
var categories := HBoxContainer.new()
|
||
categories.name = "MarketCategories"
|
||
categories.custom_minimum_size = Vector2(0, 34 if short_market else 38)
|
||
categories.add_theme_constant_override("separation", 4)
|
||
ledger.add_child(categories)
|
||
for category in ["recruit", "equipment", "battle"]:
|
||
categories.add_child(_market_category_button(category))
|
||
var offers := VBoxContainer.new()
|
||
offers.name = "MarketOfferList"
|
||
offers.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
offers.add_theme_constant_override("separation", 3)
|
||
ledger.add_child(offers)
|
||
var rows: Array = contract.market_board.get(_market_collection_field(market_category), [])
|
||
for index in range(rows.size()):
|
||
offers.add_child(_market_offer_row(market_category, rows[index], index))
|
||
var context := VBoxContainer.new()
|
||
context.name = "MarketContext"
|
||
context.custom_minimum_size = Vector2(minf(350.0, size.x * 0.40), 0)
|
||
context.add_theme_constant_override("separation", 5)
|
||
body.add_child(context)
|
||
_render_market_context(context, contract)
|
||
var footer := HBoxContainer.new()
|
||
footer.name = "MarketFooter"
|
||
footer.custom_minimum_size = Vector2(0, 44 if short_market else 48)
|
||
footer.add_theme_constant_override("separation", 8)
|
||
layout.add_child(footer)
|
||
var back := _company_button("MarketBackGuild", _tr("返回公会", "Back to Guild"), back_to_guild, "company_utility", "back")
|
||
back.custom_minimum_size = Vector2(180, 44 if short_market else 48)
|
||
footer.add_child(back)
|
||
var spacer := Control.new()
|
||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
footer.add_child(spacer)
|
||
var title := _company_button("MarketTitle", _tr("标题首页", "Title"), back_to_title, "company_secondary", "home")
|
||
title.custom_minimum_size = Vector2(180, 44 if short_market else 48)
|
||
footer.add_child(title)
|
||
var focus_target := find_child("MarketOffer%s" % market_selected_offer_id, true, false) as Control
|
||
if focus_target != null:
|
||
focus_target.call_deferred("grab_focus")
|
||
else:
|
||
categories.get_child(_market_categories().find(market_category)).call_deferred("grab_focus")
|
||
|
||
|
||
func set_market_category(value: String) -> void:
|
||
if value not in _market_categories():
|
||
return
|
||
market_category = value
|
||
market_selected_kind = ""
|
||
market_selected_offer_id = ""
|
||
market_notice = ""
|
||
show_market(guild_company)
|
||
|
||
|
||
func select_market_offer(kind: String, offer_id: String) -> void:
|
||
var offer := _market_offer_by_id(guild_company.active_contract, kind, offer_id)
|
||
if offer.is_empty():
|
||
market_notice = _tr("该货单条目已不可用。", "That ledger entry is no longer available.")
|
||
else:
|
||
market_selected_kind = kind
|
||
market_selected_offer_id = offer_id
|
||
market_notice = ""
|
||
show_market(guild_company)
|
||
|
||
|
||
func open_recruit_review() -> void:
|
||
if market_selected_kind != "recruit" or market_selected_offer_id.is_empty():
|
||
market_notice = _tr("请先选择一名招募候选。", "Select a Recruit offer first.")
|
||
show_market(guild_company)
|
||
return
|
||
show_recruit_review(market_selected_offer_id)
|
||
|
||
|
||
func show_recruit_review(offer_id: String) -> void:
|
||
var loaded := initial_company_service.load_company()
|
||
if not loaded.ok or loaded.company.active_contract.is_empty():
|
||
market_notice = _tr("无法读取稳定货单。", "The stable Market ledger could not be loaded.")
|
||
show_market()
|
||
return
|
||
var offer := _market_offer_by_id(loaded.company.active_contract, "recruit", offer_id)
|
||
if offer.is_empty():
|
||
market_notice = _tr("该招募候选已不可用。", "That Recruit offer is no longer available.")
|
||
show_market(loaded.company)
|
||
return
|
||
guild_company = loaded.company.duplicate(true)
|
||
market_category = "recruit"
|
||
market_selected_kind = "recruit"
|
||
market_selected_offer_id = offer_id
|
||
current_screen = "recruit_review"
|
||
_build_world_base()
|
||
var frame_safe := _framed_page_safe("RecruitReview")
|
||
frame_safe.add_child(_entry_chrome("RecruitReviewFrame", "initial_company_scene_frame"))
|
||
var safe := _content_page_safe("RecruitReview")
|
||
var layout := VBoxContainer.new()
|
||
layout.name = "RecruitReviewLayout"
|
||
layout.add_theme_constant_override("separation", 6)
|
||
safe.add_child(layout)
|
||
var contract: Dictionary = guild_company.active_contract
|
||
var header := HBoxContainer.new()
|
||
header.custom_minimum_size = Vector2(0, 44)
|
||
layout.add_child(header)
|
||
var heading := VBoxContainer.new()
|
||
heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
header.add_child(heading)
|
||
heading.add_child(_label(_tr("招募详情", "RECRUIT REVIEW"), _scaled(20), Color("#f5ead0")))
|
||
heading.add_child(_label(_market_recruit_name(offer), _scaled(11), Color("#e8d39d")))
|
||
var price := _label(_tr("%d 金币", "%d COIN") % int(offer.list_price), _scaled(15), Color("#f0ca75"))
|
||
price.name = "RecruitReviewPrice"
|
||
price.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
header.add_child(price)
|
||
var body := HBoxContainer.new()
|
||
body.name = "RecruitReviewBody"
|
||
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
body.add_theme_constant_override("separation", 18)
|
||
layout.add_child(body)
|
||
var portrait_stage := Control.new()
|
||
portrait_stage.name = "RecruitReviewPortrait"
|
||
portrait_stage.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
portrait_stage.size_flags_stretch_ratio = 0.9
|
||
body.add_child(portrait_stage)
|
||
var portrait := TextureRect.new()
|
||
portrait.name = "RecruitReviewPortraitTexture"
|
||
portrait.texture = InitialCompanyArt.texture_for(offer)
|
||
portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
portrait.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
|
||
portrait.anchor_left = 0.08
|
||
portrait.anchor_right = 0.92
|
||
portrait.anchor_bottom = 1.0
|
||
portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
portrait_stage.add_child(portrait)
|
||
var scroll := ScrollContainer.new()
|
||
scroll.name = "RecruitReviewScroll"
|
||
scroll.custom_minimum_size = Vector2(minf(430.0, size.x * 0.52), 0)
|
||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
body.add_child(scroll)
|
||
var details := VBoxContainer.new()
|
||
details.name = "RecruitReviewDetails"
|
||
details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
details.add_theme_constant_override("separation", 5)
|
||
scroll.add_child(details)
|
||
_add_review_detail(details, "RecruitReviewProfession", _tr("职业", "PROFESSION"), "%s · %s" % [_content_name("professions", String(offer.profession_id)), _profession_job(offer)])
|
||
_add_review_detail(details, "RecruitReviewTrait", _tr("特质", "TRAIT"), "%s\n%s: %s\n%s: %s" % [_content_name("traits", String(offer.trait_id)), _tr("收益", "Benefit"), _trait_value(offer, "benefit"), _tr("代价", "Cost"), _trait_value(offer, "cost")])
|
||
_add_review_detail(details, "RecruitReviewAttributes", _tr("属性", "ATTRIBUTES"), _attribute_summary(offer))
|
||
_add_review_detail(details, "RecruitReviewItem", _tr("初始装备", "STARTING ITEM"), "%s · %s" % [_content_name("equipment", String(offer.starting_equipment_id)), _equipment_effect(offer)])
|
||
_add_review_detail(details, "RecruitReviewHistory", _tr("成长记录", "GROWTH & HISTORY"), _tr("等级 1 · 经验 0 · 参战 0 · 击倒 0", "Level 1 · XP 0 · Battles 0 · Knockouts 0"))
|
||
var offer_available: bool = offer_id in contract.available_recruit_offer_ids
|
||
var opening_hires_complete: bool = contract.hire_receipts.size() >= 2
|
||
var impact_text := _tr("远征团 %d → %d · 队列容量保持 %d;编队在下一步决定。", "Company %d → %d · Line capacity remains %d; formation is decided next.") % [guild_company.owned_recruits.size(), guild_company.owned_recruits.size() + 1, int(contract.line_capacity)]
|
||
if not offer_available:
|
||
impact_text = _tr("该成员已加入远征团;队列容量仍为 %d。", "This Recruit already joined the Company; Line capacity remains %d.") % int(contract.line_capacity)
|
||
elif opening_hires_complete:
|
||
impact_text = _tr("开场两笔招募已完成;先建立编队。队列容量仍为 %d。", "The two opening hires are complete; establish formation next. Line capacity remains %d.") % int(contract.line_capacity)
|
||
_add_review_detail(details, "RecruitReviewImpact", _tr("远征团影响", "COMPANY IMPACT"), impact_text)
|
||
var footer := HBoxContainer.new()
|
||
footer.name = "RecruitReviewFooter"
|
||
footer.custom_minimum_size = Vector2(0, 48)
|
||
layout.add_child(footer)
|
||
var cancel := _company_button("RecruitReviewCancel", _tr("取消并返回同一货单", "Cancel to Same Offer"), cancel_recruit_review, "company_utility", "back")
|
||
cancel.custom_minimum_size = Vector2(250, 48)
|
||
footer.add_child(cancel)
|
||
var footer_spacer := Control.new()
|
||
footer_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
footer.add_child(footer_spacer)
|
||
if offer_available and not opening_hires_complete and int(contract.coin) >= int(offer.list_price):
|
||
var hire := _company_button("RecruitHire", _tr("招募 · %d 金币", "Recruit · %d Coin") % int(offer.list_price), hire_selected_recruit, "company_primary", "confirm")
|
||
hire.custom_minimum_size = Vector2(230, 48)
|
||
hire.tooltip_text = _tr("确认招募 %s,花费 %d 金币,远征团 %d → %d", "Recruit %s for %d Coin; Company %d → %d") % [_market_recruit_name(offer), int(offer.list_price), guild_company.owned_recruits.size(), guild_company.owned_recruits.size() + 1]
|
||
footer.add_child(hire)
|
||
else:
|
||
var state_text := _tr("已招募 · 无重复扣款", "HIRED · NO DUPLICATE CHARGE") if not offer_available else _tr("开场招募已完成 · 下一步建立编队", "OPENING HIRES COMPLETE · FORMATION NEXT") if opening_hires_complete else _tr("金币不足 · 未改变状态", "INSUFFICIENT COIN · NO CHANGE")
|
||
var state := _label(state_text, _scaled(10), Color("#9ed7aa"))
|
||
state.name = "RecruitHireState"
|
||
state.custom_minimum_size = Vector2(190, 0)
|
||
state.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
state.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
footer.add_child(state)
|
||
cancel.call_deferred("grab_focus")
|
||
|
||
|
||
func cancel_recruit_review() -> void:
|
||
market_notice = ""
|
||
show_market(guild_company)
|
||
|
||
|
||
func hire_selected_recruit() -> void:
|
||
if market_selected_kind != "recruit" or market_selected_offer_id.is_empty():
|
||
market_notice = _tr("没有可提交的招募候选。", "No Recruit offer is ready to commit.")
|
||
show_market(guild_company)
|
||
return
|
||
var contract: Dictionary = guild_company.active_contract
|
||
var offer := _market_offer_by_id(contract, "recruit", market_selected_offer_id)
|
||
if offer.is_empty():
|
||
market_notice = _tr("该招募候选已不可用。", "That Recruit offer is unavailable.")
|
||
show_market(guild_company)
|
||
return
|
||
guild_contract_service.company_path = initial_company_service.company_path
|
||
var operation_id := ("%s|hire-recruit|%s" % [String(guild_company.company_id), market_selected_offer_id]).sha256_text().substr(0, 32)
|
||
var result: Dictionary = guild_contract_service.hire_recruit(operation_id, market_selected_offer_id, int(offer.list_price))
|
||
if not result.ok:
|
||
market_notice = _hire_error_text(String(result.get("code", "HIRE_FAILED")))
|
||
show_recruit_review(market_selected_offer_id)
|
||
return
|
||
guild_company = result.company.duplicate(true)
|
||
var receipt: Dictionary = result.receipt
|
||
market_category = "recruit"
|
||
market_selected_kind = ""
|
||
market_selected_offer_id = ""
|
||
market_notice = _tr("已招募 %s · 金币 %d → %d · 远征团 %d → %d", "Recruited %s · Coin %d → %d · Company %d → %d") % [_market_recruit_name(offer), int(receipt.coin_before), int(receipt.coin_after), int(receipt.company_before), int(receipt.company_after)]
|
||
show_market(guild_company)
|
||
|
||
|
||
func _market_category_button(category: String) -> Button:
|
||
var selected: bool = category == market_category
|
||
var title: String = {
|
||
"recruit": _tr("招募 3", "Recruit 3"),
|
||
"equipment": _tr("装备 4", "Equipment 4"),
|
||
"battle": _tr("战斗 3", "Battle 3"),
|
||
}[category]
|
||
var button := _entry_button_base("MarketCategory%s" % category.capitalize(), set_market_category.bind(category), "company_primary" if selected else "company_secondary")
|
||
button.custom_minimum_size = Vector2(0, 38)
|
||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
button.add_child(_button_label("ButtonLabel", String(title), _scaled(11), Color("#f5ead0")))
|
||
button.set_meta("selected", selected)
|
||
return button
|
||
|
||
|
||
func _market_offer_row(kind: String, offer: Dictionary, index: int) -> Button:
|
||
var offer_id: String = _market_offer_id(kind, offer)
|
||
var selected: bool = kind == market_selected_kind and offer_id == market_selected_offer_id
|
||
var button := _entry_button_base("MarketOffer%s" % offer_id, select_market_offer.bind(kind, offer_id), "company_primary" if selected else "company_secondary")
|
||
button.set_meta("offer_id", offer_id)
|
||
button.set_meta("offer_kind", kind)
|
||
button.set_meta("selected", selected)
|
||
var short_market := size.y < 380.0
|
||
button.custom_minimum_size = Vector2(0, 56 if kind == "recruit" and short_market else 62 if kind == "recruit" else 45 if short_market else 49)
|
||
button.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
var left_inset := 62.0 if kind == "recruit" else 14.0
|
||
if kind == "recruit":
|
||
var portrait := TextureRect.new()
|
||
portrait.texture = InitialCompanyArt.texture_for(offer)
|
||
portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
portrait.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
|
||
portrait.position = Vector2(7, 3)
|
||
portrait.size = Vector2(46 if short_market else 48, 50 if short_market else 56)
|
||
portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
button.add_child(portrait)
|
||
var title := _label(_market_offer_title(kind, offer), _scaled(13), Color("#f5ead0"))
|
||
title.position = Vector2(left_inset, 5)
|
||
title.size = Vector2(maxf(80.0, 240.0 - left_inset), 22)
|
||
title.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
title.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
button.add_child(title)
|
||
var detail := _label(_market_offer_detail(kind, offer), _scaled(9), Color("#aebbc2"))
|
||
detail.position = Vector2(left_inset, 25)
|
||
detail.size = Vector2(maxf(80.0, 315.0 - left_inset), 30 if kind == "recruit" else 19)
|
||
detail.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
detail.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
button.add_child(detail)
|
||
var price := _label(_market_offer_price(kind, offer), _scaled(11), Color("#f0ca75"))
|
||
price.anchor_left = 1.0
|
||
price.anchor_right = 1.0
|
||
price.offset_left = -88
|
||
price.offset_right = -10
|
||
price.offset_top = 5
|
||
price.offset_bottom = 27
|
||
price.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||
price.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
button.add_child(price)
|
||
return button
|
||
|
||
|
||
func _render_market_context(context: VBoxContainer, contract: Dictionary) -> void:
|
||
context.add_child(_label(_tr("检查", "INSPECT"), _scaled(10), Color("#e8d39d")))
|
||
if market_selected_offer_id.is_empty():
|
||
var empty := _label(_tr("选择任意货单条目查看完整信息。\n没有候选被自动选择或标记为推荐。", "Select any ledger entry to inspect it.\nNo offer is auto-selected or marked recommended."), _scaled(13), Color("#f5ead0"))
|
||
empty.name = "MarketNoSelection"
|
||
empty.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
empty.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
context.add_child(empty)
|
||
if not market_notice.is_empty():
|
||
var receipt_notice := _label(market_notice, _scaled(11), Color("#9ed7aa"))
|
||
receipt_notice.name = "MarketReceiptNotice"
|
||
receipt_notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
context.add_child(receipt_notice)
|
||
return
|
||
var offer := _market_offer_by_id(contract, market_selected_kind, market_selected_offer_id)
|
||
if offer.is_empty():
|
||
context.add_child(_label(_tr("该条目已不可用。", "That entry is unavailable."), _scaled(13), Color("#ffd4d4")))
|
||
return
|
||
context.add_child(_label(_market_offer_title(market_selected_kind, offer), _scaled(17), Color("#f5ead0")))
|
||
var detail := _label(_market_context_detail(market_selected_kind, offer), _scaled(11), Color("#aebbc2"))
|
||
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
detail.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
context.add_child(detail)
|
||
context.add_child(_label(_market_offer_price(market_selected_kind, offer), _scaled(14), Color("#f0ca75")))
|
||
if market_selected_kind == "recruit":
|
||
var inspect := _company_button("MarketReviewRecruit", _tr("查看招募详情", "Review Recruit"), open_recruit_review, "company_primary", "inspect")
|
||
inspect.custom_minimum_size = Vector2(0, 50)
|
||
context.add_child(inspect)
|
||
if not market_notice.is_empty():
|
||
var notice := _label(market_notice, _scaled(10), Color("#f0ca75"))
|
||
notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
context.add_child(notice)
|
||
|
||
|
||
func _market_categories() -> Array[String]:
|
||
return ["recruit", "equipment", "battle"]
|
||
|
||
|
||
func _market_collection_field(kind: String) -> String:
|
||
return {"recruit": "recruit_offers", "equipment": "equipment_offers", "battle": "battle_offers"}.get(kind, "recruit_offers")
|
||
|
||
|
||
func _market_offer_id(kind: String, offer: Dictionary) -> String:
|
||
return String(offer.get("source_offer_id" if kind == "recruit" else "offer_id", ""))
|
||
|
||
|
||
func _market_offer_by_id(contract: Dictionary, kind: String, offer_id: String) -> Dictionary:
|
||
for offer in contract.market_board.get(_market_collection_field(kind), []):
|
||
if _market_offer_id(kind, offer) == offer_id:
|
||
return Dictionary(offer).duplicate(true)
|
||
return {}
|
||
|
||
|
||
func _market_offer_title(kind: String, offer: Dictionary) -> String:
|
||
match kind:
|
||
"recruit": return _market_recruit_name(offer)
|
||
"equipment": return _content_name("equipment", String(offer.catalog_id))
|
||
"battle": return _tr("路线 %d · 风压 %d", "Route %d · Risk %d") % [int(offer.get("risk", 0)) + 1, int(offer.get("risk", 0))]
|
||
return ""
|
||
|
||
|
||
func _market_offer_detail(kind: String, offer: Dictionary) -> String:
|
||
match kind:
|
||
"recruit":
|
||
var detail := "%s · %s" % [_content_name("professions", String(offer.profession_id)), _content_name("traits", String(offer.trait_id))]
|
||
if not guild_company.is_empty() and String(offer.source_offer_id) not in guild_company.active_contract.available_recruit_offer_ids:
|
||
detail = "%s · %s" % [_tr("已招募", "HIRED"), detail]
|
||
return detail
|
||
"equipment": return _equipment_effect_by_id(String(offer.catalog_id))
|
||
"battle": return _tr("%d 波 · %d 名敌人", "%d waves · %d enemies") % [int(offer.get("wave_count", 0)), Array(offer.get("enemy_ids", [])).size()]
|
||
return ""
|
||
|
||
|
||
func _market_offer_price(kind: String, offer: Dictionary) -> String:
|
||
if kind == "battle":
|
||
return _tr("入场 0", "Entry 0")
|
||
if kind == "recruit" and not guild_company.is_empty() and String(offer.source_offer_id) not in guild_company.active_contract.available_recruit_offer_ids:
|
||
return _tr("已招募", "Hired")
|
||
return _tr("%d 金币", "%d Coin") % int(offer.get("list_price", 0))
|
||
|
||
|
||
func _market_context_detail(kind: String, offer: Dictionary) -> String:
|
||
match kind:
|
||
"recruit":
|
||
return "%s\n%s: %s\n%s: %s\n%s" % [_content_name("professions", String(offer.profession_id)), _tr("特质收益", "Trait benefit"), _trait_value(offer, "benefit"), _tr("特质代价", "Trait cost"), _trait_value(offer, "cost"), _attribute_summary(offer)]
|
||
"equipment":
|
||
var row: Dictionary = RuntimeContentRegistry.rows_by_id(market_content, "equipment").get(String(offer.catalog_id), {})
|
||
var locale := String(settings.get("locale", "zh_CN"))
|
||
return "%s\n%s: %s\n%s: %s" % [MarketLocalization.equipment_value(String(offer.catalog_id), "effect", String(row.get("effect", "")), locale), _tr("适用", "Fit"), MarketLocalization.equipment_value(String(offer.catalog_id), "fit", String(row.get("fit", "")), locale), _tr("取舍", "Tradeoff"), MarketLocalization.equipment_value(String(offer.catalog_id), "tradeoff", String(row.get("tradeoff", "")), locale)]
|
||
"battle":
|
||
var enemy_names: Array[String] = []
|
||
for enemy_id in offer.get("enemy_ids", []):
|
||
enemy_names.append(_content_name("enemies", String(enemy_id)))
|
||
var risk_copy := _tr("稳健压力", "steady pressure") if int(offer.get("risk", 0)) == 0 else _tr("加剧压力", "elevated pressure") if int(offer.get("risk", 0)) == 1 else _tr("高压路线", "high-pressure route")
|
||
return _tr("已知压力:%s\n敌方:%s\n选择路线不会在此页面开始战斗。", "Known pressure: %s\nEnemies: %s\nSelecting a route does not start battle from this page.") % [risk_copy, ", ".join(enemy_names)]
|
||
return ""
|
||
|
||
|
||
func _market_recruit_name(offer: Dictionary) -> String:
|
||
var tokens: Array = offer.get("display_name_tokens", [])
|
||
var parts: Array[String] = []
|
||
for token in tokens:
|
||
var segments := String(token).split(".")
|
||
parts.append(String(segments[segments.size() - 1]))
|
||
return " ".join(parts)
|
||
|
||
|
||
func _content_name(family: String, row_id: String) -> String:
|
||
var locale := String(settings.get("locale", "zh_CN"))
|
||
return RuntimeContentRegistry.localized_name(market_content, row_id, locale)
|
||
|
||
|
||
func _profession_job(offer: Dictionary) -> String:
|
||
var row: Dictionary = RuntimeContentRegistry.rows_by_id(market_content, "professions").get(String(offer.profession_id), {})
|
||
var fallback := String(row.get("role", ""))
|
||
return MarketLocalization.profession_job(String(offer.profession_id), fallback, String(settings.get("locale", "zh_CN")))
|
||
|
||
|
||
func _trait_value(offer: Dictionary, field: String) -> String:
|
||
var row: Dictionary = RuntimeContentRegistry.rows_by_id(market_content, "traits").get(String(offer.trait_id), {})
|
||
var fallback := String(row.get(field, ""))
|
||
return MarketLocalization.trait_value(String(offer.trait_id), field, fallback, String(settings.get("locale", "zh_CN")))
|
||
|
||
|
||
func _equipment_effect(offer: Dictionary) -> String:
|
||
return _equipment_effect_by_id(String(offer.starting_equipment_id))
|
||
|
||
|
||
func _equipment_effect_by_id(item_id: String) -> String:
|
||
var row: Dictionary = RuntimeContentRegistry.rows_by_id(market_content, "equipment").get(item_id, {})
|
||
var fallback := String(row.get("effect", ""))
|
||
return MarketLocalization.equipment_value(item_id, "effect", fallback, String(settings.get("locale", "zh_CN")))
|
||
|
||
|
||
func _attribute_summary(offer: Dictionary) -> String:
|
||
var attributes: Dictionary = offer.get("attribute_rolls", {})
|
||
return "VIG %s · FOR %s · FIN %s · RES %s · FOC %s" % [String(attributes.get("vigor", "-")), String(attributes.get("force", "-")), String(attributes.get("finesse", "-")), String(attributes.get("resolve", "-")), String(attributes.get("focus", "-"))]
|
||
|
||
|
||
func _add_review_detail(parent: VBoxContainer, node_name: String, heading: String, value: String) -> void:
|
||
var line := _label("%s\n%s" % [heading, value], _scaled(10), Color("#f5ead0"))
|
||
line.name = node_name
|
||
line.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
line.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
parent.add_child(line)
|
||
|
||
|
||
func back_to_guild() -> void:
|
||
var loaded := initial_company_service.load_company()
|
||
if loaded.ok:
|
||
show_guild_handoff(loaded.company)
|
||
else:
|
||
title_notice = _tr("无法读取远征团。", "The Company could not be loaded.") + " [%s]" % String(loaded.code)
|
||
show_title()
|
||
|
||
|
||
func _framed_page_safe(prefix: String) -> MarginContainer:
|
||
var safe := MarginContainer.new()
|
||
safe.name = "%sFrameSafeArea" % prefix
|
||
safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
safe.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
var margin := maxi(12, int(settings.get("safe_area", 24)))
|
||
var vertical_margin := 12 if size.y < 500.0 else margin
|
||
safe.add_theme_constant_override("margin_left", margin)
|
||
safe.add_theme_constant_override("margin_right", margin)
|
||
safe.add_theme_constant_override("margin_top", vertical_margin)
|
||
safe.add_theme_constant_override("margin_bottom", vertical_margin)
|
||
add_child(safe)
|
||
return safe
|
||
|
||
|
||
func _content_page_safe(prefix: String) -> MarginContainer:
|
||
var safe := MarginContainer.new()
|
||
safe.name = "%sSafeArea" % prefix
|
||
safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
var margin := maxi(12, int(settings.get("safe_area", 24)))
|
||
var vertical_margin := 12 if size.y < 500.0 else margin
|
||
safe.add_theme_constant_override("margin_left", margin + 12)
|
||
safe.add_theme_constant_override("margin_right", margin + 12)
|
||
safe.add_theme_constant_override("margin_top", vertical_margin + 8)
|
||
safe.add_theme_constant_override("margin_bottom", vertical_margin + 8)
|
||
add_child(safe)
|
||
return safe
|
||
|
||
|
||
func _contract_company_scene(recruits: Array) -> Control:
|
||
var stage := Control.new()
|
||
stage.name = "ContractCompanyScene"
|
||
stage.custom_minimum_size = Vector2(0, 105)
|
||
stage.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
stage.clip_contents = true
|
||
for index in range(mini(2, recruits.size())):
|
||
var portrait := TextureRect.new()
|
||
portrait.name = "ContractCompanyMember_%d" % index
|
||
portrait.texture = InitialCompanyArt.texture_for(recruits[index])
|
||
portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
portrait.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
|
||
portrait.anchor_left = 0.02 + index * 0.55
|
||
portrait.anchor_top = 0.02
|
||
portrait.anchor_right = 0.43 + index * 0.55
|
||
portrait.anchor_bottom = 1.0
|
||
portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
stage.add_child(portrait)
|
||
var board = GuildDestinationSticker.new().configure("contract")
|
||
board.name = "ContractBoardSticker"
|
||
board.anchor_left = 0.39
|
||
board.anchor_top = 0.20
|
||
board.anchor_right = 0.61
|
||
board.anchor_bottom = 0.78
|
||
board.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
stage.add_child(board)
|
||
return stage
|
||
|
||
|
||
func _contract_segment_button(difficulty: String) -> Button:
|
||
var selected := difficulty == contract_difficulty
|
||
var node_name := "ContractDifficulty%s" % difficulty.to_pascal_case().replace("_", "")
|
||
var button := _entry_button_base(node_name, select_contract_difficulty.bind(difficulty), "company_primary" if selected else "company_secondary")
|
||
button.custom_minimum_size = Vector2(0, 44)
|
||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
button.tooltip_text = _difficulty_copy(difficulty)
|
||
var label := _button_label("ButtonLabel", _difficulty_name(difficulty), _scaled(10), Color("#f5ead0"))
|
||
label.offset_left = 5
|
||
label.offset_right = -5
|
||
button.add_child(label)
|
||
button.set_meta("selected", selected)
|
||
return button
|
||
|
||
|
||
func _difficulty_name(value: String) -> String:
|
||
match value:
|
||
"wayfinder": return _tr("引路", "Wayfinder")
|
||
"stormbound": return _tr("风暴", "Stormbound")
|
||
"iron_oath": return _tr("铁誓", "Iron Oath")
|
||
return _tr("标准", "Standard")
|
||
|
||
|
||
func _difficulty_copy(value: String) -> String:
|
||
match value:
|
||
"wayfinder": return _tr("较宽容的敌方参数;所有系统、进度与记录仍然可用。", "More forgiving enemy parameters; all systems, progression and Records remain available.")
|
||
"stormbound": return _tr("更高压力与回报;目标规则和远征团所有权不变。", "Higher pressure and rewards; target rules and Company ownership remain unchanged.")
|
||
"iron_oath": return _tr("最高压力;只改变敌人与回报参数,不隐藏规则。", "Highest pressure; only enemy and reward parameters change, never hidden rules.")
|
||
return _tr("完整的标准平衡;不需要额外永久战力。", "The complete standard balance; no extra permanent power is required.")
|
||
|
||
|
||
func _contract_error_text(code: String) -> String:
|
||
match code:
|
||
"CONTRACT_ALREADY_ACTIVE": return _tr("已有进行中的委托。", "A Contract is already active.")
|
||
"CONTRACT_LOCKED": return _tr("该委托尚未解锁。", "That Contract is still locked.")
|
||
"CONTRACT_DIFFICULTY_INVALID": return _tr("难度无效,请重新选择。", "The difficulty is invalid; choose again.")
|
||
return _tr("无法建立委托;远征团与资源未改变。", "The Contract could not be created; Company and resources are unchanged.") + " [%s]" % code
|
||
|
||
|
||
func _hire_error_text(code: String) -> String:
|
||
match code:
|
||
"HIRE_PRICE_MISMATCH": return _tr("价格已改变;未扣除金币。", "The price changed; no Coin was charged.")
|
||
"HIRE_INSUFFICIENT_COIN": return _tr("金币不足;未改变远征团。", "Insufficient Coin; Company state is unchanged.")
|
||
"HIRE_OFFER_UNAVAILABLE": return _tr("该招募候选已不可用;未扣除金币。", "That Recruit offer is unavailable; no Coin was charged.")
|
||
"OPENING_HIRES_COMPLETE": return _tr("开场两笔招募已完成;下一步建立编队。", "The two opening hires are complete; establish formation next.")
|
||
return _tr("招募未完成;金币与远征团保持不变。", "Recruitment did not complete; Coin and Company are unchanged.") + " [%s]" % code
|
||
|
||
|
||
func _guild_member_column(index: int, recruit: Dictionary) -> Control:
|
||
var box := VBoxContainer.new()
|
||
box.name = "GuildMember_%d" % index
|
||
box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
box.size_flags_stretch_ratio = 1.0
|
||
box.add_theme_constant_override("separation", 2)
|
||
box.add_child(_label(_tr("远征团成员 %d" % (index + 1), "COMPANY MEMBER %d" % (index + 1)), _scaled(10), Color("#73b9e7")))
|
||
var portrait_stage := Control.new()
|
||
portrait_stage.name = "GuildMemberPortrait_%d" % index
|
||
portrait_stage.custom_minimum_size = Vector2(90, 76)
|
||
portrait_stage.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
portrait_stage.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
var tint := ColorRect.new()
|
||
tint.color = Color(InitialCompanyArt.accent_for(recruit), 0.10)
|
||
tint.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
tint.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
portrait_stage.add_child(tint)
|
||
var portrait := TextureRect.new()
|
||
portrait.name = "GuildMemberPortraitTexture_%d" % index
|
||
portrait.texture = InitialCompanyArt.texture_for(recruit)
|
||
portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
portrait.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
|
||
portrait.anchor_left = 0.12
|
||
portrait.anchor_right = 0.88
|
||
portrait.anchor_bottom = 1.0
|
||
portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
portrait_stage.add_child(portrait)
|
||
box.add_child(portrait_stage)
|
||
var locale := String(settings.get("locale", "zh_CN"))
|
||
var name := _label(String(recruit.rendered_name_snapshot.get(locale, recruit.rendered_name_snapshot.en)), _scaled(16), Color("#f5ead0"))
|
||
name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
box.add_child(name)
|
||
box.add_child(_label("%s · %s" % [String(recruit.profession_name.get(locale, recruit.profession_name.en)), _tr("等级 1", "Level 1")], _scaled(10), Color("#e8d39d")))
|
||
box.add_child(_label(String(recruit.trait_name.get(locale, recruit.trait_name.en)), _scaled(10), Color("#9ed7aa")))
|
||
var promise := _label(_tr("跨委托保留 · 击倒后下关归队", "Persists across Contracts · returns next stage after knockout"), _scaled(9), Color("#73b9e7"))
|
||
promise.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
box.add_child(promise)
|
||
return box
|
||
|
||
|
||
func _guild_destination_button(node_name: String, kind: String, title: String, callable: Callable) -> Button:
|
||
var button := _entry_button_base(node_name, callable, "company_destination")
|
||
button.custom_minimum_size = Vector2(190, 52)
|
||
var sticker = GuildDestinationSticker.new().configure(kind)
|
||
sticker.name = "Guild%sSticker" % kind.capitalize()
|
||
sticker.position = Vector2(10, 5)
|
||
sticker.size = Vector2(58, 42)
|
||
button.add_child(sticker)
|
||
var label := _label(title, _scaled(15), Color("#aebbc2"))
|
||
label.position = Vector2(76, 0)
|
||
label.size = Vector2(maxf(60.0, button.custom_minimum_size.x - 88.0), 52)
|
||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
button.add_child(label)
|
||
return button
|
||
|
||
|
||
func _company_button(node_name: String, text_value: String, callback: Callable, variant: String, icon: String) -> Button:
|
||
var button := Button.new()
|
||
button.name = node_name
|
||
button.flat = true
|
||
button.tooltip_text = text_value
|
||
button.custom_minimum_size = Vector2(190, 52)
|
||
for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]:
|
||
button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new())
|
||
button.add_child(_entry_chrome("Chrome", variant, icon))
|
||
var color := Color("#202a31") if variant == "company_utility" else Color("#f5ead0")
|
||
var label := _button_label("ButtonLabel", text_value, _scaled(16), color)
|
||
label.offset_left = 30.0
|
||
label.offset_right = -10.0
|
||
button.add_child(label)
|
||
button.set_meta("entry_chrome_variant", variant)
|
||
button.pressed.connect(callback, CONNECT_DEFERRED)
|
||
return button
|
||
|
||
|
||
func _set_guild_notice(value: String) -> void:
|
||
var label := get_node_or_null("GuildHandoffSafeArea/GuildHandoffLayout/GuildHandoffBody/GuildNextActions/GuildNotice") as Label
|
||
if label != null:
|
||
label.text = value
|
||
|
||
|
||
func show_settings_from_guild() -> void:
|
||
settings_caller = "guild_handoff"
|
||
show_settings()
|
||
|
||
|
||
func show_settings_placeholder() -> void:
|
||
settings_caller = "title"
|
||
show_settings()
|
||
|
||
|
||
func show_credits_placeholder() -> void:
|
||
settings_caller = "title"
|
||
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)
|
||
var short_layout := size.y < 500.0
|
||
safe.add_theme_constant_override("margin_top", 12 if short_layout else 24)
|
||
safe.add_theme_constant_override("margin_bottom", 12 if short_layout else 24)
|
||
add_child(safe)
|
||
safe.add_child(_entry_chrome("SettingsFrame", "settings_frame"))
|
||
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(24 if short_layout else 26), Color("#f5ead0"))
|
||
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
heading.add_child(title)
|
||
heading.add_child(_label(_tr("来源:标题首页", "Caller: Title"), _scaled(12), Color("#e8d39d")))
|
||
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: Container
|
||
if short_layout:
|
||
rail = GridContainer.new()
|
||
rail.columns = 3
|
||
rail.name = "SettingsCategories"
|
||
rail.add_theme_constant_override("h_separation", 5)
|
||
rail.add_theme_constant_override("v_separation", 4)
|
||
outer.move_child(workspace, outer.get_child_count() - 1)
|
||
outer.add_child(rail)
|
||
outer.move_child(rail, workspace.get_index())
|
||
else:
|
||
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(0 if short_layout else 174, 38 if short_layout else 44)
|
||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
var selected: bool = settings_category == String(row[0])
|
||
button.set_meta("selected_entry_tab", selected)
|
||
_set_entry_button_disabled(button, selected, selected)
|
||
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(12 if short_layout else 13), Color("#f0ca75"))
|
||
notice.name = "SettingsNotice"
|
||
notice.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
footer.add_child(notice)
|
||
var back := _tool_button("SettingsBack", _tr("返回", "Back"), request_settings_back)
|
||
back.custom_minimum_size = Vector2(110, 44 if short_layout else 48)
|
||
footer.add_child(back)
|
||
var discard := _tool_button("SettingsDiscard", _tr("放弃并返回", "Discard & Back"), discard_settings)
|
||
discard.custom_minimum_size = Vector2(160, 44 if short_layout else 48)
|
||
footer.add_child(discard)
|
||
var apply := _action_button("SettingsApply", _tr("应用", "Apply"), apply_settings)
|
||
apply.custom_minimum_size = Vector2(118 if short_layout else 190, 44 if short_layout else 52)
|
||
footer.add_child(apply)
|
||
|
||
|
||
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("#aebbc2")))
|
||
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("#f5ead0")))
|
||
"credits":
|
||
content.add_child(_settings_heading(_tr("制作人员、许可与数据", "CREDITS, LICENSES & DATA")))
|
||
var credits := _label(_credits_text(), _scaled(14), Color("#f5ead0"))
|
||
credits.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
credits.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
content.add_child(credits)
|
||
|
||
|
||
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:
|
||
_return_from_settings()
|
||
|
||
|
||
func discard_settings() -> void:
|
||
settings_draft = {}
|
||
settings_notice = ""
|
||
_return_from_settings()
|
||
|
||
|
||
func _return_from_settings() -> void:
|
||
if settings_caller == "guild_handoff":
|
||
show_guild_handoff(guild_company)
|
||
else:
|
||
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(19), Color("#e8d39d"))
|
||
|
||
|
||
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(14), Color("#f5ead0"))
|
||
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.custom_minimum_size.y = 24
|
||
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
slider.add_theme_stylebox_override("slider", _flat_style(Color("#0d151d"), Color("#866f47"), 1, 3))
|
||
slider.add_theme_stylebox_override("grabber_area", _flat_style(Color("#73b9e7"), Color("#ead49b"), 1, 3))
|
||
slider.add_theme_stylebox_override("grabber_area_highlight", _flat_style(Color("#a9d9f2"), Color("#f5ead0"), 1, 3))
|
||
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))
|
||
_set_entry_button_disabled(button, 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)
|
||
safe.add_child(_entry_chrome("ChildFrame", "settings_frame"))
|
||
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("#f5ead0")))
|
||
var explanation := _label(body, _scaled(17), Color("#aebbc2"))
|
||
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 = ""
|
||
settings_caller = "title"
|
||
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")
|
||
_set_entry_button_text(box.get_node("ChildBack"), _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 == "initial_company" and initial_company_page != null:
|
||
initial_company_page.request_back()
|
||
elif current_screen == "guild_handoff":
|
||
back_to_title()
|
||
elif current_screen in ["contract_choice", "market"]:
|
||
back_to_guild()
|
||
elif current_screen == "recruit_review":
|
||
cancel_recruit_review()
|
||
elif current_screen in ["settings", "credits", "exit_review"]:
|
||
_return_from_settings()
|
||
|
||
|
||
func _save_status() -> String:
|
||
var company := initial_company_service.load_company()
|
||
if company.ok:
|
||
return "valid"
|
||
if String(company.code) != "COMPANY_MISSING":
|
||
return "unsafe"
|
||
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 _entry_chrome(node_name: String, variant: String, icon := "") -> Control:
|
||
var chrome = EntryChrome.new().configure(variant, icon)
|
||
chrome.name = node_name
|
||
chrome.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
return chrome
|
||
|
||
|
||
func _title_button(node_name: String, text_value: String, callback: Callable, icon: String, variant: String) -> Button:
|
||
var button := Button.new()
|
||
button.name = node_name
|
||
button.flat = true
|
||
button.clip_contents = false
|
||
button.tooltip_text = text_value
|
||
button.add_theme_font_override("font", UI_FONT)
|
||
for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]:
|
||
button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new())
|
||
button.add_child(_entry_chrome("Chrome", variant, icon))
|
||
var label := Label.new()
|
||
label.name = "ButtonLabel"
|
||
label.text = text_value
|
||
label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
label.offset_left = 49.0
|
||
label.offset_right = -9.0
|
||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
label.add_theme_font_override("font", UI_FONT)
|
||
label.add_theme_font_size_override("font_size", _scaled(14 if variant == "utility" else 17))
|
||
label.add_theme_color_override("font_color", Color("#202a31") if variant == "utility" else Color("#f5ead0"))
|
||
button.add_child(label)
|
||
button.set_meta("entry_chrome_variant", variant)
|
||
button.pressed.connect(callback, CONNECT_DEFERRED)
|
||
return button
|
||
|
||
|
||
func _set_title_button_disabled(button: Button, disabled: bool) -> void:
|
||
button.disabled = disabled
|
||
var label := button.get_node_or_null("ButtonLabel") as Label
|
||
if label != null:
|
||
var utility := String(button.get_meta("entry_chrome_variant", "")) == "utility"
|
||
label.add_theme_color_override("font_color", Color("#626970") if utility else Color("#89939a") if disabled else Color("#f5ead0"))
|
||
|
||
|
||
func _choice_button(node_name: String, label_text: String, value_text: String, callback: Callable) -> Button:
|
||
var button := _entry_button_base(node_name, callback, "choice")
|
||
button.tooltip_text = "%s: %s" % [label_text, value_text]
|
||
button.custom_minimum_size = Vector2(0, 48)
|
||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
var label := _button_label("ChoiceLabel", label_text, _scaled(15), Color("#f5ead0"))
|
||
label.anchor_right = 0.58
|
||
label.offset_left = 14.0
|
||
label.offset_right = -6.0
|
||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||
button.add_child(label)
|
||
var value := _button_label("ValueLabel", value_text, _scaled(14), Color("#202a31"))
|
||
value.anchor_left = 0.58
|
||
value.offset_left = 6.0
|
||
value.offset_right = -10.0
|
||
button.add_child(value)
|
||
return button
|
||
|
||
|
||
func _action_button(node_name: String, text_value: String, callback: Callable) -> Button:
|
||
var button := _entry_button_base(node_name, callback, "action_primary")
|
||
button.tooltip_text = text_value
|
||
button.custom_minimum_size = Vector2(190, 52)
|
||
var label := _button_label("ButtonLabel", text_value, _scaled(16), Color("#f5ead0"))
|
||
label.offset_left = 46.0
|
||
label.offset_right = -10.0
|
||
button.add_child(label)
|
||
return button
|
||
|
||
|
||
func _tool_button(node_name: String, text_value: String, callback: Callable) -> Button:
|
||
var button := _entry_button_base(node_name, callback, "tool")
|
||
button.tooltip_text = text_value
|
||
button.custom_minimum_size = Vector2(112, 48)
|
||
var label := _button_label("ButtonLabel", text_value, _scaled(14), Color("#f5ead0"))
|
||
label.offset_left = 16.0
|
||
label.offset_right = -10.0
|
||
button.add_child(label)
|
||
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.button_pressed = enabled
|
||
check.custom_minimum_size = Vector2(0, 48)
|
||
check.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
check.flat = true
|
||
for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]:
|
||
check.add_theme_stylebox_override(style_name, StyleBoxEmpty.new())
|
||
check.add_child(_entry_chrome("Chrome", "toggle"))
|
||
var label := _button_label("ButtonLabel", text_value, _scaled(14), Color("#f5ead0"))
|
||
label.offset_left = 14.0
|
||
label.offset_right = -52.0
|
||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
check.add_child(label)
|
||
check.toggled.connect(callback)
|
||
return check
|
||
|
||
|
||
func _entry_button_base(node_name: String, callback: Callable, variant: String) -> Button:
|
||
var button := Button.new()
|
||
button.name = node_name
|
||
button.flat = true
|
||
for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]:
|
||
button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new())
|
||
button.add_child(_entry_chrome("Chrome", variant))
|
||
button.set_meta("entry_chrome_variant", variant)
|
||
button.pressed.connect(callback, CONNECT_DEFERRED)
|
||
return button
|
||
|
||
|
||
func _button_label(node_name: String, text_value: String, font_size: int, color: Color) -> Label:
|
||
var label := _label(text_value, font_size, color)
|
||
label.name = node_name
|
||
label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
return label
|
||
|
||
|
||
func _set_entry_button_disabled(button: Button, disabled: bool, selected := false) -> void:
|
||
button.disabled = disabled
|
||
for child_name in ["ButtonLabel", "ChoiceLabel", "ValueLabel"]:
|
||
var label := button.get_node_or_null(child_name) as Label
|
||
if label == null:
|
||
continue
|
||
if selected:
|
||
label.add_theme_color_override("font_color", Color("#202a31"))
|
||
elif disabled:
|
||
label.add_theme_color_override("font_color", Color("#4f565b") if child_name == "ValueLabel" else Color("#89939a"))
|
||
elif child_name == "ValueLabel":
|
||
label.add_theme_color_override("font_color", Color("#202a31"))
|
||
else:
|
||
label.add_theme_color_override("font_color", Color("#f5ead0"))
|
||
|
||
|
||
func _set_entry_button_text(button: Button, text_value: String) -> void:
|
||
var label := button.get_node_or_null("ButtonLabel") as Label
|
||
if label != null:
|
||
label.text = text_value
|
||
button.tooltip_text = text_value
|
||
|
||
|
||
func _flat_style(background: Color, border: Color, width: int, radius: int) -> StyleBoxFlat:
|
||
var style := StyleBoxFlat.new()
|
||
style.bg_color = background
|
||
style.border_color = border
|
||
style.set_border_width_all(width)
|
||
style.set_corner_radius_all(radius)
|
||
return style
|
||
|
||
|
||
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))
|