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

4301 lines
216 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name ABGEntryShell
extends Control
const BootService = preload("res://entry/boot_service.gd")
const SettingsStore = preload("res://entry/settings_store.gd")
const EntryChrome = preload("res://entry/entry_chrome.gd")
const InitialCompanyArt = preload("res://entry/initial_company_art.gd")
const GuildDestinationSticker = preload("res://entry/guild_destination_sticker.gd")
const InitialCompanyDraftService = preload("res://entry/initial_company_draft_service.gd")
const GuildContractService = preload("res://entry/guild_contract_service.gd")
const FormationRiskService = preload("res://entry/formation_risk_service.gd")
const BattleService = preload("res://entry/battle_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 battle_service := BattleService.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 roster_selected_recruit_id := ""
var dismissal_recruit_id := ""
var dismissal_return_screen := "roster"
var dismissal_notice := ""
var contract_return_notice := ""
var risk_selected_id := ""
var battle_operation_id := ""
var battle_paused := false
var battle_speed := 1.0
var battle_notice := ""
var battle_playback_generation := 0
var battle_playback_timer: Timer
var battle_result_diagnosis_open := false
func _ready() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
tree_exiting.connect(_invalidate_battle_playback)
_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("battle_state") and not company.active_contract.battle_state.is_empty():
var battle_status := String(company.active_contract.battle_state.get("status", ""))
if battle_status == "active":
show_battle(company)
return
if battle_status == "result":
show_battle_result(company)
return
if String(company.active_contract.get("status", "")) == "returned":
show_guild_handoff(company)
return
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 contract_status := String(resolved.get("active_contract", {}).get("status", ""))
var active_contract: bool = contract_status in ["market", "battle", "result", "recovery"]
var returned_contract: bool = contract_status == "returned"
copy.add_child(_label(
_tr("首个委托已开始", "FIRST CONTRACT ACTIVE")
if active_contract else (_tr("委托已返回 · 成长保留", "CONTRACT RETURNED · GROWTH RETAINED") if returned_contract else _tr("初始远征团已建立", "INITIAL EXPEDITION COMPANY ESTABLISHED")),
_scaled(11),
Color("#e8d39d")
))
var identity := _label(
_tr("市场准备", "MARKET READY") if active_contract else (_tr("可继续管理", "READY FOR MANAGEMENT") if returned_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("委托已主动返回;金币、成员、等级、装备与历史均保留。", "The Contract was returned voluntarily; Coin, members, levels, equipment and history are retained.") if returned_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 ("GuildContractReturned" if returned_contract else "GuildContractBoard"),
"contract",
_tr("委托已开始", "Contract Active") if active_contract else (_tr("委托已返回", "Contract Returned") if returned_contract else _tr("委托板", "Contract Board")),
func(): _set_guild_notice(_tr("已有进行中的委托。先进入市场。", "A Contract is active. Enter its Market next.")) if active_contract else (_set_guild_notice(_tr("该委托已返回;后续 Contract 创建属于更晚步骤。", "This Contract has returned; creating another Contract belongs to a later step.")) if returned_contract else show_contract_choice())
)
_set_entry_button_disabled(contract, active_contract or returned_contract)
contract.tooltip_text = _tr("已有进行中的委托", "A Contract is already active") if active_contract else (_tr("该委托已返回;下一份委托尚未开放", "This Contract has returned; the next Contract is not open yet") if returned_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.get("difficulty", "standard")))
if active_contract else (_tr("已返回:成长继续保留", "Returned: persistent growth remains available") if returned_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("可用:%d 金币 · 远征团 %d · 队列容量 %d", "Available: %d Coin · Company %d · Line capacity %d") % [int(resolved.active_contract.get("coin", 0)), resolved.owned_recruits.size(), int(resolved.active_contract.get("line_capacity", 4))]
if active_contract else (_tr("不可用:委托已返回;成员与成长仍可查看。", "Unavailable: the Contract has returned; members and growth remain viewable.") if returned_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("GuildRosterButton", _tr("成员", "Roster"), show_roster.bind(resolved), "company_secondary", "inspect"))
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_roster(company := {}) -> void:
var resolved: Dictionary = Dictionary(company).duplicate(true) if company is Dictionary else guild_company.duplicate(true)
if resolved.is_empty():
var loaded := initial_company_service.load_company()
if not loaded.ok:
show_title()
return
resolved = loaded.company.duplicate(true)
guild_company = resolved
current_screen = "roster"
if roster_selected_recruit_id.is_empty() or _recruit_by_id(resolved, roster_selected_recruit_id).is_empty():
roster_selected_recruit_id = String(resolved.owned_recruits[0].recruit_id) if not resolved.owned_recruits.is_empty() else ""
_build_world_base()
var frame_safe := _framed_page_safe("Roster")
frame_safe.add_child(_entry_chrome("RosterFrame", "initial_company_scene_frame"))
var safe := _content_page_safe("Roster")
var layout := VBoxContainer.new()
layout.name = "RosterLayout"
layout.add_theme_constant_override("separation", 5)
safe.add_child(layout)
var header := HBoxContainer.new()
header.custom_minimum_size = Vector2(0, 44 if size.y < 500.0 else 52)
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("远征团成员", "COMPANY ROSTER"), _scaled(21), Color("#f5ead0")))
heading.add_child(_label(_tr("稳定身份 · 个人成长 · 装备与战斗历史", "STABLE IDENTITY · PERSONAL GROWTH · EQUIPMENT & BATTLE HISTORY"), _scaled(10), Color("#e8d39d")))
var count := _label(_tr("成员 %d", "%d MEMBERS") % resolved.owned_recruits.size(), _scaled(12), Color("#73b9e7"))
count.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
header.add_child(count)
var body := HBoxContainer.new()
body.name = "RosterBody"
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_theme_constant_override("separation", 14)
layout.add_child(body)
var list := VBoxContainer.new()
list.name = "RosterList"
list.custom_minimum_size = Vector2(minf(420.0, size.x * 0.54), 0)
list.size_flags_vertical = Control.SIZE_EXPAND_FILL
list.add_theme_constant_override("separation", 5)
body.add_child(list)
for index in range(resolved.owned_recruits.size()):
list.add_child(_roster_member_button(index, resolved.owned_recruits[index]))
var detail := VBoxContainer.new()
detail.name = "RosterDetail"
detail.size_flags_horizontal = Control.SIZE_EXPAND_FILL
detail.add_theme_constant_override("separation", 5)
body.add_child(detail)
_render_roster_detail(detail, resolved, roster_selected_recruit_id)
var footer := HBoxContainer.new()
footer.name = "RosterFooter"
footer.custom_minimum_size = Vector2(0, 44 if size.y < 500.0 else 52)
footer.add_theme_constant_override("separation", 8)
layout.add_child(footer)
footer.add_child(_company_button("RosterBackGuild", _tr("返回公会", "Back to Guild"), back_to_guild, "company_utility", "back"))
var roster_back := footer.get_child(footer.get_child_count() - 1) as Button
roster_back.custom_minimum_size = Vector2(190, footer.custom_minimum_size.y)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
footer.add_child(spacer)
var dismiss_eligible: bool = not roster_selected_recruit_id.is_empty() and resolved.owned_recruits.size() > 1 and String(resolved.active_contract.get("status", "")) == "market"
var dismiss := _company_button("RosterDismiss", _tr("解雇", "Dismiss"), open_dismissal_review.bind(roster_selected_recruit_id), "company_secondary", "utility")
dismiss.custom_minimum_size = Vector2(190, footer.custom_minimum_size.y)
_set_entry_button_disabled(dismiss, not dismiss_eligible)
dismiss.tooltip_text = _tr("审查 %s 的解雇后果", "Review dismissal consequences for %s") % _recruit_name(_recruit_by_id(resolved, roster_selected_recruit_id)) if dismiss_eligible else _tr("最后一名成员不可解雇,或当前阶段不是 Market", "The last member cannot be dismissed, or the current phase is not Market")
footer.add_child(dismiss)
var detail_button := _company_button("RosterOpenDetail", _tr("查看详情", "Open Detail"), show_recruit_detail.bind(roster_selected_recruit_id), "company_primary", "inspect")
detail_button.custom_minimum_size = Vector2(190, footer.custom_minimum_size.y)
footer.add_child(detail_button)
find_child("RosterOpenDetail", true, false).call_deferred("grab_focus")
func show_recruit_detail(recruit_id: String) -> void:
var loaded: Dictionary = initial_company_service.load_company()
if not loaded.ok:
show_title()
return
var recruit := _recruit_by_id(loaded.company, recruit_id)
if recruit.is_empty():
roster_selected_recruit_id = ""
show_roster(loaded.company)
return
guild_company = loaded.company.duplicate(true)
roster_selected_recruit_id = recruit_id
current_screen = "recruit_detail"
_build_world_base()
var frame_safe := _framed_page_safe("RecruitDetail")
frame_safe.add_child(_entry_chrome("RecruitDetailFrame", "initial_company_scene_frame"))
var safe := _content_page_safe("RecruitDetail")
var layout := VBoxContainer.new()
layout.name = "RecruitDetailLayout"
layout.add_theme_constant_override("separation", 5)
safe.add_child(layout)
var header := HBoxContainer.new()
header.custom_minimum_size = Vector2(0, 48 if size.y < 500.0 else 56)
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 DETAIL"), _scaled(21), Color("#f5ead0")))
heading.add_child(_label(_recruit_name(recruit), _scaled(12), Color("#e8d39d")))
var state := _label(_tr("保留中", "RETAINED"), _scaled(11), Color("#9ed7aa"))
state.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
header.add_child(state)
var body := HBoxContainer.new()
body.name = "RecruitDetailBody"
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_theme_constant_override("separation", 16)
layout.add_child(body)
var portrait_stage := Control.new()
portrait_stage.name = "RecruitDetailPortrait"
portrait_stage.size_flags_horizontal = Control.SIZE_EXPAND_FILL
portrait_stage.size_flags_stretch_ratio = 0.8
body.add_child(portrait_stage)
var portrait := TextureRect.new()
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.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
portrait_stage.add_child(portrait)
var details := VBoxContainer.new()
details.name = "RecruitDetailFacts"
details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
details.size_flags_stretch_ratio = 1.2
details.add_theme_constant_override("separation", 5)
body.add_child(details)
_add_review_detail(details, "RecruitDetailProfession", _tr("职业", "PROFESSION"), "%s · %s" % [_recruit_profession_name(recruit), String(recruit.profession_job)])
_add_review_detail(details, "RecruitDetailTrait", _tr("特质", "TRAIT"), _recruit_trait_name(recruit))
_add_review_detail(details, "RecruitDetailGrowth", _tr("个人成长", "PERSONAL GROWTH"), _tr("等级 %d · XP %d", "Level %d · XP %d") % [int(recruit.level), int(recruit.xp)])
_add_review_detail(details, "RecruitDetailEquipment", _tr("当前装备", "EQUIPPED ITEM"), _recruit_item_name(recruit))
var history: Dictionary = recruit.get("battle_history", {})
_add_review_detail(details, "RecruitDetailHistory", _tr("战斗历史", "BATTLE HISTORY"), _tr("参战 %d · 胜利 %d · 击倒 %d", "Entered %d · Victories %d · Knockouts %d") % [int(history.get("battles_entered", 0)), int(history.get("victories", 0)), int(history.get("knockouts", 0))])
var note := _label(_tr("解雇、晋升与更多构筑操作将在后续 DEV-6 步骤启用。", "Dismissal, promotion and further build actions are enabled in later DEV-6 steps."), _scaled(10), Color("#e8d39d"))
note.name = "RecruitDetailFutureReason"
note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
details.add_child(note)
var footer := HBoxContainer.new()
footer.name = "RecruitDetailFooter"
footer.custom_minimum_size = Vector2(0, 44 if size.y < 500.0 else 52)
layout.add_child(footer)
var detail_back := _company_button("RecruitDetailBackRoster", _tr("返回成员", "Back to Roster"), show_roster.bind(guild_company), "company_utility", "back")
detail_back.custom_minimum_size = Vector2(190, footer.custom_minimum_size.y)
footer.add_child(detail_back)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
footer.add_child(spacer)
var dismiss_eligible: bool = guild_company.owned_recruits.size() > 1 and String(guild_company.active_contract.get("status", "")) == "market"
var dismiss := _company_button("RecruitDetailDismiss", _tr("解雇", "Dismiss"), open_dismissal_review.bind(recruit_id), "company_secondary", "utility")
dismiss.custom_minimum_size = Vector2(190, footer.custom_minimum_size.y)
_set_entry_button_disabled(dismiss, not dismiss_eligible)
dismiss.tooltip_text = _tr("审查 %s 的解雇后果", "Review dismissal consequences for %s") % _recruit_name(recruit) if dismiss_eligible else _tr("最后一名成员不可解雇,或当前阶段不是 Market", "The last member cannot be dismissed, or the current phase is not Market")
footer.add_child(dismiss)
find_child("RecruitDetailBackRoster", true, false).call_deferred("grab_focus")
func open_dismissal_review(recruit_id: String) -> void:
var loaded := initial_company_service.load_company()
if not loaded.ok:
show_title()
return
var recruit := _recruit_by_id(loaded.company, recruit_id)
if recruit.is_empty():
roster_selected_recruit_id = ""
show_roster(loaded.company)
return
guild_company = loaded.company.duplicate(true)
dismissal_recruit_id = recruit_id
dismissal_return_screen = current_screen if current_screen in ["roster", "recruit_detail"] else "roster"
current_screen = "dismissal_review"
_build_world_base()
var frame_safe := _framed_page_safe("DismissalReview")
frame_safe.add_child(_entry_chrome("DismissalReviewFrame", "initial_company_scene_frame"))
var safe := _content_page_safe("DismissalReview")
var layout := VBoxContainer.new()
layout.name = "DismissalReviewLayout"
layout.add_theme_constant_override("separation", 6)
safe.add_child(layout)
var header := HBoxContainer.new()
header.custom_minimum_size = Vector2(0, 48 if size.y < 500.0 else 56)
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("解雇审查", "DISMISSAL REVIEW"), _scaled(21), Color("#f5ead0")))
heading.add_child(_label(_recruit_name(recruit), _scaled(13), Color("#e8d39d")))
var eligibility := _label(_tr("可执行 · 当前成员 %d", "ELIGIBLE · %d LIVE MEMBERS") % loaded.company.owned_recruits.size(), _scaled(11), Color("#9ed7aa"))
eligibility.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
header.add_child(eligibility)
var body := HBoxContainer.new()
body.name = "DismissalReviewBody"
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 = "DismissalReviewPortrait"
portrait_stage.size_flags_horizontal = Control.SIZE_EXPAND_FILL
portrait_stage.size_flags_stretch_ratio = 0.72
body.add_child(portrait_stage)
var portrait := TextureRect.new()
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.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
portrait_stage.add_child(portrait)
var facts := VBoxContainer.new()
facts.name = "DismissalReviewFacts"
facts.size_flags_horizontal = Control.SIZE_EXPAND_FILL
facts.add_theme_constant_override("separation", 6)
body.add_child(facts)
facts.add_child(_label(_tr("将离开当前 Company", "LEAVES THE CURRENT COMPANY"), _scaled(11), Color("#e8d39d")))
_add_review_detail(facts, "DismissalReviewProfession", _tr("职业", "PROFESSION"), "%s · %s" % [_recruit_profession_name(recruit), String(recruit.profession_job)])
_add_review_detail(facts, "DismissalReviewTrait", _tr("特质", "TRAIT"), _recruit_trait_name(recruit))
_add_review_detail(facts, "DismissalReviewEquipment", _tr("装备归还", "EQUIPMENT RETURN"), _tr("%s 将回到库存", "%s returns to inventory") % _recruit_item_name(recruit))
_add_review_detail(facts, "DismissalReviewGrowth", _tr("保留成长", "GROWTH RETAINED"), _tr("等级 %d · XP %d · 不扣金币", "Level %d · XP %d · no Coin refund or charge") % [int(recruit.level), int(recruit.xp)])
var consequence := _label(_tr("解雇不可撤销;至少保留一名成员。当前阶段必须仍是 Market,已锁定的队列不可解雇。", "Dismissal is irreversible; at least one member must remain. The current phase must be Market, and a locked Line cannot dismiss members."), _scaled(11), Color("#f0ca75"))
consequence.name = "DismissalReviewConsequence"
consequence.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
consequence.size_flags_vertical = Control.SIZE_EXPAND_FILL
facts.add_child(consequence)
var notice := _label(dismissal_notice, _scaled(10), Color("#ffd4d4"))
notice.name = "DismissalReviewNotice"
notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
facts.add_child(notice)
var footer := HBoxContainer.new()
footer.name = "DismissalReviewFooter"
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)
var cancel := _company_button("DismissalReviewCancel", _tr("取消", "Cancel"), cancel_dismissal_review, "company_utility", "back")
cancel.custom_minimum_size = Vector2(170 if size.y < 500.0 else 190, footer.custom_minimum_size.y)
footer.add_child(cancel)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
footer.add_child(spacer)
var confirm := _company_button("DismissalReviewConfirm", _tr("确认解雇", "Confirm Dismissal"), commit_dismissal, "company_primary", "confirm")
confirm.custom_minimum_size = Vector2(170 if size.y < 500.0 else 190, footer.custom_minimum_size.y)
confirm.tooltip_text = _tr("移除该成员,装备归还库存,金币不变", "Remove this member, return equipment to inventory and leave Coin unchanged")
footer.add_child(confirm)
confirm.call_deferred("grab_focus")
func cancel_dismissal_review() -> void:
dismissal_notice = ""
if dismissal_return_screen == "recruit_detail":
show_recruit_detail(dismissal_recruit_id)
else:
show_roster(guild_company)
func commit_dismissal() -> void:
if dismissal_recruit_id.is_empty():
return
guild_contract_service.company_path = initial_company_service.company_path
var operation_id := ("%s|dismiss|%s" % [String(guild_company.get("company_id", "")), dismissal_recruit_id]).sha256_text().substr(0, 32)
var result := guild_contract_service.dismiss_recruit(operation_id, dismissal_recruit_id)
if not result.ok:
dismissal_notice = _dismissal_error_text(String(result.get("code", "DISMISS_FAILED")))
open_dismissal_review(dismissal_recruit_id)
return
dismissal_notice = ""
guild_company = result.company.duplicate(true)
if roster_selected_recruit_id == dismissal_recruit_id:
roster_selected_recruit_id = String(guild_company.owned_recruits[0].recruit_id) if not guild_company.owned_recruits.is_empty() else ""
dismissal_recruit_id = ""
show_roster(guild_company)
func open_contract_return_review() -> void:
var loaded := initial_company_service.load_company()
if not loaded.ok:
show_title()
return
var contract: Dictionary = loaded.company.get("active_contract", {})
if contract.is_empty() or String(contract.get("status", "")) != "market":
contract_return_notice = _tr("当前委托已不在可返回的 Market 阶段。", "The current Contract is no longer in a returnable Market phase.")
show_market(loaded.company)
return
guild_company = loaded.company.duplicate(true)
current_screen = "contract_return_review"
_build_world_base()
var frame_safe := _framed_page_safe("ContractReturnReview")
frame_safe.add_child(_entry_chrome("ContractReturnReviewFrame", "initial_company_scene_frame"))
var safe := _content_page_safe("ContractReturnReview")
var layout := VBoxContainer.new()
layout.name = "ContractReturnReviewLayout"
layout.add_theme_constant_override("separation", 6)
safe.add_child(layout)
var header := HBoxContainer.new()
header.custom_minimum_size = Vector2(0, 48 if size.y < 500.0 else 56)
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("主动返回委托", "RETURN CONTRACT"), _scaled(21), Color("#f5ead0")))
heading.add_child(_label(_tr("风镜初渡 · 保留 Company 成长", "FIRST WINDGLASS CROSSING · RETAIN COMPANY GROWTH"), _scaled(11), Color("#e8d39d")))
var status := _label(_tr("可确认", "READY TO CONFIRM"), _scaled(11), Color("#9ed7aa"))
status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
header.add_child(status)
var body := HBoxContainer.new()
body.name = "ContractReturnReviewBody"
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 = "ContractReturnCompanyScene"
scene.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scene.size_flags_stretch_ratio = 1.2
scene.add_theme_constant_override("separation", 2)
body.add_child(scene)
scene.add_child(_label(_tr("当前远征团", "CURRENT COMPANY"), _scaled(11), Color("#e8d39d")))
for index in range(guild_company.owned_recruits.size()):
var recruit: Dictionary = guild_company.owned_recruits[index]
var line := HBoxContainer.new()
line.name = "ContractReturnMember%d" % index
line.custom_minimum_size = Vector2(0, 56 if size.y < 500.0 else 68)
var portrait := TextureRect.new()
portrait.texture = InitialCompanyArt.texture_for(recruit)
portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
portrait.custom_minimum_size = Vector2(58, 56 if size.y < 500.0 else 68)
portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
line.add_child(portrait)
line.add_child(_label("%s\n%s · Lv %d · XP %d" % [_recruit_name(recruit), _recruit_profession_name(recruit), int(recruit.level), int(recruit.xp)], _scaled(11), Color("#f5ead0")))
scene.add_child(line)
var facts := VBoxContainer.new()
facts.name = "ContractReturnFacts"
facts.size_flags_horizontal = Control.SIZE_EXPAND_FILL
facts.add_theme_constant_override("separation", 6)
body.add_child(facts)
facts.add_child(_label(_tr("这是自愿退出,不是 Recruit 解雇。", "VOLUNTARY WITHDRAWAL, NOT RECRUIT DISMISSAL"), _scaled(11), Color("#f0ca75")))
facts.add_child(_label(_tr("保留金币:%d", "Coin retained: %d") % int(contract.coin), _scaled(13), Color("#f5ead0")))
facts.add_child(_label(_tr("成员:%d · 等级/XP、职业、特质、装备、历史全部保留。", "Members: %d · levels/XP, Professions, Traits, equipment and history are all retained.") % guild_company.owned_recruits.size(), _scaled(12), Color("#9ed7aa")))
var warning := _label(_tr("返回后当前委托状态变为 returned,不能再次进入这份 Market;下一份 Contract 属于更晚步骤。", "After returning, this Contract becomes returned and cannot re-open its Market; a later Contract belongs to a later step."), _scaled(11), Color("#e8d39d"))
warning.name = "ContractReturnWarning"
warning.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
warning.size_flags_vertical = Control.SIZE_EXPAND_FILL
facts.add_child(warning)
var notice := _label(contract_return_notice, _scaled(10), Color("#ffd4d4"))
notice.name = "ContractReturnNotice"
notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
facts.add_child(notice)
for fact_node in facts.get_children():
if fact_node is Label:
fact_node.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
fact_node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var footer := HBoxContainer.new()
footer.name = "ContractReturnReviewFooter"
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)
var cancel := _company_button("ContractReturnCancel", _tr("取消", "Cancel"), cancel_contract_return, "company_utility", "back")
cancel.custom_minimum_size = Vector2(170 if size.y < 500.0 else 190, footer.custom_minimum_size.y)
footer.add_child(cancel)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
footer.add_child(spacer)
var confirm := _company_button("ContractReturnConfirm", _tr("确认返回公会", "Confirm Return to Guild"), commit_contract_return, "company_primary", "confirm")
confirm.custom_minimum_size = Vector2(170 if size.y < 500.0 else 190, footer.custom_minimum_size.y)
confirm.tooltip_text = _tr("保留全部 Company 成长并返回公会", "Retain all Company growth and return to Guild")
footer.add_child(confirm)
confirm.call_deferred("grab_focus")
func cancel_contract_return() -> void:
contract_return_notice = ""
show_market(guild_company)
func commit_contract_return() -> void:
guild_contract_service.company_path = initial_company_service.company_path
var operation_id := ("%s|return-contract|%s" % [String(guild_company.get("company_id", "")), String(guild_company.get("active_contract", {}).get("run_id", ""))]).sha256_text().substr(0, 32)
var result := guild_contract_service.return_contract(operation_id)
if not result.ok:
contract_return_notice = _contract_return_error_text(String(result.get("code", "CONTRACT_RETURN_FAILED")))
open_contract_return_review()
return
contract_return_notice = ""
guild_company = result.company.duplicate(true)
show_guild_handoff(guild_company)
func _roster_member_button(index: int, recruit: Dictionary) -> Button:
var button := _entry_button_base("RosterMember%d" % index, select_roster_recruit.bind(String(recruit.recruit_id)), "company_primary" if String(recruit.recruit_id) == roster_selected_recruit_id else "company_secondary")
button.set_meta("recruit_id", String(recruit.recruit_id))
button.custom_minimum_size = Vector2(0, 62 if size.y < 500.0 else 72)
var portrait := TextureRect.new()
portrait.texture = InitialCompanyArt.texture_for(recruit)
portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
portrait.position = Vector2(8, 4)
portrait.size = Vector2(52, 54)
portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(portrait)
var name := _label(_recruit_name(recruit), _scaled(14), Color("#f5ead0"))
name.position = Vector2(70, 6)
name.size = Vector2(230, 24)
name.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(name)
var facts := _label(_tr("%s · 等级 %d · XP %d", "%s · Level %d · XP %d") % [_recruit_profession_name(recruit), int(recruit.level), int(recruit.xp)], _scaled(10), Color("#aebbc2"))
facts.position = Vector2(70, 31)
facts.size = Vector2(290, 24)
facts.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
facts.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(facts)
return button
func select_roster_recruit(recruit_id: String) -> void:
roster_selected_recruit_id = recruit_id
show_roster(guild_company)
func _render_roster_detail(parent: VBoxContainer, company: Dictionary, recruit_id: String) -> void:
var recruit := _recruit_by_id(company, recruit_id)
if recruit.is_empty():
parent.add_child(_label(_tr("选择一名成员查看详情。", "Select a Recruit to inspect detail."), _scaled(13), Color("#f5ead0")))
return
parent.add_child(_label(_tr("已选成员", "SELECTED RECRUIT"), _scaled(10), Color("#e8d39d")))
parent.add_child(_label(_recruit_name(recruit), _scaled(18), Color("#f0ca75")))
parent.add_child(_label(_tr("%s · 等级 %d · XP %d", "%s · Level %d · XP %d") % [_recruit_profession_name(recruit), int(recruit.level), int(recruit.xp)], _scaled(11), Color("#f5ead0")))
parent.add_child(_label(_tr("当前装备:%s", "Equipped: %s") % _recruit_item_name(recruit), _scaled(11), Color("#9ed7aa")))
var history: Dictionary = recruit.get("battle_history", {})
var history_copy := _label(_tr("参战 %d · 胜利 %d · 击倒 %d", "Entered %d · Victories %d · Knockouts %d") % [int(history.get("battles_entered", 0)), int(history.get("victories", 0)), int(history.get("knockouts", 0))], _scaled(10), Color("#aebbc2"))
history_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
history_copy.size_flags_vertical = Control.SIZE_EXPAND_FILL
parent.add_child(history_copy)
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 next_stage_market: bool = not contract.get("recovery_receipts", []).is_empty()
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("下一阶段市场", "NEXT-STAGE MARKET") if next_stage_market else _tr("首次市场", "FIRST MARKET"), _scaled(21), Color("#f5ead0")))
heading.add_child(_label(_tr("战后归队 · 稳定货单", "POST-BATTLE RETURN · STABLE LEDGER") if next_stage_market else _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(150 if short_market else 180, 44 if short_market else 48)
footer.add_child(title)
var return_contract := _company_button("MarketReturnContract", _tr("返回委托", "Return Contract"), open_contract_return_review, "company_secondary", "back")
return_contract.custom_minimum_size = Vector2(170 if short_market else 190, 44 if short_market else 48)
return_contract.tooltip_text = _tr("主动结束当前委托;金币、成员、等级、装备与历史均保留", "Voluntarily leave this Contract; Coin, members, levels, equipment and history are retained")
footer.add_child(return_contract)
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(160 if short_market else 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("自动战斗:可暂停与切换速度;没有战斗指令。", "Automatic battle: pause or change speed; no combat commands."), _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("RiskBeginBattle", _tr("开始战斗", "Begin Battle"), begin_battle, "company_primary", "confirm")
begin.custom_minimum_size = Vector2(220, footer.custom_minimum_size.y)
begin.tooltip_text = _tr("创建一次战斗回执并开始自动观察", "Create one battle receipt and begin automatic observation")
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 begin_battle() -> void:
if risk_selected_id.is_empty():
show_risk_board(guild_company)
return
battle_service.company_path = initial_company_service.company_path
var loaded: Dictionary = battle_service.load_company()
if not loaded.ok:
party_notice = _battle_error_text(String(loaded.get("code", "BATTLE_LOAD_FAILED")))
show_readiness_review(guild_company)
return
var operation_id := ("%s|battle|%s" % [String(loaded.company.company_id), risk_selected_id]).sha256_text().substr(0, 32)
var started: Dictionary = battle_service.begin_battle(operation_id, risk_selected_id)
if not started.ok:
party_notice = _battle_error_text(String(started.get("code", "BATTLE_START_FAILED")))
show_risk_commitment(loaded.company)
return
battle_operation_id = operation_id
battle_paused = false
battle_speed = 1.0
battle_notice = ""
guild_company = started.company.duplicate(true)
show_battle(guild_company)
func show_battle(company := {}, auto_play := true) -> void:
_clear_battle_playback_timer()
battle_playback_generation += 1
var playback_generation := battle_playback_generation
battle_service.company_path = initial_company_service.company_path
var resolved: Dictionary = Dictionary(company).duplicate(true) if company is Dictionary else {}
if resolved.is_empty():
var loaded: Dictionary = battle_service.load_company()
if not loaded.ok:
party_notice = _battle_error_text(String(loaded.get("code", "BATTLE_LOAD_FAILED")))
show_readiness_review(guild_company)
return
resolved = loaded.company.duplicate(true)
var battle: Dictionary = resolved.active_contract.get("battle_state", {})
if battle.is_empty():
party_notice = _tr("没有可恢复的战斗回执。", "No resumable battle receipt exists.")
show_readiness_review(resolved)
return
if String(battle.status) == "result":
show_battle_result(resolved)
return
battle_operation_id = String(battle.operation_id)
guild_company = resolved
current_screen = "battle"
_build_world_base()
var frame_safe := _framed_page_safe("Battle")
frame_safe.add_child(_entry_chrome("BattleFrame", "initial_company_scene_frame"))
var safe := _content_page_safe("Battle")
var layout := VBoxContainer.new()
layout.name = "BattleLayout"
layout.add_theme_constant_override("separation", 4)
safe.add_child(layout)
var header := HBoxContainer.new()
header.name = "BattleHeader"
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("风镜初渡 · 自动战斗", "FIRST WINDGLASS CROSSING · AUTO BATTLE"), _scaled(19), Color("#f5ead0")))
heading.add_child(_label(_tr("目标:击破敌方队列 · 首要威胁锁定 0 号前排", "OBJECTIVE: BREAK THE ENEMY LINE · FIRST THREAT TARGETS FRONT 0"), _scaled(9), Color("#e8d39d")))
var phase_status := _label(_tr("阶段 %d/%d", "PHASE %d/%d") % [int(battle.phase_index), int(battle.total_phases)], _scaled(11), Color("#73b9e7"))
phase_status.name = "BattlePhaseStatus"
phase_status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
header.add_child(phase_status)
var body := HBoxContainer.new()
body.name = "BattleBody"
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_theme_constant_override("separation", 12)
layout.add_child(body)
var field := VBoxContainer.new()
field.name = "Battlefield"
field.size_flags_horizontal = Control.SIZE_EXPAND_FILL
field.size_flags_stretch_ratio = 1.7
field.add_theme_constant_override("separation", 2)
body.add_child(field)
var enemy_heading := _label(_tr("敌方队列 ◁ 接触走廊", "ENEMY LINE < CONTACT CORRIDOR"), _scaled(9), Color("#f0ca75"))
enemy_heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
field.add_child(enemy_heading)
var enemies := HBoxContainer.new()
enemies.name = "BattleEnemies"
enemies.custom_minimum_size = Vector2(0, 58 if size.y < 500.0 else 100)
enemies.alignment = BoxContainer.ALIGNMENT_END
enemies.add_theme_constant_override("separation", 7)
field.add_child(enemies)
for enemy_index in range(battle.enemy_ids.size()):
enemies.add_child(_battle_enemy_marker(enemy_index, String(battle.enemy_ids[enemy_index]), battle))
var corridor := ColorRect.new()
corridor.name = "BattleContactCorridor"
corridor.custom_minimum_size = Vector2(0, 3)
corridor.color = Color("#d6b76c")
corridor.mouse_filter = Control.MOUSE_FILTER_IGNORE
field.add_child(corridor)
var actors := HBoxContainer.new()
actors.name = "BattleActors"
actors.size_flags_vertical = Control.SIZE_EXPAND_FILL
actors.add_theme_constant_override("separation", 4)
field.add_child(actors)
for visual_index in range(battle.front_to_rear_ids.size() - 1, -1, -1):
var recruit_id := String(battle.front_to_rear_ids[visual_index])
actors.add_child(_battle_actor(visual_index, _recruit_by_id(resolved, recruit_id), battle))
var orientation := _label(_tr("受保护后排 ◁────────▷ 暴露前排 · 0 号", "PROTECTED REAR <--------> EXPOSED FRONT · POSITION 0"), _scaled(9), Color("#9ed7aa"))
orientation.name = "BattleOrientation"
field.add_child(orientation)
var context := VBoxContainer.new()
context.name = "BattleEventContext"
context.custom_minimum_size = Vector2(minf(310.0, size.x * 0.35), 0)
context.add_theme_constant_override("separation", 4)
body.add_child(context)
_render_battle_event_context(context, resolved, battle)
var footer := HBoxContainer.new()
footer.name = "BattleFooter"
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)
var pause := _company_button("BattlePause", _tr("继续观察", "Resume") if battle_paused else _tr("暂停", "Pause"), toggle_battle_pause, "company_utility", "pause")
pause.custom_minimum_size = Vector2(170, footer.custom_minimum_size.y)
pause.tooltip_text = _tr("继续自动事件时间线", "Resume the automatic event timeline") if battle_paused else _tr("暂停自动事件时间线", "Pause the automatic event timeline")
footer.add_child(pause)
var speed := _company_button("BattleSpeed", _tr("速度 %.0fx", "Speed %.0fx") % battle_speed, cycle_battle_speed, "company_secondary", "speed")
speed.custom_minimum_size = Vector2(150, footer.custom_minimum_size.y)
speed.tooltip_text = _tr("切换 1x / 2x 自动观察速度", "Cycle automatic observation speed between 1x and 2x")
footer.add_child(speed)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
footer.add_child(spacer)
var lock := _label(_tr("风险已提交 · 战斗中不能返回 · 没有战斗指令", "RISK COMMITTED · NO BACK · NO COMBAT COMMANDS"), _scaled(9), Color("#e8d39d"))
lock.name = "BattleCommittedRule"
lock.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
footer.add_child(lock)
pause.call_deferred("grab_focus")
if auto_play and not battle_paused:
_schedule_battle_playback(playback_generation)
func _invalidate_battle_playback() -> void:
battle_playback_generation += 1
battle_paused = true
_clear_battle_playback_timer()
func _clear_battle_playback_timer() -> void:
if is_instance_valid(battle_playback_timer):
battle_playback_timer.stop()
battle_playback_timer.queue_free()
battle_playback_timer = null
func _schedule_battle_playback(playback_generation: int) -> void:
_clear_battle_playback_timer()
battle_playback_timer = Timer.new()
battle_playback_timer.name = "BattlePlaybackTimer"
battle_playback_timer.one_shot = true
battle_playback_timer.wait_time = 0.42 / maxf(1.0, battle_speed)
battle_playback_timer.timeout.connect(_on_battle_playback_timeout.bind(playback_generation))
add_child(battle_playback_timer)
battle_playback_timer.start()
func _on_battle_playback_timeout(playback_generation: int) -> void:
if playback_generation != battle_playback_generation or current_screen != "battle" or battle_paused:
return
battle_playback_timer = null
advance_battle_phase()
func advance_battle_phase() -> void:
battle_service.company_path = initial_company_service.company_path
var advanced: Dictionary = battle_service.advance_battle(battle_operation_id)
if not advanced.ok:
battle_notice = _battle_error_text(String(advanced.get("code", "BATTLE_ADVANCE_FAILED")))
battle_paused = true
show_battle(guild_company, false)
return
guild_company = advanced.company.duplicate(true)
if String(advanced.battle.status) == "result":
show_battle_result(guild_company)
else:
show_battle(guild_company, true)
func toggle_battle_pause() -> void:
battle_paused = not battle_paused
show_battle(guild_company, not battle_paused)
func cycle_battle_speed() -> void:
battle_speed = 2.0 if is_equal_approx(battle_speed, 1.0) else 1.0
show_battle(guild_company, not battle_paused)
func show_battle_result(company := {}) -> void:
var resolved: Dictionary = Dictionary(company).duplicate(true) if company is Dictionary else guild_company.duplicate(true)
guild_company = resolved
current_screen = "battle_result"
_build_world_base()
var frame_safe := _framed_page_safe("BattleResult")
frame_safe.add_child(_entry_chrome("BattleResultFrame", "initial_company_scene_frame"))
var safe := _content_page_safe("BattleResult")
var battle: Dictionary = resolved.active_contract.get("battle_state", {})
var result: Dictionary = battle.get("result", {})
var short_result := size.y < 500.0
var layout := VBoxContainer.new()
layout.name = "BattleResultLayout"
layout.add_theme_constant_override("separation", 5)
safe.add_child(layout)
var header := HBoxContainer.new()
header.name = "BattleResultHeader"
header.custom_minimum_size = Vector2(0, 44 if short_result else 54)
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("战斗结果", "BATTLE RESULT"), _scaled(21), Color("#f5ead0")))
heading.add_child(_label(_tr("风镜初渡 · 可审计结算", "FIRST WINDGLASS CROSSING · REVIEWABLE SETTLEMENT"), _scaled(10), Color("#e8d39d")))
var outcome := _label(_battle_outcome_text(String(result.get("outcome", "unknown"))), _scaled(15), Color("#9ed7aa") if String(result.get("outcome", "")) == "victory" else Color("#ffd4d4"))
outcome.name = "BattleResultSummary"
outcome.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
header.add_child(outcome)
var body := HBoxContainer.new()
body.name = "BattleResultBody"
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_theme_constant_override("separation", 14)
layout.add_child(body)
var report := VBoxContainer.new()
report.name = "BattleResultReport"
report.size_flags_horizontal = Control.SIZE_EXPAND_FILL
report.size_flags_stretch_ratio = 1.35
report.add_theme_constant_override("separation", 5)
body.add_child(report)
var objective := _label(_tr("目标:击破敌方队列", "OBJECTIVE: BREAK THE ENEMY LINE"), _scaled(13), Color("#f5ead0"))
objective.name = "BattleResultObjective"
report.add_child(objective)
var decisive := _battle_result_decisive_event(battle)
var decisive_copy := _label(
_tr("第一决定性事件:%s", "FIRST DECISIVE EVENT: %s") % _battle_event_text(String(decisive.get("caption_id", ""))),
_scaled(13), Color("#f0ca75")
)
decisive_copy.name = "BattleResultDecisive"
decisive_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
report.add_child(decisive_copy)
var totals := HBoxContainer.new()
totals.name = "BattleResultTotals"
totals.custom_minimum_size = Vector2(0, 42 if short_result else 48)
totals.add_theme_constant_override("separation", 8)
report.add_child(totals)
var coin := _label(_tr("金币 %d → %d\n+%d", "COIN %d -> %d\n+%d") % [int(result.get("coin_before", 0)), int(result.get("coin_after", 0)), int(result.get("reward_coin", 0))], _scaled(12), Color("#f0ca75"))
coin.name = "BattleResultReward"
coin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
coin.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
totals.add_child(coin)
var xp := _label(_tr("经验 +%d\n存活成员各 +%d", "XP +%d\n%d per survivor") % [int(result.get("xp_award", 0)), int(result.get("xp_per_recruit", 0))], _scaled(12), Color("#9ed7aa"))
xp.name = "BattleResultXP"
xp.size_flags_horizontal = Control.SIZE_EXPAND_FILL
xp.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
totals.add_child(xp)
var knockouts: Array = result.get("knocked_out_ids", [])
var knockout_copy := _label(_battle_result_knockout_text(resolved, knockouts), _scaled(11), Color("#ffd4d4") if not knockouts.is_empty() else Color("#9ed7aa"))
knockout_copy.name = "BattleResultKnockouts"
knockout_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
knockout_copy.size_flags_vertical = Control.SIZE_EXPAND_FILL
report.add_child(knockout_copy)
var context := VBoxContainer.new()
context.name = "BattleResultDiagnosisContext"
context.custom_minimum_size = Vector2(minf(350.0, size.x * 0.38), 0)
context.add_theme_constant_override("separation", 4)
body.add_child(context)
context.add_child(_label(_tr("因果链", "CAUSAL CHAIN"), _scaled(10), Color("#e8d39d")))
var chain := _label(_battle_result_chain_summary(battle), _scaled(11), Color("#f5ead0"))
chain.name = "BattleResultChain"
chain.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
chain.size_flags_vertical = Control.SIZE_EXPAND_FILL
context.add_child(chain)
if battle_result_diagnosis_open:
var detail := _label(_battle_result_diagnosis_text(resolved, battle), _scaled(10), Color("#aebbc2"))
detail.name = "BattleResultDiagnosisDetail"
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
detail.size_flags_vertical = Control.SIZE_EXPAND_FILL
context.add_child(detail)
var footer := HBoxContainer.new()
footer.name = "BattleResultFooter"
footer.custom_minimum_size = Vector2(0, 46 if short_result else 52)
footer.add_theme_constant_override("separation", 8)
layout.add_child(footer)
var inspect := _company_button("BattleResultDiagnosis", _tr("因果详情", "Cause Details"), toggle_battle_result_diagnosis, "company_secondary", "inspect")
inspect.custom_minimum_size = Vector2(230, footer.custom_minimum_size.y)
footer.add_child(inspect)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
footer.add_child(spacer)
var continue_button := _company_button("BattleResultContinue", _tr("继续", "Continue"), continue_from_battle_result, "company_primary", "confirm")
continue_button.custom_minimum_size = Vector2(220, footer.custom_minimum_size.y)
continue_button.tooltip_text = _tr("先查看因果链,再进入下一阶段恢复", "Review the causal chain before next-stage recovery")
footer.add_child(continue_button)
inspect.call_deferred("grab_focus")
func toggle_battle_result_diagnosis() -> void:
battle_result_diagnosis_open = not battle_result_diagnosis_open
show_battle_result(guild_company)
func continue_from_battle_result() -> void:
show_battle_knockout_review(guild_company)
func show_battle_knockout_review(company := {}) -> void:
var resolved: Dictionary = Dictionary(company).duplicate(true) if company is Dictionary else guild_company.duplicate(true)
guild_company = resolved
current_screen = "battle_knockout_review"
_build_world_base()
var frame_safe := _framed_page_safe("BattleKnockout")
frame_safe.add_child(_entry_chrome("BattleKnockoutFrame", "initial_company_scene_frame"))
var safe := _content_page_safe("BattleKnockout")
var layout := VBoxContainer.new()
layout.name = "BattleKnockoutLayout"
layout.add_theme_constant_override("separation", 5)
safe.add_child(layout)
var header := HBoxContainer.new()
header.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 54)
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("击倒回顾", "KNOCKOUT REVIEW"), _scaled(21), Color("#f5ead0")))
heading.add_child(_label(_tr("确认战斗内后果;没有成员被删除。", "Confirm the battle-local consequence; no Recruit was deleted."), _scaled(10), Color("#e8d39d")))
var status := _label(_tr("战斗回执已提交", "BATTLE RECEIPT COMMITTED"), _scaled(11), Color("#9ed7aa"))
status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
header.add_child(status)
var body := HBoxContainer.new()
body.name = "BattleKnockoutBody"
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 = "BattleKnockoutScene"
scene.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scene.size_flags_stretch_ratio = 1.2
scene.add_theme_constant_override("separation", 3)
body.add_child(scene)
var battle: Dictionary = resolved.active_contract.get("battle_state", {})
var knocked: Array = battle.get("result", {}).get("knocked_out_ids", [])
if knocked.is_empty():
scene.add_child(_label(_tr("本场没有击倒成员。", "No Recruit was knocked out in this battle."), _scaled(15), Color("#9ed7aa")))
else:
for recruit_id in knocked:
var recruit := _recruit_by_id(resolved, String(recruit_id))
if recruit.is_empty():
continue
scene.add_child(_readiness_member(0, recruit))
scene.add_child(_label(_tr("%s · %s", "%s · %s") % [_recruit_name(recruit), _recruit_profession_name(recruit)], _scaled(16), Color("#f0ca75")))
scene.add_child(_label(_tr("特质:%s\n等级 %d · XP %d\n装备:%s", "Trait: %s\nLevel %d · XP %d\nEquipment: %s") % [_recruit_trait_name(recruit), int(recruit.level), int(recruit.xp), _recruit_item_name(recruit)], _scaled(11), Color("#f5ead0")))
var context := VBoxContainer.new()
context.name = "BattleKnockoutContext"
context.custom_minimum_size = Vector2(minf(350.0, size.x * 0.38), 0)
context.add_theme_constant_override("separation", 5)
body.add_child(context)
context.add_child(_label(_tr("当前战斗空位", "CURRENT BATTLE GAP"), _scaled(10), Color("#e8d39d")))
var gap := _label(_tr("被击倒成员只离开本场战斗。其稳定 Recruit ID、Profession、Trait、等级、XP 和装备所有权仍在 Company 中。", "The knocked-out member leaves this battle only. The stable Recruit ID, Profession, Trait, level, XP and equipment ownership remain in the Company."), _scaled(12), Color("#ffd4d4"))
gap.name = "BattleKnockoutCurrentGap"
gap.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
gap.size_flags_vertical = Control.SIZE_EXPAND_FILL
context.add_child(gap)
var return_rule := _label(_tr("下关归队:恢复后进入下一阶段 Market,不会永久死亡。", "Next-stage return: recovery opens the next Market; there is no permanent death."), _scaled(11), Color("#9ed7aa"))
return_rule.name = "BattleKnockoutReturnRule"
return_rule.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
context.add_child(return_rule)
var footer := HBoxContainer.new()
footer.name = "BattleKnockoutFooter"
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)
var back := _company_button("BattleKnockoutBack", _tr("返回结果", "Back to Result"), show_battle_result.bind(resolved), "company_utility", "back")
footer.add_child(back)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
footer.add_child(spacer)
var acknowledge := _company_button("BattleKnockoutAcknowledge", _tr("确认击倒", "Acknowledge"), acknowledge_battle_knockout.bind(resolved), "company_primary", "confirm")
acknowledge.custom_minimum_size = Vector2(230, footer.custom_minimum_size.y)
acknowledge.tooltip_text = _tr("确认已提交的战斗内后果;不会删除成员", "Acknowledge the committed battle-local consequence; the member is not deleted")
footer.add_child(acknowledge)
back.call_deferred("grab_focus")
func acknowledge_battle_knockout(company := {}) -> void:
show_battle_recovery(company)
func show_battle_recovery(company := {}) -> void:
var resolved: Dictionary = Dictionary(company).duplicate(true) if company is Dictionary else guild_company.duplicate(true)
guild_company = resolved
current_screen = "battle_recovery"
_build_world_base()
var frame_safe := _framed_page_safe("BattleRecovery")
frame_safe.add_child(_entry_chrome("BattleRecoveryFrame", "initial_company_scene_frame"))
var safe := _content_page_safe("BattleRecovery")
var layout := VBoxContainer.new()
layout.name = "BattleRecoveryLayout"
layout.add_theme_constant_override("separation", 6)
safe.add_child(layout)
var heading := VBoxContainer.new()
heading.custom_minimum_size = Vector2(0, 48 if size.y < 500.0 else 58)
layout.add_child(heading)
heading.add_child(_label(_tr("下一阶段恢复", "NEXT-STAGE RECOVERY"), _scaled(21), Color("#f5ead0")))
heading.add_child(_label(_tr("一次原子回执将恢复同一 Company 的下一阶段准备入口。", "One atomic receipt restores the next-stage preparation entry for the same Company."), _scaled(10), Color("#e8d39d")))
var body := HBoxContainer.new()
body.name = "BattleRecoveryBody"
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_theme_constant_override("separation", 14)
layout.add_child(body)
var summary := VBoxContainer.new()
summary.name = "BattleRecoverySummary"
summary.size_flags_horizontal = Control.SIZE_EXPAND_FILL
summary.add_theme_constant_override("separation", 5)
body.add_child(summary)
var battle: Dictionary = resolved.active_contract.get("battle_state", {})
var knocked: Array = battle.get("result", {}).get("knocked_out_ids", [])
summary.add_child(_label(_tr("恢复成员 %d 名", "%d Recruit(s) to return") % knocked.size(), _scaled(16), Color("#f0ca75")))
var ids := _label("\n".join(_recovery_identity_lines(resolved, knocked)), _scaled(11), Color("#f5ead0"))
ids.name = "BattleRecoveryIdentities"
ids.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
ids.size_flags_vertical = Control.SIZE_EXPAND_FILL
summary.add_child(ids)
var context := VBoxContainer.new()
context.name = "BattleRecoveryContext"
context.custom_minimum_size = Vector2(minf(350.0, size.x * 0.38), 0)
context.add_theme_constant_override("separation", 5)
body.add_child(context)
context.add_child(_label(_tr("恢复承诺", "RECOVERY COMMITMENT"), _scaled(10), Color("#e8d39d")))
var rule := _label(_tr("保留稳定 ID、Profession、Trait、等级、XP、历史和装备。战斗状态回到 Market;下一阶段可以重新建立 Party。", "Stable ID, Profession, Trait, level, XP, history and equipment are retained. Battle state returns to Market; the next stage can establish a new Party."), _scaled(12), Color("#9ed7aa"))
rule.name = "BattleRecoveryRule"
rule.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
rule.size_flags_vertical = Control.SIZE_EXPAND_FILL
context.add_child(rule)
var receipt := _label(_tr("恢复回执只提交一次;重试会返回原回执。", "The recovery receipt submits once; retries return the original receipt."), _scaled(10), Color("#e8d39d"))
receipt.name = "BattleRecoveryReceiptRule"
receipt.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
context.add_child(receipt)
var notice := _label(battle_notice, _scaled(10), Color("#ffd4d4"))
notice.name = "BattleRecoveryNotice"
notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
context.add_child(notice)
var footer := HBoxContainer.new()
footer.name = "BattleRecoveryFooter"
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)
var back := _company_button("BattleRecoveryBack", _tr("返回", "Back"), show_battle_knockout_review.bind(resolved), "company_utility", "back")
footer.add_child(back)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
footer.add_child(spacer)
var commit := _company_button("BattleRecoveryCommit", _tr("恢复并进入市场", "Return to Market"), commit_battle_recovery, "company_primary", "confirm")
commit.custom_minimum_size = Vector2(260, footer.custom_minimum_size.y)
commit.tooltip_text = _tr("恢复相同 Recruit 身份并打开下一阶段 Market", "Return the same Recruit identities and open the next-stage Market")
footer.add_child(commit)
back.call_deferred("grab_focus")
func commit_battle_recovery() -> void:
battle_service.company_path = initial_company_service.company_path
var loaded: Dictionary = battle_service.load_company()
if not loaded.ok:
battle_notice = _battle_error_text(String(loaded.get("code", "RECOVERY_LOAD_FAILED")))
show_battle_recovery(guild_company)
return
var battle: Dictionary = loaded.company.active_contract.get("battle_state", {})
var operation_id := ("%s|recovery|%s" % [String(battle.get("operation_id", "")), String(loaded.company.company_id)]).sha256_text().substr(0, 32)
var recovered: Dictionary = battle_service.recover_next_stage(operation_id)
if not recovered.ok:
battle_notice = _battle_error_text(String(recovered.get("code", "RECOVERY_FAILED")))
show_battle_recovery(loaded.company)
return
battle_notice = ""
guild_company = recovered.company.duplicate(true)
show_market(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 open_equipment_review() -> void:
if market_selected_kind != "equipment" or market_selected_offer_id.is_empty():
market_notice = _tr("请先选择一件装备。", "Select an equipment offer first.")
show_market(guild_company)
return
show_equipment_review(market_selected_offer_id)
func show_equipment_review(offer_id: String) -> void:
var loaded: Dictionary = initial_company_service.load_company()
if not loaded.ok or loaded.company.active_contract.is_empty():
market_notice = _tr("无法读取稳定装备货单。", "The stable Equipment ledger could not be loaded.")
show_market()
return
var offer := _market_offer_by_id(loaded.company.active_contract, "equipment", offer_id)
if offer.is_empty():
market_notice = _tr("该装备候选已不可用。", "That equipment offer is no longer available.")
show_market(loaded.company)
return
guild_company = loaded.company.duplicate(true)
market_category = "equipment"
market_selected_kind = "equipment"
market_selected_offer_id = offer_id
current_screen = "equipment_review"
_build_world_base()
var frame_safe := _framed_page_safe("EquipmentReview")
frame_safe.add_child(_entry_chrome("EquipmentReviewFrame", "initial_company_scene_frame"))
var safe := _content_page_safe("EquipmentReview")
var layout := VBoxContainer.new()
layout.name = "EquipmentReviewLayout"
layout.add_theme_constant_override("separation", 5)
safe.add_child(layout)
var header := HBoxContainer.new()
header.custom_minimum_size = Vector2(0, 44 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("装备详情", "EQUIPMENT REVIEW"), _scaled(20), Color("#f5ead0")))
heading.add_child(_label(_market_offer_title("equipment", offer), _scaled(11), Color("#e8d39d")))
var price := _label(_market_offer_price("equipment", offer), _scaled(15), Color("#f0ca75"))
price.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
header.add_child(price)
var body := HBoxContainer.new()
body.name = "EquipmentReviewBody"
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_theme_constant_override("separation", 16)
layout.add_child(body)
var detail := VBoxContainer.new()
detail.name = "EquipmentReviewDetail"
detail.size_flags_horizontal = Control.SIZE_EXPAND_FILL
detail.size_flags_stretch_ratio = 1.05
detail.add_theme_constant_override("separation", 5)
body.add_child(detail)
detail.add_child(_label(_market_offer_title("equipment", offer), _scaled(17), Color("#f0ca75")))
var effect := _label(_market_context_detail("equipment", offer), _scaled(11), Color("#f5ead0"))
effect.name = "EquipmentReviewEffect"
effect.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
effect.size_flags_vertical = Control.SIZE_EXPAND_FILL
detail.add_child(effect)
var inventory_note := _label(_tr("购买后装备到一名 Recruit;原装备返回 Company inventory。", "Purchase equips one Recruit; the previous item returns to Company inventory."), _scaled(10), Color("#9ed7aa"))
inventory_note.name = "EquipmentReviewInventoryRule"
inventory_note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
detail.add_child(inventory_note)
var targets := VBoxContainer.new()
targets.name = "EquipmentReviewTargets"
targets.size_flags_horizontal = Control.SIZE_EXPAND_FILL
targets.size_flags_stretch_ratio = 1.0
targets.add_theme_constant_override("separation", 4)
body.add_child(targets)
targets.add_child(_label(_tr("选择装备成员", "CHOOSE WEARER"), _scaled(10), Color("#e8d39d")))
var target_grid := GridContainer.new()
target_grid.name = "EquipmentReviewTargetGrid"
target_grid.columns = 2
target_grid.size_flags_vertical = Control.SIZE_EXPAND_FILL
target_grid.add_theme_constant_override("h_separation", 5)
target_grid.add_theme_constant_override("v_separation", 5)
targets.add_child(target_grid)
for recruit in guild_company.owned_recruits:
var recruit_id := String(recruit.recruit_id)
var equip := _company_button("EquipmentEquipRecruit%s" % recruit_id, _tr("装备 %s", "Equip %s") % _recruit_name(recruit), purchase_selected_equipment.bind(recruit_id), "company_secondary", "confirm")
equip.custom_minimum_size = Vector2(180, 50 if size.y < 500.0 else 54)
var affordable := int(guild_company.active_contract.coin) >= int(offer.list_price)
_set_entry_button_disabled(equip, not affordable)
equip.tooltip_text = _tr("装备到 %s;当前:%s", "Equip on %s; current: %s") % [_recruit_name(recruit), _recruit_item_name(recruit)] if affordable else _tr("金币不足:需要 %d,当前 %d;未改变状态", "Insufficient Coin: need %d, have %d; state is unchanged") % [int(offer.list_price), int(guild_company.active_contract.coin)]
target_grid.add_child(equip)
var footer := HBoxContainer.new()
footer.name = "EquipmentReviewFooter"
footer.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 50)
footer.add_theme_constant_override("separation", 8)
layout.add_child(footer)
var cancel := _company_button("EquipmentReviewCancel", _tr("返回市场", "Back to Market"), cancel_equipment_review, "company_utility", "back")
footer.add_child(cancel)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
footer.add_child(spacer)
var state := _label(_tr("选择成员后提交一次", "Choose a Recruit to commit once"), _scaled(10), Color("#e8d39d"))
state.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
footer.add_child(state)
cancel.call_deferred("grab_focus")
func cancel_equipment_review() -> void:
market_notice = ""
show_market(guild_company)
func purchase_selected_equipment(recruit_id: String) -> void:
if market_selected_kind != "equipment" or market_selected_offer_id.is_empty():
market_notice = _tr("没有可提交的装备候选。", "No equipment offer is ready to commit.")
show_market(guild_company)
return
var offer := _market_offer_by_id(guild_company.active_contract, "equipment", market_selected_offer_id)
if offer.is_empty():
market_notice = _tr("该装备候选已不可用。", "That equipment offer is unavailable.")
show_market(guild_company)
return
guild_contract_service.company_path = initial_company_service.company_path
var operation_id := ("%s|equip|%s|%s" % [String(guild_company.company_id), market_selected_offer_id, recruit_id]).sha256_text().substr(0, 32)
var result: Dictionary = guild_contract_service.purchase_equipment(operation_id, market_selected_offer_id, recruit_id, int(offer.list_price))
if not result.ok:
market_notice = _equipment_error_text(String(result.get("code", "EQUIPMENT_FAILED")))
show_equipment_review(market_selected_offer_id)
return
guild_company = result.company.duplicate(true)
market_notice = _tr("已装备 %s · 金币 %d → %d", "Equipped %s · Coin %d -> %d") % [_market_offer_title("equipment", offer), int(result.receipt.coin_before), int(result.receipt.coin_after)]
market_selected_kind = ""
market_selected_offer_id = ""
show_market(guild_company)
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 equipment_count := 4
if not guild_company.is_empty():
equipment_count = guild_company.active_contract.get("available_equipment_offer_ids", []).size()
var title: String = {
"recruit": _tr("招募 3", "Recruit 3"),
"equipment": _tr("装备 %d", "Equipment %d") % equipment_count,
"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)
elif market_selected_kind == "equipment":
var equipment_review := _company_button("MarketReviewEquipment", _tr("查看装备并选择成员", "Review & Choose Wearer"), open_equipment_review, "company_primary", "inspect")
equipment_review.custom_minimum_size = Vector2(0, 50)
equipment_review.tooltip_text = _tr("查看效果、取舍与四名成员的当前装备", "Review effect, tradeoff and the current item on all four Recruits")
context.add_child(equipment_review)
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 _equipment_error_text(code: String) -> String:
match code:
"EQUIPMENT_MARKET_REQUIRED": return _tr("装备只可在市场阶段提交。", "Equipment can only be committed during Market.")
"EQUIPMENT_OFFER_UNAVAILABLE": return _tr("该装备已不可用;未改变状态。", "That equipment offer is unavailable; state is unchanged.")
"EQUIPMENT_PRICE_MISMATCH": return _tr("价格已变化;请重新检查货单。", "The price changed; review the ledger again.")
"EQUIPMENT_INSUFFICIENT_COIN": return _tr("金币不足;未购买装备。", "Insufficient Coin; equipment was not purchased.")
"EQUIPMENT_RECRUIT_UNAVAILABLE": return _tr("该 Recruit 已不可用;未购买装备。", "That Recruit is unavailable; equipment was not purchased.")
"EQUIPMENT_OPERATION_CONFLICT": return _tr("重复操作的目标不同;原装备回执保持不变。", "The repeated operation targets a different intent; the original receipt is unchanged.")
"EQUIPMENT_OFFER_ALREADY_APPLIED_CONFLICT": return _tr("该装备已提交给另一名成员;请选择仍可用的货单。", "That equipment was already committed to another Recruit; choose an available offer.")
return _tr("装备交易未提交;Company 没有改变。", "Equipment transaction was not committed; Company is unchanged.") + " [%s]" % code
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 _dismissal_error_text(code: String) -> String:
match code:
"FINAL_LIVE_RECRUIT": return _tr("最后一名成员不可解雇;Company 没有改变。", "The final live member cannot be dismissed; Company is unchanged.")
"DISMISS_FORMATION_LOCKED": return _tr("队列已锁定;请先返回未准备的 Market。", "The Line is locked; return to an unprepared Market first.")
"DISMISS_MARKET_REQUIRED": return _tr("解雇只在 Market 可用;Company 没有改变。", "Dismissal is only available in Market; Company is unchanged.")
"DISMISS_RECRUIT_UNAVAILABLE": return _tr("该成员已不在 Company;请重新查看成员列表。", "That Recruit is no longer in the Company; review the roster again.")
return _tr("解雇未提交;金币与 Company 没有改变。", "Dismissal was not committed; Coin and Company are unchanged.") + " [%s]" % code
func _contract_return_error_text(code: String) -> String:
match code:
"CONTRACT_RETURN_FORMATION_LOCKED": return _tr("队列已锁定;不能从准备中的委托直接返回。", "The Line is locked; a prepared Contract cannot be returned directly.")
"CONTRACT_RETURN_ALREADY_APPLIED": return _tr("该委托已经返回公会;Company 没有重复改变。", "This Contract has already returned to Guild; Company was not changed again.")
"CONTRACT_RETURN_MARKET_REQUIRED": return _tr("主动返回只在 Market 可用;Company 没有改变。", "Voluntary return is only available in Market; Company is unchanged.")
return _tr("委托未返回;金币、成员与成长保持不变。", "The Contract was not returned; Coin, members and growth remain 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 _battle_enemy_marker(index: int, enemy_id: String, battle: Dictionary) -> Control:
var marker := Control.new()
marker.name = "BattleEnemy%d" % index
marker.custom_minimum_size = Vector2(66, 54 if size.y < 500.0 else 82)
marker.mouse_filter = Control.MOUSE_FILTER_IGNORE
var silhouette := ColorRect.new()
silhouette.anchor_left = 0.24
silhouette.anchor_top = 0.10
silhouette.anchor_right = 0.76
silhouette.anchor_bottom = 0.72
silhouette.color = Color("#7b3f3f") if index == 0 else Color("#5c4e50")
silhouette.mouse_filter = Control.MOUSE_FILTER_IGNORE
marker.add_child(silhouette)
var label := _label("E%d" % (index + 1), _scaled(11), Color("#f5ead0"))
label.anchor_right = 1.0
label.anchor_top = 0.70
label.anchor_bottom = 1.0
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
marker.add_child(label)
return marker
func _battle_actor(position: int, recruit: Dictionary, battle: Dictionary) -> Control:
var actor := Control.new()
actor.name = "BattleActor%d" % position
actor.custom_minimum_size = Vector2(88, 142 if size.y < 500.0 else 220)
actor.size_flags_horizontal = Control.SIZE_EXPAND_FILL
actor.clip_contents = true
var portrait := TextureRect.new()
portrait.name = "BattlePortrait"
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.06
portrait.anchor_top = 0.0
portrait.anchor_right = 0.94
portrait.anchor_bottom = 0.76
portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
actor.add_child(portrait)
var ground := ColorRect.new()
ground.anchor_left = 0.10
ground.anchor_top = 0.74
ground.anchor_right = 0.90
ground.anchor_bottom = 0.79
ground.color = Color("#b7584d") if String(recruit.recruit_id) in Array(battle.get("knocked_out_ids", [])) else Color("#75a884")
ground.mouse_filter = Control.MOUSE_FILTER_IGNORE
actor.add_child(ground)
var position_label := _label(_tr("%d 号", "POS %d") % position, _scaled(9), Color("#e8d39d"))
position_label.anchor_top = 0.78
position_label.anchor_right = 1.0
position_label.anchor_bottom = 0.89
position_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
position_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
actor.add_child(position_label)
var name := _label(_recruit_name(recruit), _scaled(9), Color("#f5ead0"))
name.anchor_top = 0.88
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
actor.add_child(name)
return actor
func _render_battle_event_context(context: VBoxContainer, company: Dictionary, battle: Dictionary) -> void:
context.add_child(_label(_tr("事件投影", "EVENT PROJECTION"), _scaled(10), Color("#e8d39d")))
var events: Array = battle.get("phase_events", [])
if events.is_empty():
context.add_child(_label(_tr("正在等待第一条事件...", "Waiting for the first event..."), _scaled(12), Color("#f5ead0")))
else:
var event: Dictionary = events.back()
var title := _label(_battle_event_text(String(event.get("caption_id", "battle_opening"))), _scaled(16), Color("#f0ca75"))
title.name = "BattleCurrentEvent"
context.add_child(title)
var actor_name := _recruit_name(_recruit_by_id(company, String(event.get("actor_id", "")))) if not String(event.get("actor_id", "")).is_empty() else _tr("敌方队列", "Enemy line")
var target_name := _recruit_name(_recruit_by_id(company, String(event.get("target_id", "")))) if _recruit_by_id(company, String(event.get("target_id", ""))).size() > 0 else _tr("敌方目标", "Enemy target")
var detail := _label(_tr("行动者:%s\n目标:%s\n事件类型:%s", "Actor: %s\nTarget: %s\nEvent: %s") % [actor_name, target_name, String(event.get("kind", ""))], _scaled(11), Color("#f5ead0"))
detail.name = "BattleEventDetail"
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
detail.size_flags_vertical = Control.SIZE_EXPAND_FILL
context.add_child(detail)
var recent: Array[String] = []
for event in events.slice(maxi(0, events.size() - 4), events.size()):
recent.append(_battle_event_text(String(event.get("caption_id", ""))))
var rail := _label("\n".join(recent), _scaled(9), Color("#aebbc2"))
rail.name = "BattleEventRail"
rail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
context.add_child(rail)
func _battle_event_text(caption_id: String) -> String:
match caption_id:
"incoming_threat": return _tr("敌方威胁已公开", "Incoming threat disclosed")
"protection_intercepts": return _tr("保护关系承接压力", "Protection relation intercepts pressure")
"ranged_contact": return _tr("远程接触命中", "Ranged contact lands")
"magic_break": return _tr("魔法破坏命中", "Magic break lands")
"healing_link": return _tr("治疗链接恢复队列", "Healing link restores the line")
"decisive_damage": return _tr("决定性伤害改变结果", "Decisive damage changes the result")
"knockout": return _tr("成员在本战斗中被击倒", "A member is knocked out for this battle")
"line_holds": return _tr("队列守住压力", "The line holds pressure")
"victory": return _tr("胜利结果已准备", "Victory result is ready")
return _tr("战斗开场", "Battle opening")
func _battle_outcome_text(outcome: String) -> String:
return _tr("胜利", "VICTORY") if outcome == "victory" else _tr("失败", "DEFEAT") if outcome == "defeat" else _tr("结果待核对", "RESULT NEEDS REVIEW")
func _battle_result_decisive_event(battle: Dictionary) -> Dictionary:
var result: Dictionary = battle.get("result", {})
var decisive_hash := String(result.get("decisive_event_hash", ""))
for event in battle.get("phase_events", []):
if String(event.get("event_hash", "")) == decisive_hash:
return Dictionary(event)
for event in battle.get("phase_events", []):
if String(event.get("kind", "")) in ["damage", "knockout"]:
return Dictionary(event)
return {}
func _battle_result_knockout_text(company: Dictionary, knockouts: Array) -> String:
if knockouts.is_empty():
return _tr("击倒:无 · 四名成员保留战斗状态", "Knockout: none · all four members retain battle state")
var lines: Array[String] = []
for recruit_id in knockouts:
var recruit := _recruit_by_id(company, String(recruit_id))
if recruit.is_empty():
lines.append(_tr("击倒成员:身份回执缺失", "Knocked-out member: identity receipt missing"))
continue
lines.append(_tr("击倒:%s · %s · 仅本战斗离场,下关归队", "Knocked out: %s · %s · battle-only absence; returns next stage") % [_recruit_name(recruit), _recruit_profession_name(recruit)])
return "\n".join(lines)
func _battle_result_chain_summary(battle: Dictionary) -> String:
var events: Array = battle.get("phase_events", [])
if events.is_empty():
return _tr("因果数据缺失。结果保持暂停。", "Causal data is missing. Result remains paused.")
var lines: Array[String] = []
for event in events:
if String(event.get("kind", "")) in ["opening", "damage", "knockout", "guarded_recovery", "result"]:
lines.append(_battle_event_text(String(event.get("caption_id", ""))))
return " > ".join(lines)
func _battle_result_diagnosis_text(company: Dictionary, battle: Dictionary) -> String:
var decisive := _battle_result_decisive_event(battle)
var result: Dictionary = battle.get("result", {})
var lines: Array[String] = []
lines.append(_tr("提交队列:%d 名成员 · 敌方:%d 名", "Committed line: %d members · enemy line: %d") % [battle.get("front_to_rear_ids", []).size(), battle.get("enemy_ids", []).size()])
lines.append(_tr("首要目标:0 号前排 · 决定性事件:%s", "First target: position 0 front · decisive event: %s") % _battle_event_text(String(decisive.get("caption_id", ""))))
lines.append(_tr("事件 hash:%s", "Event hash: %s") % String(decisive.get("event_hash", "")).substr(0, 12))
lines.append(_tr("奖励回执:%d 金币 · XP 总计 %d", "Reward receipt: %d Coin · %d total XP") % [int(result.get("reward_coin", 0)), int(result.get("xp_award", 0))])
return "\n".join(lines)
func _battle_error_text(code: String) -> String:
match code:
"BATTLE_READINESS_REQUIRED": return _tr("请先接受准备检查。", "Accept Readiness first.")
"BATTLE_RISK_UNAVAILABLE": return _tr("该风险选项已失效,请返回风险板。", "That risk option is stale; return to the Risk Board.")
"BATTLE_ALREADY_ACTIVE": return _tr("已有进行中的战斗;已恢复原战斗回执。", "A battle is already active; its original receipt was restored.")
"RECOVERY_RESULT_REQUIRED": return _tr("结果回执尚未完成,无法恢复下一阶段。", "The result receipt is not ready for next-stage recovery.")
"recovery_already_applied": return _tr("恢复已提交;正在打开下一阶段市场。", "Recovery was already committed; opening the next-stage Market.")
return _tr("战斗回执未能创建;远征团没有改变。", "The battle receipt could not be created; Company state is unchanged.") + " [%s]" % code
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 _recruit_profession_name(recruit: Dictionary) -> String:
var localized: Dictionary = recruit.get("profession_name", {})
var locale := String(settings.get("locale", "zh_CN"))
return String(localized.get(locale, localized.get("en", recruit.get("profession_id", "Unknown Profession"))))
func _recruit_trait_name(recruit: Dictionary) -> String:
var localized: Dictionary = recruit.get("trait_name", {})
var locale := String(settings.get("locale", "zh_CN"))
return String(localized.get(locale, localized.get("en", recruit.get("trait_id", "Unknown Trait"))))
func _recruit_item_name(recruit: Dictionary) -> String:
if String(recruit.get("equipped_item_id", "")) != String(recruit.get("starting_item_id", "")):
return _content_name("equipment", String(recruit.get("equipped_item_id", "")))
var localized: Dictionary = recruit.get("starting_item_name", {})
var locale := String(settings.get("locale", "zh_CN"))
return String(localized.get(locale, localized.get("en", recruit.get("equipped_item_id", "Unknown Equipment"))))
func _recovery_identity_lines(company: Dictionary, recruit_ids: Array) -> Array[String]:
var lines: Array[String] = []
for recruit_id in recruit_ids:
var recruit := _recruit_by_id(company, String(recruit_id))
if recruit.is_empty():
lines.append(_tr("身份回执缺失:%s", "Identity receipt missing: %s") % String(recruit_id))
continue
lines.append(_tr("%s · %s · ID %s", "%s · %s · ID %s") % [_recruit_name(recruit), _recruit_profession_name(recruit), String(recruit.recruit_id).substr(0, 8)])
return lines
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 == "roster":
back_to_guild()
elif current_screen == "recruit_detail":
show_roster(guild_company)
elif current_screen == "dismissal_review":
cancel_dismissal_review()
elif current_screen == "contract_return_review":
cancel_contract_return()
elif current_screen in ["contract_choice", "market"]:
back_to_guild()
elif current_screen == "equipment_review":
cancel_equipment_review()
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 == "battle":
toggle_battle_pause()
elif current_screen == "battle_result":
get_viewport().set_input_as_handled()
show_battle_result(guild_company)
elif current_screen == "battle_knockout_review":
get_viewport().set_input_as_handled()
show_battle_result(guild_company)
elif current_screen == "battle_recovery":
get_viewport().set_input_as_handled()
show_battle_knockout_review(guild_company)
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