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

2969 lines
142 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 FormationRiskService = preload("res://entry/formation_risk_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 formation_risk_service := FormationRiskService.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()
var party_draft_order: Array[String] = []
var party_undo_order: Array[String] = []
var party_selected_recruit_id := ""
var party_preview: Dictionary = {}
var party_notice := ""
var risk_selected_id := ""
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():
if company.active_contract.has("formation") and not company.active_contract.formation.is_empty():
var formation: Dictionary = company.active_contract.formation
if String(formation.get("status", "draft")) in ["ordered", "ready"]:
show_readiness_review(company)
else:
show_party_line(company)
else:
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, 44)
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 formation_available: bool = contract.hire_receipts.size() == 2 and resolved.owned_recruits.size() == int(contract.line_capacity)
var formation := _company_button(
"MarketFormation",
_tr("建立队列", "Establish Line"),
open_party_line,
"company_primary",
"confirm"
)
formation.custom_minimum_size = Vector2(190, 44 if short_market else 48)
_set_entry_button_disabled(formation, not formation_available)
formation.tooltip_text = (
_tr("四名成员已就位;进入自动编队", "Four members are ready; enter automatic formation")
if formation_available else _tr("需要完成两笔独立招募并达到远征团 4/4", "Complete both separate hires and reach Company 4/4 first")
)
footer.add_child(formation)
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 open_party_line() -> void:
show_party_line()
func show_party_line(company := {}) -> void:
formation_risk_service.company_path = initial_company_service.company_path
var begun: Dictionary = formation_risk_service.begin_formation()
if not begun.ok:
market_notice = _formation_error_text(String(begun.get("code", "FORMATION_FAILED")))
show_market(company)
return
var resolved: Dictionary = begun.company.duplicate(true)
var formation: Dictionary = resolved.active_contract.formation
if party_draft_order.is_empty() or not _same_string_members(party_draft_order, formation.base_front_to_rear_ids):
party_draft_order.assign(formation.saved_front_to_rear_ids if not formation.saved_front_to_rear_ids.is_empty() else formation.draft_front_to_rear_ids)
party_undo_order.clear()
party_selected_recruit_id = ""
party_notice = ""
var previewed: Dictionary = formation_risk_service.preview_order(party_draft_order)
if not previewed.ok:
party_draft_order.assign(formation.base_front_to_rear_ids)
previewed = formation_risk_service.preview_order(party_draft_order)
party_preview = previewed
guild_company = resolved
current_screen = "party_line"
_build_world_base()
var frame_safe := _framed_page_safe("PartyLine")
frame_safe.add_child(_entry_chrome("PartyLineFrame", "initial_company_scene_frame"))
var safe := _content_page_safe("PartyLine")
var layout := VBoxContainer.new()
layout.name = "PartyLineLayout"
layout.add_theme_constant_override("separation", 5)
safe.add_child(layout)
var header := HBoxContainer.new()
header.name = "PartyLineHeader"
header.custom_minimum_size = Vector2(0, 42 if size.y < 500.0 else 50)
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("自动编队", "AUTOMATIC FORMATION"), _scaled(20), Color("#f5ead0")))
heading.add_child(_label(_tr("点选成员,再点目标位置交换。无需拖动。", "Select a member, then a destination to swap. Drag is not required."), _scaled(10), Color("#e8d39d")))
var status := _label(_tr("后排在左 · 0 号前排在右", "REAR LEFT · FRONT POSITION 0 RIGHT"), _scaled(11), Color("#73b9e7"))
status.name = "PartyOrientation"
status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
header.add_child(status)
var body := HBoxContainer.new()
body.name = "PartyLineBody"
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_theme_constant_override("separation", 14)
layout.add_child(body)
var stage := VBoxContainer.new()
stage.name = "PartyLineStage"
stage.size_flags_horizontal = Control.SIZE_EXPAND_FILL
stage.size_flags_stretch_ratio = 1.55
stage.add_theme_constant_override("separation", 2)
body.add_child(stage)
var threat := _label(_first_threat_text(formation.first_threat), _scaled(10), Color("#f0ca75"))
threat.name = "PartyFirstThreat"
threat.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
stage.add_child(threat)
var cord := HBoxContainer.new()
cord.name = "PartyCord"
cord.size_flags_vertical = Control.SIZE_EXPAND_FILL
cord.add_theme_constant_override("separation", 4)
stage.add_child(cord)
for visual_index in range(party_draft_order.size() - 1, -1, -1):
var recruit_id := String(party_draft_order[visual_index])
cord.add_child(_party_member_button(visual_index, _recruit_by_id(resolved, recruit_id)))
var endpoints := HBoxContainer.new()
endpoints.name = "PartyEndpoints"
stage.add_child(endpoints)
var rear := _label(_tr("◁ 受保护后排", "< PROTECTED REAR"), _scaled(10), Color("#9ed7aa"))
rear.size_flags_horizontal = Control.SIZE_EXPAND_FILL
endpoints.add_child(rear)
var front := _label(_tr("暴露前排 ▷", "EXPOSED FRONT >"), _scaled(10), Color("#f0ca75"))
front.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
front.size_flags_horizontal = Control.SIZE_EXPAND_FILL
endpoints.add_child(front)
var context := VBoxContainer.new()
context.name = "PartyContext"
context.custom_minimum_size = Vector2(minf(300.0, size.x * 0.34), 0)
context.add_theme_constant_override("separation", 4)
body.add_child(context)
_render_party_context(context, resolved)
var footer := HBoxContainer.new()
footer.name = "PartyLineFooter"
footer.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 52)
footer.add_theme_constant_override("separation", 7)
layout.add_child(footer)
footer.add_child(_company_button("PartyBack", _tr("返回公会", "Back to Guild"), cancel_party_line, "company_utility", "back"))
var undo := _company_button("PartyUndo", _tr("撤销", "Undo"), undo_party_swap, "company_secondary", "undo")
undo.custom_minimum_size = Vector2(116, footer.custom_minimum_size.y)
_set_entry_button_disabled(undo, party_undo_order.is_empty())
undo.tooltip_text = _tr("撤销上一次位置交换", "Undo the last position swap") if not undo.disabled else _tr("还没有可撤销的位置交换", "No position swap to undo")
footer.add_child(undo)
var reset := _company_button("PartyReset", _tr("重置", "Reset"), reset_party_order, "company_secondary", "reset")
reset.custom_minimum_size = Vector2(116, footer.custom_minimum_size.y)
_set_entry_button_disabled(reset, party_draft_order == formation.base_front_to_rear_ids)
reset.tooltip_text = _tr("恢复系统初始顺序", "Restore the automatic starting order") if not reset.disabled else _tr("当前已是系统初始顺序", "The automatic starting order is already restored")
footer.add_child(reset)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
footer.add_child(spacer)
var review := _company_button("PartySaveReview", _tr("保存并检查", "Save & Review"), save_party_order, "company_primary", "confirm")
review.custom_minimum_size = Vector2(210, footer.custom_minimum_size.y)
review.tooltip_text = _tr("保存当前四人顺序并打开准备检查;不会开始战斗", "Save the current four-member order and open readiness review; battle will not start")
footer.add_child(review)
var selected_button := _party_button_for_recruit(party_selected_recruit_id)
if selected_button != null:
selected_button.call_deferred("grab_focus")
else:
var front_button := find_child("PartyPosition0", true, false) as Button
if front_button != null:
front_button.call_deferred("grab_focus")
func select_party_member(recruit_id: String) -> void:
var position := party_draft_order.find(recruit_id)
if position < 0:
party_notice = _tr("该成员已不在当前队列。", "That member is no longer in the current line.")
show_party_line(guild_company)
return
if party_selected_recruit_id.is_empty():
party_selected_recruit_id = recruit_id
party_notice = _tr("已选择 %s;再点一名成员交换位置。", "%s selected; choose another member to swap positions.") % _recruit_name(_recruit_by_id(guild_company, recruit_id))
elif party_selected_recruit_id == recruit_id:
party_selected_recruit_id = ""
party_notice = _tr("已取消位置选择。", "Position selection cleared.")
else:
var from_position := party_draft_order.find(party_selected_recruit_id)
if from_position < 0:
party_selected_recruit_id = ""
party_notice = _tr("原选择已失效,请重试。", "The previous selection became stale; try again.")
else:
party_undo_order.assign(party_draft_order)
var moved_id := String(party_selected_recruit_id)
party_draft_order[from_position] = recruit_id
party_draft_order[position] = moved_id
party_selected_recruit_id = moved_id
party_notice = _tr("已预览交换:%d 号 ↔ %d 号。首要目标与保护关系已更新。", "Previewed swap: position %d <-> %d. Primary target and protection are updated.") % [from_position, position]
show_party_line(guild_company)
func undo_party_swap() -> void:
if party_undo_order.is_empty():
party_notice = _tr("还没有可撤销的位置交换。", "No position swap is available to undo.")
else:
var current := party_draft_order.duplicate()
party_draft_order.assign(party_undo_order)
party_undo_order.assign(current)
party_selected_recruit_id = ""
party_notice = _tr("已撤销上一次位置交换。", "The last position swap was undone.")
show_party_line(guild_company)
func reset_party_order() -> void:
formation_risk_service.company_path = initial_company_service.company_path
var reset: Dictionary = formation_risk_service.reset_order()
if not reset.ok:
party_notice = _formation_error_text(String(reset.get("code", "FORMATION_RESET_FAILED")))
else:
party_undo_order.assign(party_draft_order)
party_draft_order.assign(reset.front_to_rear_ids)
party_selected_recruit_id = ""
party_notice = _tr("已恢复系统初始顺序。", "Automatic starting order restored.")
show_party_line(guild_company)
func cancel_party_line() -> void:
party_draft_order.clear()
party_undo_order.clear()
party_selected_recruit_id = ""
party_preview = {}
party_notice = ""
back_to_guild()
func save_party_order() -> void:
formation_risk_service.company_path = initial_company_service.company_path
var order_hash := JSON.stringify(_canonical_value(party_draft_order)).sha256_text()
var operation_id := ("%s|save-party-order|%s" % [String(guild_company.company_id), order_hash]).sha256_text().substr(0, 32)
var saved: Dictionary = formation_risk_service.save_order(operation_id, party_draft_order)
if not saved.ok:
party_notice = _formation_error_text(String(saved.get("code", "FORMATION_SAVE_FAILED")))
show_party_line(guild_company)
return
guild_company = saved.company.duplicate(true)
party_notice = ""
show_readiness_review(guild_company)
func show_readiness_review(company := {}) -> void:
formation_risk_service.company_path = initial_company_service.company_path
var loaded: Dictionary = formation_risk_service.load_company()
if not loaded.ok:
party_notice = _formation_error_text(String(loaded.get("code", "READINESS_LOAD_FAILED")))
show_party_line(company)
return
var resolved: Dictionary = loaded.company.duplicate(true)
var formation: Dictionary = resolved.active_contract.get("formation", {})
if formation.is_empty() or formation.saved_front_to_rear_ids.is_empty():
party_notice = _tr("请先保存当前队列顺序。", "Save the current line order first.")
show_party_line(resolved)
return
guild_company = resolved
party_draft_order.assign(formation.saved_front_to_rear_ids)
party_preview = {
"relations": Array(formation.relations).duplicate(true),
"coverage": Dictionary(formation.coverage).duplicate(true),
}
current_screen = "readiness_review"
_build_world_base()
var frame_safe := _framed_page_safe("Readiness")
frame_safe.add_child(_entry_chrome("ReadinessFrame", "initial_company_scene_frame"))
var safe := _content_page_safe("Readiness")
var layout := VBoxContainer.new()
layout.name = "ReadinessLayout"
layout.add_theme_constant_override("separation", 5)
safe.add_child(layout)
var header := HBoxContainer.new()
header.custom_minimum_size = Vector2(0, 42 if size.y < 500.0 else 50)
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("准备检查", "READINESS REVIEW"), _scaled(20), Color("#f5ead0")))
heading.add_child(_label(_tr("确认队列事实,不预测胜负。", "Confirm party facts without predicting the outcome."), _scaled(10), Color("#e8d39d")))
var state := _label(
_tr("准备已接受", "PREPARATION ACCEPTED") if String(formation.status) == "ready" else _tr("已保存 · 尚未接受", "SAVED · NOT YET ACCEPTED"),
_scaled(11),
Color("#9ed7aa") if String(formation.status) == "ready" else Color("#f0ca75")
)
state.name = "ReadinessState"
state.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
header.add_child(state)
var body := HBoxContainer.new()
body.name = "ReadinessBody"
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_theme_constant_override("separation", 14)
layout.add_child(body)
var line := VBoxContainer.new()
line.name = "ReadinessLineArea"
line.size_flags_horizontal = Control.SIZE_EXPAND_FILL
line.size_flags_stretch_ratio = 1.45
line.add_theme_constant_override("separation", 3)
body.add_child(line)
line.add_child(_label(_tr("受保护后排 ◁────────▷ 暴露前排 · 0 号", "PROTECTED REAR <--------> EXPOSED FRONT · POSITION 0"), _scaled(10), Color("#73b9e7")))
var members := HBoxContainer.new()
members.name = "ReadinessMembers"
members.size_flags_vertical = Control.SIZE_EXPAND_FILL
members.add_theme_constant_override("separation", 4)
line.add_child(members)
for visual_index in range(party_draft_order.size() - 1, -1, -1):
members.add_child(_readiness_member(visual_index, _recruit_by_id(resolved, String(party_draft_order[visual_index]))))
var detail := VBoxContainer.new()
detail.name = "ReadinessFacts"
detail.custom_minimum_size = Vector2(minf(315.0, size.x * 0.35), 0)
detail.add_theme_constant_override("separation", 4)
body.add_child(detail)
detail.add_child(_label(_tr("已冻结事实", "FROZEN FACTS"), _scaled(10), Color("#e8d39d")))
var front_recruit := _recruit_by_id(resolved, String(party_draft_order[0]))
var coverage: Dictionary = formation.coverage
var facts := _label(
_tr(
"0 号首要目标:%s\n保护 %d · 远程 %d · 续航 %d · 控制 %d\n四名成员同时出战;没有自动替补。",
"Primary target at position 0: %s\nGuard %d · Reach %d · Sustain %d · Control %d\nAll four deploy together; no automatic Reserve entry."
) % [_recruit_name(front_recruit), int(coverage.protection), int(coverage.long_reach), int(coverage.sustain), int(coverage.control)],
_scaled(11),
Color("#f5ead0")
)
facts.name = "ReadinessFrozenFacts"
facts.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
facts.size_flags_vertical = Control.SIZE_EXPAND_FILL
detail.add_child(facts)
var warning := _label(_readiness_warning(formation), _scaled(10), Color("#f0ca75"))
warning.name = "ReadinessWarning"
warning.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
detail.add_child(warning)
var knockout := _label(_tr("击倒只离开当前战斗;同一成员下关归队,不会永久死亡。", "Knockout removes a member only from this battle; the same Recruit returns next stage and never dies permanently."), _scaled(9), Color("#9ed7aa"))
knockout.name = "ReadinessKnockoutRule"
knockout.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
detail.add_child(knockout)
if String(formation.status) == "ready":
var receipt := _label(_tr("准备回执已保存 · 风险选择尚未提交", "Readiness receipt saved · no risk has been committed"), _scaled(9), Color("#73b9e7"))
receipt.name = "ReadinessReceipt"
detail.add_child(receipt)
var footer := HBoxContainer.new()
footer.name = "ReadinessFooter"
footer.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 52)
footer.add_theme_constant_override("separation", 8)
layout.add_child(footer)
footer.add_child(_company_button("ReadinessRevise", _tr("修改队列", "Revise Line"), revise_readiness, "company_utility", "back"))
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
footer.add_child(spacer)
if String(formation.status) == "ready":
var risk := _company_button("ReadinessRisk", _tr("比较风险", "Compare Risk"), open_risk_board, "company_primary", "confirm")
risk.custom_minimum_size = Vector2(220, footer.custom_minimum_size.y)
risk.tooltip_text = _tr("比较首个委托的三种压力与回报", "Compare three pressure and reward options for the first Contract")
footer.add_child(risk)
else:
var accept := _company_button("ReadinessAccept", _tr("接受准备", "Accept Preparation"), accept_readiness, "company_primary", "confirm")
accept.custom_minimum_size = Vector2(230, footer.custom_minimum_size.y)
accept.tooltip_text = _tr("冻结当前四人顺序与关系;不会选择风险或开始战斗", "Freeze this four-member order and its relations; no risk or battle starts")
footer.add_child(accept)
var focus := find_child("ReadinessRisk" if String(formation.status) == "ready" else "ReadinessAccept", true, false) as Control
if focus != null and not bool(focus.get("disabled")):
focus.call_deferred("grab_focus")
else:
find_child("ReadinessRevise", true, false).call_deferred("grab_focus")
func revise_readiness() -> void:
party_selected_recruit_id = ""
party_undo_order.clear()
party_notice = _tr("已加载保存的顺序;修改后需要再次保存并接受。", "Saved order loaded; save and accept again after any change.")
show_party_line(guild_company)
func accept_readiness() -> void:
formation_risk_service.company_path = initial_company_service.company_path
var formation: Dictionary = guild_company.active_contract.formation
var order_hash := JSON.stringify(_canonical_value(formation.saved_front_to_rear_ids)).sha256_text()
var operation_id := ("%s|accept-readiness|%s" % [String(guild_company.company_id), order_hash]).sha256_text().substr(0, 32)
var accepted: Dictionary = formation_risk_service.accept_readiness(operation_id, order_hash)
if not accepted.ok:
party_notice = _formation_error_text(String(accepted.get("code", "READINESS_FAILED")))
show_readiness_review(guild_company)
return
guild_company = accepted.company.duplicate(true)
show_readiness_review(guild_company)
func open_risk_board() -> void:
show_risk_board(guild_company)
func show_risk_board(company := {}) -> void:
formation_risk_service.company_path = initial_company_service.company_path
var loaded: Dictionary = formation_risk_service.load_company()
if not loaded.ok:
party_notice = _formation_error_text(String(loaded.get("code", "RISK_LOAD_FAILED")))
show_readiness_review(company)
return
var resolved: Dictionary = loaded.company.duplicate(true)
var formation: Dictionary = resolved.active_contract.get("formation", {})
if formation.is_empty() or String(formation.get("status", "draft")) != "ready":
party_notice = _tr("请先接受准备检查。", "Accept the readiness review first.")
show_readiness_review(resolved)
return
guild_company = resolved
current_screen = "risk_board"
_build_world_base()
var frame_safe := _framed_page_safe("RiskBoard")
frame_safe.add_child(_entry_chrome("RiskBoardFrame", "initial_company_scene_frame"))
var safe := _content_page_safe("RiskBoard")
var layout := VBoxContainer.new()
layout.name = "RiskBoardLayout"
layout.add_theme_constant_override("separation", 5)
safe.add_child(layout)
var header := HBoxContainer.new()
header.custom_minimum_size = Vector2(0, 42 if size.y < 500.0 else 50)
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("风险板", "RISK BOARD"), _scaled(20), Color("#f5ead0")))
heading.add_child(_label(_tr("选择只用于比较;没有推荐答案。", "Selection is for comparison only; nothing is recommended."), _scaled(10), Color("#e8d39d")))
var party_status := _label(_tr("四人准备已冻结", "FOUR-MEMBER PARTY FROZEN"), _scaled(11), Color("#9ed7aa"))
party_status.name = "RiskPartyStatus"
party_status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
header.add_child(party_status)
var body := HBoxContainer.new()
body.name = "RiskBoardBody"
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_theme_constant_override("separation", 14)
layout.add_child(body)
var band := VBoxContainer.new()
band.name = "RiskOptionBand"
band.size_flags_horizontal = Control.SIZE_EXPAND_FILL
band.size_flags_stretch_ratio = 1.35
band.add_theme_constant_override("separation", 4)
body.add_child(band)
band.add_child(_label(_tr("压力比较带", "PRESSURE COMPARISON BAND"), _scaled(10), Color("#e8d39d")))
var options: Array = formation.risk_options
for option in options:
band.add_child(_risk_option_button(option))
var band_note := _label(_tr("已知威胁 · 未解析信息 · 入场成本 · 回报 · 恢复暴露", "Known threat · uncertainty · entry cost · reward · recovery exposure"), _scaled(9), Color("#73b9e7"))
band_note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
band.add_child(band_note)
var context := VBoxContainer.new()
context.name = "RiskContext"
context.custom_minimum_size = Vector2(minf(320.0, size.x * 0.36), 0)
context.add_theme_constant_override("separation", 4)
body.add_child(context)
_render_risk_context(context, formation)
var footer := HBoxContainer.new()
footer.name = "RiskBoardFooter"
footer.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 52)
footer.add_theme_constant_override("separation", 8)
layout.add_child(footer)
footer.add_child(_company_button("RiskBackReadiness", _tr("返回准备", "Readiness"), back_to_readiness, "company_utility", "back"))
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
footer.add_child(spacer)
var review := _company_button("RiskReview", _tr("检查选项", "Review Option"), open_risk_commitment, "company_primary", "inspect")
review.custom_minimum_size = Vector2(210, footer.custom_minimum_size.y)
_set_entry_button_disabled(review, risk_selected_id.is_empty())
review.tooltip_text = _tr("先选择一条风险选项", "Select a risk option first") if risk_selected_id.is_empty() else _tr("查看选项与冻结的四人准备", "Review the option with the frozen four-member preparation")
footer.add_child(review)
var focus := find_child("RiskOption%s" % risk_selected_id, true, false) as Button
if focus != null:
focus.call_deferred("grab_focus")
else:
band.get_child(1).call_deferred("grab_focus")
func select_risk_option(option_id: String) -> void:
risk_selected_id = option_id
show_risk_board(guild_company)
func open_risk_commitment() -> void:
if risk_selected_id.is_empty():
show_risk_board(guild_company)
return
show_risk_commitment(guild_company)
func show_risk_commitment(company := {}) -> void:
formation_risk_service.company_path = initial_company_service.company_path
var loaded: Dictionary = formation_risk_service.load_company()
if not loaded.ok:
show_readiness_review(company)
return
var resolved: Dictionary = loaded.company.duplicate(true)
var formation: Dictionary = resolved.active_contract.get("formation", {})
var option := _risk_option_by_id(formation, risk_selected_id)
if option.is_empty():
party_notice = _tr("该风险选项已失效,请重新检查风险板。", "That risk option is stale; review the Risk Board again.")
show_risk_board(resolved)
return
guild_company = resolved
current_screen = "risk_commitment"
_build_world_base()
var frame_safe := _framed_page_safe("RiskCommitment")
frame_safe.add_child(_entry_chrome("RiskCommitmentFrame", "initial_company_scene_frame"))
var safe := _content_page_safe("RiskCommitment")
var layout := VBoxContainer.new()
layout.name = "RiskCommitmentLayout"
layout.add_theme_constant_override("separation", 5)
safe.add_child(layout)
var heading := VBoxContainer.new()
heading.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 54)
layout.add_child(heading)
heading.add_child(_label(_tr("风险确认", "RISK COMMITMENT"), _scaled(20), Color("#f5ead0")))
heading.add_child(_label(_tr("开始战斗前,确认已知压力与恢复暴露。", "Confirm known pressure and recovery exposure before battle."), _scaled(10), Color("#e8d39d")))
var body := HBoxContainer.new()
body.name = "RiskCommitmentBody"
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_theme_constant_override("separation", 14)
layout.add_child(body)
var scene := VBoxContainer.new()
scene.name = "RiskCommitmentScene"
scene.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scene.size_flags_stretch_ratio = 1.15
scene.add_theme_constant_override("separation", 3)
body.add_child(scene)
scene.add_child(_label(_risk_title(option), _scaled(17), Color("#f0ca75")))
scene.add_child(_label(_risk_threat_copy(option), _scaled(11), Color("#f5ead0")))
scene.add_child(_label(_tr("冻结队列", "FROZEN PARTY"), _scaled(10), Color("#e8d39d")))
var members := HBoxContainer.new()
members.name = "RiskFrozenMembers"
members.size_flags_vertical = Control.SIZE_EXPAND_FILL
members.add_theme_constant_override("separation", 2)
scene.add_child(members)
var party_ids: Array = Array(formation.accepted_party_snapshot.front_to_rear_ids)
for visual_index in range(party_ids.size() - 1, -1, -1):
members.add_child(_readiness_member(visual_index, _recruit_by_id(resolved, String(party_ids[visual_index]))))
var facts := VBoxContainer.new()
facts.name = "RiskCommitmentFacts"
facts.custom_minimum_size = Vector2(minf(335.0, size.x * 0.37), 0)
facts.add_theme_constant_override("separation", 4)
body.add_child(facts)
facts.add_child(_label(_tr("已知后果", "KNOWN CONSEQUENCES"), _scaled(10), Color("#e8d39d")))
var detail := _label(_risk_detail_copy(option), _scaled(11), Color("#f5ead0"))
detail.name = "RiskCommitmentDetail"
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
detail.size_flags_vertical = Control.SIZE_EXPAND_FILL
facts.add_child(detail)
var recovery := _label(_tr("击倒回执:成员只离开当前战斗,下关以同一身份归队。", "Knockout receipt: a member leaves only this battle and returns next stage with the same identity."), _scaled(9), Color("#9ed7aa"))
recovery.name = "RiskRecoveryRule"
recovery.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
facts.add_child(recovery)
var reason := _label(_tr("DEV-5:战斗提交尚未启用", "DEV-5: BATTLE SUBMISSION NOT YET ENABLED"), _scaled(9), Color("#e8d39d"))
reason.name = "RiskBeginBattleReason"
reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
facts.add_child(reason)
var footer := HBoxContainer.new()
footer.name = "RiskCommitmentFooter"
footer.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 52)
footer.add_theme_constant_override("separation", 8)
layout.add_child(footer)
footer.add_child(_company_button("RiskCommitmentCancel", _tr("返回风险板", "Risk Board"), cancel_risk_commitment, "company_utility", "back"))
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
footer.add_child(spacer)
var begin := _company_button("RiskBeginBattleUnavailable", _tr("开始战斗", "Begin Battle"), show_battle_unavailable, "company_primary", "confirm")
begin.custom_minimum_size = Vector2(220, footer.custom_minimum_size.y)
_set_entry_button_disabled(begin, true)
begin.tooltip_text = _tr("DEV-5 才创建战斗回执;当前确认不会消耗金币", "DEV-5 creates the battle receipt; this confirmation does not spend Coin")
footer.add_child(begin)
find_child("RiskCommitmentCancel", true, false).call_deferred("grab_focus")
func back_to_readiness() -> void:
show_readiness_review(guild_company)
func cancel_risk_commitment() -> void:
show_risk_board(guild_company)
func show_battle_unavailable() -> void:
show_risk_commitment(guild_company)
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, 44)
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 _formation_error_text(code: String) -> String:
match code:
"FORMATION_OPENING_HIRES_REQUIRED": return _tr("需要先完成两笔独立招募,并达到远征团 4/4。", "Complete both separate hires and reach Company 4/4 first.")
"FORMATION_CAPACITY_STALE": return _tr("队列容量已改变;请返回市场重新检查。", "Line capacity changed; return to the Market and review it again.")
"FORMATION_COUNT_INVALID": return _tr("当前队列人数与容量不一致。", "The current party count does not match Line capacity.")
"FORMATION_RECRUIT_UNKNOWN", "FORMATION_RECRUIT_DUPLICATE": return _tr("队列成员已改变;请重新进入编队。", "Party membership changed; reopen formation.")
return _tr("无法建立队列;远征团与资源没有改变。", "The line could not be established; Company and resources are unchanged.") + " [%s]" % code
func _party_member_button(position: int, recruit: Dictionary) -> Button:
var recruit_id := String(recruit.get("recruit_id", ""))
var selected := recruit_id == party_selected_recruit_id
var button := Button.new()
button.name = "PartyPosition%d" % position
button.flat = true
button.clip_contents = true
button.custom_minimum_size = Vector2(92, 128 if size.y < 500.0 else 190)
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.set_meta("recruit_id", recruit_id)
button.tooltip_text = _tr("%d 号位置 · %s · 点选后再点目标位置交换", "Position %d · %s · select, then choose a destination to swap") % [position, _recruit_name(recruit)]
for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]:
button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new())
var selection := ColorRect.new()
selection.name = "SelectionGround"
selection.anchor_left = 0.12
selection.anchor_top = 0.82
selection.anchor_right = 0.88
selection.anchor_bottom = 0.89
selection.color = Color("#73b9e7") if selected else Color("#8b7141")
selection.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(selection)
var portrait := TextureRect.new()
portrait.name = "PartyPortrait"
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.08
portrait.anchor_top = 0.0
portrait.anchor_right = 0.92
portrait.anchor_bottom = 0.80
portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(portrait)
var position_label := _button_label("PositionLabel", _tr("%d 号", "POS %d") % position, _scaled(10), Color("#202a31") if selected else Color("#f5ead0"))
position_label.anchor_top = 0.78
position_label.anchor_bottom = 0.90
position_label.offset_left = 6
position_label.offset_right = -6
button.add_child(position_label)
var name := _button_label("NameLabel", _recruit_name(recruit), _scaled(10), Color("#f5ead0"))
name.anchor_top = 0.89
name.offset_left = 3
name.offset_right = -3
button.add_child(name)
button.pressed.connect(select_party_member.bind(recruit_id), CONNECT_DEFERRED)
return button
func _readiness_member(position: int, recruit: Dictionary) -> Control:
var member := Control.new()
member.name = "ReadinessPosition%d" % position
member.custom_minimum_size = Vector2(88, 126 if size.y < 500.0 else 180)
member.size_flags_horizontal = Control.SIZE_EXPAND_FILL
member.clip_contents = true
var ground := ColorRect.new()
ground.anchor_left = 0.14
ground.anchor_top = 0.80
ground.anchor_right = 0.86
ground.anchor_bottom = 0.86
ground.color = Color("#8b7141")
ground.mouse_filter = Control.MOUSE_FILTER_IGNORE
member.add_child(ground)
var portrait := TextureRect.new()
portrait.name = "ReadinessPortrait"
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.08
portrait.anchor_right = 0.92
portrait.anchor_bottom = 0.79
portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
member.add_child(portrait)
var position_label := _label(_tr("%d 号", "POS %d") % position, _scaled(9), Color("#e8d39d"))
position_label.anchor_top = 0.79
position_label.anchor_right = 1.0
position_label.anchor_bottom = 0.90
position_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
position_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
member.add_child(position_label)
var name := _label(_recruit_name(recruit), _scaled(9), Color("#f5ead0"))
name.anchor_top = 0.89
name.anchor_right = 1.0
name.anchor_bottom = 1.0
name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
member.add_child(name)
return member
func _readiness_warning(formation: Dictionary) -> String:
var coverage: Dictionary = formation.coverage
var warnings: Array[String] = []
if int(coverage.protection) == 0:
warnings.append(_tr("没有明确保护贡献者", "No explicit protection contributor"))
if int(coverage.long_reach) == 0:
warnings.append(_tr("没有整线射程", "No full-line reach"))
if int(coverage.sustain) == 0:
warnings.append(_tr("没有续航职责", "No sustain responsibility"))
if warnings.is_empty():
return _tr("允许的警告:无。队列覆盖完整,但结果仍不保证。", "Allowed warnings: none. Coverage is complete, but the outcome is not guaranteed.")
return _tr("允许继续的警告:", "Allowed warnings: ") + " · ".join(warnings)
func _risk_option_button(option: Dictionary) -> Button:
var option_id := String(option.risk_option_id)
var selected := option_id == risk_selected_id
var button := Button.new()
button.name = "RiskOption%s" % option_id
button.flat = true
button.custom_minimum_size = Vector2(0, 54 if size.y < 500.0 else 66)
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.tooltip_text = _risk_title(option)
button.set_meta("selected", selected)
for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]:
button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new())
var rail := ColorRect.new()
rail.name = "RiskRail"
rail.anchor_right = 1.0
rail.offset_left = 4
rail.offset_right = -4
rail.offset_top = 2
rail.offset_bottom = -2
rail.color = Color(0.09, 0.16, 0.21, 0.84 if selected else 0.52)
rail.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(rail)
var accent := ColorRect.new()
accent.anchor_bottom = 1.0
accent.offset_left = 4
accent.offset_right = 10
accent.offset_top = 2
accent.offset_bottom = -2
accent.color = [Color("#75a884"), Color("#d6b76c"), Color("#b7584d")][int(option.risk)]
accent.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(accent)
var mark := _button_label("RiskMark", ["○", "△", "✦"][int(option.risk)], _scaled(18), Color("#f5ead0"))
mark.anchor_right = 0.14
mark.offset_left = 12
button.add_child(mark)
var title := _button_label("RiskTitle", _risk_title(option), _scaled(13), Color("#f5ead0"))
title.anchor_left = 0.14
title.anchor_right = 0.48
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
title.offset_left = 5
button.add_child(title)
var summary := _button_label("RiskSummary", _risk_option_summary(option), _scaled(9), Color("#e8d39d"))
summary.anchor_left = 0.48
summary.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
summary.offset_left = 5
summary.offset_right = -8
summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
button.add_child(summary)
button.pressed.connect(select_risk_option.bind(option_id), CONNECT_DEFERRED)
return button
func _render_risk_context(context: VBoxContainer, formation: Dictionary) -> void:
context.add_child(_label(_tr("选项检查", "OPTION INSPECT"), _scaled(10), Color("#e8d39d")))
if risk_selected_id.is_empty():
var empty := _label(_tr("选择一条风险查看完整已知信息。\n没有选项被自动选中或标记为推荐。", "Select one risk to inspect all known information.\nNo option is auto-selected or marked recommended."), _scaled(12), Color("#f5ead0"))
empty.name = "RiskNoSelection"
empty.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
empty.size_flags_vertical = Control.SIZE_EXPAND_FILL
context.add_child(empty)
return
var option := _risk_option_by_id(formation, risk_selected_id)
if option.is_empty():
context.add_child(_label(_tr("该选项已失效,请返回准备检查。", "That option is stale; return to Readiness."), _scaled(11), Color("#ffd4d4")))
return
context.add_child(_label(_risk_title(option), _scaled(17), Color("#f0ca75")))
var detail := _label(_risk_detail_copy(option), _scaled(10), Color("#f5ead0"))
detail.name = "RiskSelectedDetail"
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
detail.size_flags_vertical = Control.SIZE_EXPAND_FILL
context.add_child(detail)
var coverage: Dictionary = formation.coverage
var coverage_copy := _label(_tr("当前覆盖:保护 %d · 远程 %d · 续航 %d · 控制 %d", "Current coverage: Guard %d · Reach %d · Sustain %d · Control %d") % [int(coverage.protection), int(coverage.long_reach), int(coverage.sustain), int(coverage.control)], _scaled(9), Color("#9ed7aa"))
coverage_copy.name = "RiskCoverage"
coverage_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
context.add_child(coverage_copy)
func _risk_option_by_id(formation: Dictionary, option_id: String) -> Dictionary:
for option in formation.get("risk_options", []):
if String(option.get("risk_option_id", "")) == option_id:
return Dictionary(option)
return {}
func _risk_title(option: Dictionary) -> String:
match String(option.get("tier_id", "steady")):
"pressured": return _tr("受压路线", "PRESSURED ROUTE")
"elite": return _tr("精英路线", "ELITE ROUTE")
return _tr("稳健路线", "STEADY ROUTE")
func _risk_option_summary(option: Dictionary) -> String:
return _tr("%d 金币 · %s · %s", "%d Coin · %s · %s") % [int(option.reward_coin), _uncertainty_name(String(option.uncertainty_id)), _recovery_name(String(option.recovery_exposure_id))]
func _risk_threat_copy(option: Dictionary) -> String:
var threat: Dictionary = option.known_threat
return _tr("已知:%d 名敌人 · %d 波 · 首要目标 0 号", "Known: %d enemies · %d waves · primary target position 0") % [int(threat.enemy_count), int(threat.wave_count)]
func _risk_detail_copy(option: Dictionary) -> String:
return _tr(
"已知威胁:%d 名敌人 · %d 波 · 0 号为首要目标\n未解析信息:%s\n入场成本:%d 金币\n胜利回报:%d 金币 · %s\n恢复暴露:%s",
"Known threat: %d enemies · %d waves · position 0 is primary\nUncertainty: %s\nEntry cost: %d Coin\nVictory reward: %d Coin · %s\nRecovery exposure: %s"
) % [
int(option.known_threat.enemy_count),
int(option.known_threat.wave_count),
_uncertainty_name(String(option.uncertainty_id)),
int(option.entry_cost),
int(option.reward_coin),
_reward_class_name(String(option.reward_class_id)),
_recovery_name(String(option.recovery_exposure_id)),
]
func _uncertainty_name(value: String) -> String:
match value:
"one_intent_unresolved": return _tr("一项敌方意图未解析", "one enemy intent unresolved")
"elite_response_unresolved": return _tr("精英反应时机未解析", "elite response timing unresolved")
return _tr("全部公开", "fully disclosed")
func _recovery_name(value: String) -> String:
match value:
"moderate": return _tr("中", "moderate")
"high": return _tr("高", "high")
return _tr("低", "low")
func _reward_class_name(value: String) -> String:
match value:
"enhanced_choice": return _tr("强化选择", "enhanced choice")
"elite_cache": return _tr("精英宝箱", "elite cache")
return _tr("标准补给", "standard supplies")
func _render_party_context(context: VBoxContainer, company: Dictionary) -> void:
context.add_child(_label(_tr("位置关系", "POSITION RELATIONS"), _scaled(10), Color("#e8d39d")))
var context_id := party_selected_recruit_id if not party_selected_recruit_id.is_empty() else String(party_draft_order[0])
var recruit := _recruit_by_id(company, context_id)
var relation := _party_relation(context_id)
var name := _label(_recruit_name(recruit), _scaled(17), Color("#f5ead0"))
name.name = "PartyContextName"
context.add_child(name)
var position := int(relation.get("position", 0))
var position_copy := _label(
_tr("%d 号 · %s", "POSITION %d · %s") % [position, _exposure_name(String(relation.get("exposure_id", "highest")))],
_scaled(11),
Color("#f0ca75")
)
position_copy.name = "PartyContextPosition"
context.add_child(position_copy)
var protected_names: Array[String] = []
for recruit_id in relation.get("protected_by_recruit_ids", []):
protected_names.append(_recruit_name(_recruit_by_id(company, String(recruit_id))))
var protection_copy := _tr("无前方保护者", "No protector ahead") if protected_names.is_empty() else _tr("受到保护:", "Protected by: ") + " / ".join(protected_names)
var target_copy := _tr("首要威胁目标", "Primary threat target") if bool(relation.get("first_threat_target", false)) else _tr("预期目标顺位 %d", "Expected target rank %d") % int(relation.get("expected_target_rank", position + 1))
var reach_copy := _reach_name(String(relation.get("reach_id", "limited")))
var summary := _label("%s\n%s\n%s" % [target_copy, protection_copy, reach_copy], _scaled(11), Color("#aebbc2"))
summary.name = "PartyRelationSummary"
summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
summary.size_flags_vertical = Control.SIZE_EXPAND_FILL
context.add_child(summary)
var coverage: Dictionary = party_preview.get("coverage", {})
var coverage_copy := _label(
_tr("队列覆盖 保护 %d · 远程 %d · 续航 %d · 控制 %d", "LINE COVERAGE Guard %d · Reach %d · Sustain %d · Control %d") % [
int(coverage.get("protection", 0)), int(coverage.get("long_reach", 0)), int(coverage.get("sustain", 0)), int(coverage.get("control", 0))
],
_scaled(10),
Color("#9ed7aa")
)
coverage_copy.name = "PartyCoverage"
coverage_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
context.add_child(coverage_copy)
var instruction := _label(
party_notice if not party_notice.is_empty() else _tr("先点一名成员,再点目标位置。选择只在当前预览中变化。", "Select a member, then a destination. Changes remain in this preview only."),
_scaled(10),
Color("#73b9e7")
)
instruction.name = "PartyInstruction"
instruction.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
context.add_child(instruction)
var next_boundary := _label(_tr("尚未保存 · 准备检查将在下一步启用", "NOT SAVED · READINESS REVIEW ENABLES NEXT"), _scaled(9), Color("#e8d39d"))
next_boundary.name = "PartyReviewReason"
next_boundary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
context.add_child(next_boundary)
func _party_relation(recruit_id: String) -> Dictionary:
for relation in party_preview.get("relations", []):
if String(relation.get("recruit_id", "")) == recruit_id:
return Dictionary(relation)
return {}
func _party_button_for_recruit(recruit_id: String) -> Button:
if recruit_id.is_empty():
return null
for position in range(party_draft_order.size()):
var button := find_child("PartyPosition%d" % position, true, false) as Button
if button != null and String(button.get_meta("recruit_id", "")) == recruit_id:
return button
return null
func _recruit_by_id(company: Dictionary, recruit_id: String) -> Dictionary:
for recruit in company.get("owned_recruits", []):
if String(recruit.get("recruit_id", "")) == recruit_id:
return Dictionary(recruit)
return {}
func _recruit_name(recruit: Dictionary) -> String:
var localized: Dictionary = recruit.get("rendered_name_snapshot", {})
var locale := String(settings.get("locale", "zh_CN"))
return String(localized.get(locale, localized.get("en", recruit.get("recruit_id", "Unknown"))))
func _exposure_name(exposure_id: String) -> String:
match exposure_id:
"rear": return _tr("受保护后排", "Protected rear")
"guarded": return _tr("受保护中段", "Guarded center")
"high": return _tr("高暴露", "High exposure")
return _tr("最高暴露", "Highest exposure")
func _reach_name(reach_id: String) -> String:
match reach_id:
"full_line": return _tr("射程:覆盖整条队列", "Reach: full line")
"front_three": return _tr("射程:覆盖前 3 位", "Reach: front three")
"front_two": return _tr("射程:覆盖前 2 位", "Reach: front two")
return _tr("射程受限:调整到更前位置", "Limited reach: move farther forward")
func _first_threat_text(threat: Dictionary) -> String:
return _tr("首要威胁 → 0 号前排 · %d 名敌人 · %d 波", "FIRST THREAT -> FRONT 0 · %d ENEMIES · %d WAVES") % [int(threat.get("enemy_count", 0)), int(threat.get("wave_count", 0))]
func _same_string_members(left: Array, right: Array) -> bool:
if left.size() != right.size():
return false
var left_ids: Array[String] = []
var right_ids: Array[String] = []
for value in left:
left_ids.append(String(value))
for value in right:
right_ids.append(String(value))
left_ids.sort()
right_ids.sort()
return left_ids == right_ids
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 == "party_line":
cancel_party_line()
elif current_screen == "readiness_review":
revise_readiness()
elif current_screen == "risk_board":
back_to_readiness()
elif current_screen == "risk_commitment":
cancel_risk_commitment()
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))
func _canonical_value(value):
if value is float and is_equal_approx(value, round(value)):
return int(round(value))
if value is Dictionary:
var result := {}
var keys: Array = value.keys()
keys.sort_custom(func(left, right): return String(left) < String(right))
for key in keys:
result[key] = _canonical_value(value[key])
return result
if value is Array:
var result: Array = []
for item in value:
result.append(_canonical_value(item))
return result
return value