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

2017 lines
97 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name ABGEntryShell
extends Control
const BootService = preload("res://entry/boot_service.gd")
const SettingsStore = preload("res://entry/settings_store.gd")
const 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))