2969 lines
142 KiB
GDScript
2969 lines
142 KiB
GDScript
class_name ABGEntryShell
|
||
extends Control
|
||
|
||
const BootService = preload("res://entry/boot_service.gd")
|
||
const SettingsStore = preload("res://entry/settings_store.gd")
|
||
const EntryChrome = preload("res://entry/entry_chrome.gd")
|
||
const InitialCompanyArt = preload("res://entry/initial_company_art.gd")
|
||
const GuildDestinationSticker = preload("res://entry/guild_destination_sticker.gd")
|
||
const InitialCompanyDraftService = preload("res://entry/initial_company_draft_service.gd")
|
||
const GuildContractService = preload("res://entry/guild_contract_service.gd")
|
||
const 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
|